@max-null/dsh-memory 0.6.0 → 0.7.0

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/client.js CHANGED
@@ -94,11 +94,11 @@ var BRAIN_PATHS = [
94
94
  ];
95
95
  function brainIcon() {
96
96
  return (0, import_react.createElement)("svg", {
97
- width: 15,
98
- height: 15,
97
+ width: 22,
98
+ height: 22,
99
99
  viewBox: "0 0 24 24",
100
100
  fill: "none",
101
- stroke: "currentColor",
101
+ stroke: "#8b5cf6",
102
102
  strokeWidth: 2,
103
103
  strokeLinecap: "round",
104
104
  strokeLinejoin: "round"
@@ -175,26 +175,34 @@ var STRINGS = {
175
175
  promptFilterAll: "\u5168\u90E8",
176
176
  memorySearch: "\u641C\u7D22\u8BB0\u5FC6\u2026",
177
177
  empty: "\u9ED1\u6697\u4E2D\u672A\u89C1\u7075\u5149",
178
- confirm: "\u786E\u8BA4",
178
+ confirm: "\u653E\u884C",
179
179
  forget: "\u5220\u9664",
180
- groupPending: "\u5F85\u5BA1\u6838",
181
- groupOnDemand: "\u5DF2\u5BA1\u6838 \xB7 \u6309\u9700",
180
+ groupQuarantine: "\u9694\u79BB\u533A",
181
+ groupQuarantineHint: "\u547D\u4E2D\u5371\u9669\u5185\u5BB9\u89C4\u5219\uFF08\u5BC6\u94A5/\u51ED\u636E\u7C7B\uFF09\u2192 \u4E0D\u8FDB\u6CE8\u5165\u3001\u4E0D\u8FDB\u68C0\u7D22\u3002\u653E\u884C\u540E\u6309\u666E\u901A\u8BB0\u5FC6\u5904\u7406\u3002",
182
+ quarantineReason: "\u89C4\u5219\uFF1A{reason}",
183
+ release: "\u653E\u884C",
184
+ blockedWhileQuarantined: "\u9694\u79BB\u4E2D \xB7 \u653E\u884C\u540E\u53EF\u5E38\u9A7B\u6CE8\u5165",
185
+ groupPending: "\u5F85\u653E\u884C",
186
+ groupOnDemand: "\u6309\u9700\u68C0\u7D22",
182
187
  groupInjected: "\u5E38\u9A7B\u6CE8\u5165",
188
+ groupCold: "\u51B7\u6570\u636E\xB7\u6298\u53E0\uFF08{days} \u5929\u672A\u68C0\u7D22\uFF09",
183
189
  injectSwitch: "\u5E38\u9A7B\u6CE8\u5165",
184
- approveFirst: "\u5BA1\u6838\u901A\u8FC7\u540E\u53EF\u5E38\u9A7B\u6CE8\u5165",
190
+ approveFirst: "\u5148\u653E\u884C\u9694\u79BB\u624D\u80FD\u5E38\u9A7B\u6CE8\u5165",
185
191
  allNamespaces: "\u5168\u90E8",
186
192
  nsGlobal: "\u5168\u5C40",
187
193
  nsWorkspace: "\u5DE5\u4F5C\u533A",
188
194
  noWorkspace: "\u672A\u9009\u62E9\u5DE5\u4F5C\u533A",
189
195
  organizeMemory: "\u6574\u7406\u8BB0\u5FC6",
190
- confirmAll: "\u5168\u90E8\u786E\u8BA4",
196
+ confirmAll: "\u5168\u90E8\u653E\u884C",
191
197
  injectPreview: "\u6CE8\u5165\u9884\u89C8",
192
198
  contextUsage: "\u4E0A\u4E0B\u6587\u5360\u7528",
193
199
  keywordsLabel: "\u5173\u952E\u8BCD",
194
- suggested: "\u5F85\u5BA1\u6838",
195
- approved: "\u5DF2\u5BA1\u6838",
200
+ suggested: "\u9694\u79BB",
201
+ approved: "\u5DF2\u751F\u6548",
196
202
  refresh: "\u5237\u65B0",
197
- coldHint: "\u51B7\u6570\u636E \xB7 \u5EFA\u8BAE\u6309\u9700\u5316"
203
+ coldHint: "\u51B7\u6570\u636E",
204
+ lastUsedAt: "\u4E0A\u641C",
205
+ hitCount: "\u547D\u4E2D {count} \u6B21"
198
206
  },
199
207
  en: {
200
208
  tabMemory: "Memory",
@@ -220,26 +228,34 @@ var STRINGS = {
220
228
  promptFilterAll: "All",
221
229
  memorySearch: "Search memory\u2026",
222
230
  empty: "No spark in the dark",
223
- confirm: "Confirm",
231
+ confirm: "Release",
224
232
  forget: "Forget",
225
- groupPending: "Pending review",
226
- groupOnDemand: "Approved \xB7 on demand",
233
+ groupQuarantine: "Quarantine",
234
+ groupQuarantineHint: "Flagged by a dangerous-content rule (secrets/credentials) \u2192 kept out of injection and recall. Releasing it makes it an ordinary memory.",
235
+ quarantineReason: "Rule: {reason}",
236
+ release: "Release",
237
+ blockedWhileQuarantined: "Quarantined \xB7 release to allow injection",
238
+ groupPending: "Pending release",
239
+ groupOnDemand: "On demand",
227
240
  groupInjected: "Always injected",
241
+ groupCold: "Cold \xB7 collapsed ({days} days untouched)",
228
242
  injectSwitch: "Inject every turn",
229
- approveFirst: "Approve to enable injection",
243
+ approveFirst: "Release the quarantine first",
230
244
  allNamespaces: "All",
231
245
  nsGlobal: "Global",
232
246
  nsWorkspace: "Workspace",
233
247
  noWorkspace: "No workspace selected",
234
248
  organizeMemory: "Organize memory",
235
- confirmAll: "Approve all",
249
+ confirmAll: "Release all",
236
250
  injectPreview: "Injection preview",
237
251
  contextUsage: "Context usage",
238
252
  keywordsLabel: "Keywords",
239
- suggested: "Suggested",
240
- approved: "Approved",
253
+ suggested: "Quarantined",
254
+ approved: "Active",
241
255
  refresh: "Refresh",
242
- coldHint: "Cold \xB7 consider on-demand"
256
+ coldHint: "Cold",
257
+ lastUsedAt: "Recalled",
258
+ hitCount: "{count} hits"
243
259
  }
244
260
  };
245
261
  var localeId = "zh";
@@ -356,7 +372,7 @@ var memoryApi = {
356
372
  return api("prompt.refresh", { ...cwd === void 0 ? {} : { cwd } });
357
373
  }
358
374
  };
359
- var ORGANIZE_PROMPT = "\u8BF7\u6574\u7406\u6211\u7684\u8BB0\u5FC6\u5E93\uFF1A\u7528 memory_list \u67E5\u770B\u5168\u90E8\u8BB0\u5FC6\u3002\u6574\u7406\u89C4\u5219\uFF08\u5FC5\u987B\u6267\u884C\uFF09\uFF1A\u2460\u540C\u7C7B\u6761\u76EE\u5408\u5E76\u2014\u2014\u5DE5\u4F5C\u4E60\u60EF/\u7EA6\u5B9A\u7C7B\u591A\u6761\u5408\u5E76\u4E3A\u4E00\u6761\uFF08\u5185\u5BB9\u7528 \u2460\u2461\u2462 \u5E76\u5217\uFF0C\u907F\u514D\u788E\u7247\u5316\uFF09\uFF1B\u2461\u4E0E\u300C[\u8BB0\u5FC6\u7CFB\u7EDF\u81EA\u8FF0]\u300D\uFF08\u4F60\u4E0A\u4E0B\u6587\u4E2D\u7684\u8BB0\u5FC6\u673A\u5236\u8BF4\u660E\uFF09\u5185\u5BB9\u91CD\u590D\u7684\u63D2\u4EF6\u4ECB\u7ECD\u6761\u76EE\uFF08\u5982 dsh-memory \u63D2\u4EF6\u53D1\u5E03\u4FE1\u606F\uFF09\u5E94\u5220\u9664\u2014\u2014\u673A\u5236\u8BF4\u660E\u5DF2\u7531\u7CFB\u7EDF\u5E38\u9A7B\u63D0\u4F9B\uFF0C\u65E0\u9700\u7528\u6237\u5B58\u50A8\uFF1B\u2462\u5BF9\u8FC7\u65F6\u3001\u9519\u8BEF\u6216\u5DF2\u53D8\u5316\u7684\u5185\u5BB9\u7528 memory_update \u4FEE\u6B63\uFF08\u4F1A\u91CD\u7F6E\u4E3A\u5F85\u5BA1\u6838\uFF09\uFF1B\u2463\u7CBE\u7B80\u5197\u957F\u5185\u5BB9\uFF0C\u4E3A\u6BCF\u6761\u8865\u5145\u6216\u4FEE\u6B63 keywords\uFF1B\u2464\u9700\u8981\u5220\u9664\u7684\u7528 memory_forget\uFF0C\u9700\u8981\u65B0\u589E\u7684\u7528 memory_save\u3002\u5224\u65AD\u5185\u5BB9\u662F\u5426\u8FC7\u65F6\u7684\u65B9\u6CD5\uFF1A\u628A\u8BB0\u5FC6\u91CC\u63D0\u5230\u7684\u5DE5\u5177\u540D/\u6570\u91CF\u4E0E\u4F60\u5F53\u524D\u5B9E\u9645\u53EF\u7528\u7684\u8BB0\u5FC6\u5DE5\u5177\u5BF9\u7167\u2014\u2014\u4F60\u5F53\u524D\u53EF\u7528\uFF1Amemory_save / memory_list / memory_search / memory_confirm / memory_forget / memory_update\uFF08\u5171 6 \u4E2A\uFF09\uFF1B\u82E5\u8BB0\u5FC6\u4E2D\u7684\u5DE5\u5177\u5217\u8868\u3001\u6570\u91CF\u3001\u6D41\u7A0B\u4E0E\u6B64\u4E0D\u7B26\u5373\u4E3A\u8FC7\u65F6\uFF0C\u7528 memory_update \u4FEE\u6B63\u3002\u6539\u52A8\u5168\u90E8\u843D\u5728 suggested \u7B49\u5F85\u5BA1\u6838\uFF08\u4E0D\u8981\u8C03\u7528 memory_confirm\uFF09\uFF0C\u5B8C\u6210\u540E\u7528\u4E00\u53E5\u8BDD\u6C47\u62A5\u6574\u7406\u7ED3\u679C\u3002";
375
+ var ORGANIZE_PROMPT = "\u8BF7\u6574\u7406\u6211\u7684\u8BB0\u5FC6\u5E93\uFF1A\u7528 memory_list \u67E5\u770B\u5168\u90E8\u8BB0\u5FC6\u3002\u6574\u7406\u89C4\u5219\uFF08\u5FC5\u987B\u6267\u884C\uFF09\uFF1A\u2460\u540C\u7C7B\u6761\u76EE\u5408\u5E76\u2014\u2014\u5DE5\u4F5C\u4E60\u60EF/\u7EA6\u5B9A\u7C7B\u591A\u6761\u5408\u5E76\u4E3A\u4E00\u6761\uFF08\u5185\u5BB9\u7528 \u2460\u2461\u2462 \u5E76\u5217\uFF0C\u907F\u514D\u788E\u7247\u5316\uFF09\uFF1B\u2461\u4E0E\u300C[\u8BB0\u5FC6\u7CFB\u7EDF\u81EA\u8FF0]\u300D\uFF08\u4F60\u4E0A\u4E0B\u6587\u4E2D\u7684\u8BB0\u5FC6\u673A\u5236\u8BF4\u660E\uFF09\u5185\u5BB9\u91CD\u590D\u7684\u63D2\u4EF6\u4ECB\u7ECD\u6761\u76EE\uFF08\u5982 dsh-memory \u63D2\u4EF6\u53D1\u5E03\u4FE1\u606F\uFF09\u5E94\u5220\u9664\u2014\u2014\u673A\u5236\u8BF4\u660E\u5DF2\u7531\u7CFB\u7EDF\u5E38\u9A7B\u63D0\u4F9B\uFF0C\u65E0\u9700\u7528\u6237\u5B58\u50A8\uFF1B\u2462\u5BF9\u8FC7\u65F6\u3001\u9519\u8BEF\u6216\u5DF2\u53D8\u5316\u7684\u5185\u5BB9\u7528 memory_update \u4FEE\u6B63\uFF08\u6539\u52A8\u751F\u6548\u3001\u6CE8\u5165\u5F00\u5173\u4FDD\u7559\uFF09\uFF1B\u2463\u7CBE\u7B80\u5197\u957F\u5185\u5BB9\uFF0C\u4E3A\u6BCF\u6761\u8865\u5145\u6216\u4FEE\u6B63 keywords\uFF1B\u2464\u9700\u8981\u5220\u9664\u7684\u7528 memory_forget\uFF0C\u9700\u8981\u65B0\u589E\u7684\u7528 memory_save\u3002\u5224\u65AD\u5185\u5BB9\u662F\u5426\u8FC7\u65F6\u7684\u65B9\u6CD5\uFF1A\u628A\u8BB0\u5FC6\u91CC\u63D0\u5230\u7684\u5DE5\u5177\u540D/\u6570\u91CF\u4E0E\u4F60\u5F53\u524D\u5B9E\u9645\u53EF\u7528\u7684\u8BB0\u5FC6\u5DE5\u5177\u5BF9\u7167\u2014\u2014\u4F60\u5F53\u524D\u53EF\u7528\uFF1Amemory_save / memory_list / memory_search / memory_confirm / memory_forget / memory_update\uFF08\u5171 6 \u4E2A\uFF09\uFF1B\u82E5\u8BB0\u5FC6\u4E2D\u7684\u5DE5\u5177\u5217\u8868\u3001\u6570\u91CF\u3001\u6D41\u7A0B\u4E0E\u6B64\u4E0D\u7B26\u5373\u4E3A\u8FC7\u65F6\uFF0C\u7528 memory_update \u4FEE\u6B63\u3002\u6539\u52A8\u76F4\u63A5\u751F\u6548\uFF08\u4E0D\u8981\u8C03\u7528 memory_confirm\uFF09\uFF0C\u5B8C\u6210\u540E\u7528\u4E00\u53E5\u8BDD\u6C47\u62A5\u6574\u7406\u7ED3\u679C\u3002";
360
376
  function insertIntoInput(ctx, text) {
361
377
  const sessions = ctx.get?.("sessions");
362
378
  const conversation = ctx.get?.("conversation");
@@ -585,6 +601,9 @@ function MemoryView(props) {
585
601
  const t = useT();
586
602
  const [mode, setMode] = (0, import_react.useState)("mem");
587
603
  const [records, setRecords] = (0, import_react.useState)([]);
604
+ const [quarantined, setQuarantined] = (0, import_react.useState)([]);
605
+ const [coldOpen, setColdOpen] = (0, import_react.useState)(false);
606
+ const [quarantineOpen, setQuarantineOpen] = (0, import_react.useState)(true);
588
607
  const [query, setQuery] = (0, import_react.useState)("");
589
608
  const [namespace, setNamespace] = (0, import_react.useState)(null);
590
609
  const [refreshing, setRefreshing] = (0, import_react.useState)(false);
@@ -608,11 +627,16 @@ function MemoryView(props) {
608
627
  setRecords(await memoryApi.list({}, props.cwd));
609
628
  } catch {
610
629
  }
630
+ try {
631
+ setQuarantined(await memoryApi.list({ quarantined: true }, props.cwd));
632
+ } catch {
633
+ }
611
634
  };
612
635
  const refreshFromDisk = async () => {
613
636
  setRefreshing(true);
614
637
  try {
615
638
  setRecords(await memoryApi.reload(props.cwd));
639
+ setQuarantined(await memoryApi.list({ quarantined: true }, props.cwd));
616
640
  } catch {
617
641
  await reload();
618
642
  } finally {
@@ -623,15 +647,22 @@ function MemoryView(props) {
623
647
  if (props.visible) void reload();
624
648
  }, [props.visible]);
625
649
  const toggleInjected = async (record) => {
626
- if (record.status !== "approved") return;
650
+ if (record.status !== "approved" || record.quarantined) return;
627
651
  try {
628
652
  await memoryApi.setInjected(record.id, !record.injected, props.cwd);
629
653
  } catch {
630
654
  }
631
655
  await reload();
632
656
  };
657
+ const release = async (record) => {
658
+ try {
659
+ await memoryApi.confirm(record.id, props.cwd);
660
+ } catch {
661
+ }
662
+ await reload();
663
+ };
633
664
  const confirmAll = async () => {
634
- const pending = records.filter((record) => record.status === "suggested");
665
+ const pending = records.filter((record) => record.status === "suggested" && !record.quarantined);
635
666
  if (pending.length === 0) return;
636
667
  await Promise.all(pending.map((record) => memoryApi.confirm(record.id, props.cwd).catch(() => null)));
637
668
  await reload();
@@ -671,16 +702,180 @@ function MemoryView(props) {
671
702
  const q = query.trim().toLowerCase();
672
703
  const byNs = namespace === null ? records : namespace === "workspace" ? records.filter((record) => record.namespace === "project") : records.filter((record) => record.namespace === "global");
673
704
  const filtered = byNs.filter((record) => q === "" || record.content.toLowerCase().includes(q));
705
+ const quarantinedScoped = (namespace === null ? quarantined : namespace === "workspace" ? quarantined.filter((record) => record.namespace === "project") : quarantined.filter((record) => record.namespace === "global")).filter((record) => q === "" || record.content.toLowerCase().includes(q));
674
706
  const nsCounts = {
675
- all: records.length,
676
- global: records.filter((record) => record.namespace === "global").length,
677
- workspace: records.filter((record) => record.namespace === "project").length
707
+ all: records.length + quarantined.length,
708
+ global: records.filter((record) => record.namespace === "global").length + quarantined.filter((record) => record.namespace === "global").length,
709
+ workspace: records.filter((record) => record.namespace === "project").length + quarantined.filter((record) => record.namespace === "project").length
678
710
  };
711
+ const coldNow = Date.now();
712
+ const settled = (excludeCold) => excludeCold ? filtered.filter((record) => !isCold(record.lastUsedAt, coldNow)) : filtered;
679
713
  const groups = [
680
- { key: "pending", label: t("groupPending"), items: filtered.filter((record) => record.status === "suggested") },
681
- { key: "ondemand", label: t("groupOnDemand"), items: filtered.filter((record) => record.status === "approved" && !record.injected) },
682
- { key: "injected", label: t("groupInjected"), items: filtered.filter((record) => record.status === "approved" && record.injected) }
714
+ { key: "pending", label: t("groupPending"), items: settled(false).filter((record) => record.status === "suggested") },
715
+ { key: "injected", label: t("groupInjected"), items: settled(true).filter((record) => record.status === "approved" && record.injected) },
716
+ { key: "ondemand", label: t("groupOnDemand"), items: settled(true).filter((record) => record.status === "approved" && !record.injected) }
683
717
  ].filter((group) => group.items.length > 0);
718
+ const coldItems = settled(false).filter((record) => record.status === "approved" && isCold(record.lastUsedAt, coldNow));
719
+ const memoryCard = (record, options = {}) => {
720
+ const isQuarantined = options.quarantined === true || record.quarantined;
721
+ const injectable = record.status === "approved" && !isQuarantined;
722
+ return (0, import_react.createElement)(
723
+ "div",
724
+ {
725
+ key: record.id,
726
+ style: {
727
+ ...ssid.card,
728
+ // 隔离区显眼但不吓人:左边一条警示色,不换底色(安全网,不是错误)
729
+ ...isQuarantined ? { borderLeft: "3px solid var(--dsw-alias-state-warning-primary, #f7c94f)" } : {}
730
+ }
731
+ },
732
+ (0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
733
+ // 隔离原因:命中的规则名(安全网,只陈述事实,不做道德评判)
734
+ isQuarantined ? (0, import_react.createElement)(
735
+ "div",
736
+ { style: { ...ssid.muted, marginTop: 6, display: "flex", flexWrap: "wrap", gap: 4, alignItems: "center" } },
737
+ (0, import_react.createElement)("span", {
738
+ style: {
739
+ fontSize: 10,
740
+ padding: "1px 7px",
741
+ borderRadius: 8,
742
+ fontWeight: 600,
743
+ background: "var(--dsw-alias-state-warning-tertiary, rgba(247,201,79,.16))",
744
+ color: "var(--dsw-alias-state-warning-primary, #f7c94f)"
745
+ }
746
+ }, t("groupQuarantine")),
747
+ (0, import_react.createElement)("span", null, record.quarantineReason === void 0 || record.quarantineReason === "" ? t("quarantineReason", { reason: "\u2014" }) : t("quarantineReason", { reason: record.quarantineReason }))
748
+ ) : null,
749
+ // keywords 展示(0.3.5:设计约定 UI 显示 keywords)
750
+ record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
751
+ "div",
752
+ { style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
753
+ record.keywords.map((keyword) => (0, import_react.createElement)("span", {
754
+ key: keyword,
755
+ style: {
756
+ fontSize: 10,
757
+ padding: "1px 7px",
758
+ borderRadius: 8,
759
+ background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
760
+ color: "var(--dsw-alias-label-secondary, #67748a)"
761
+ }
762
+ }, keyword))
763
+ ) : null,
764
+ (0, import_react.createElement)(
765
+ "div",
766
+ { style: { ...ssid.muted, marginTop: 6 } },
767
+ `${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}${record.lastUsedAt !== void 0 ? ` \xB7 ${t("lastUsedAt")} ${fmtDate(record.lastUsedAt)}` : ""}${isCold(record.lastUsedAt, coldNow) ? ` \xB7 ${t("coldHint")}` : ""}${record.hitCount > 0 ? ` \xB7 ${t("hitCount", { count: record.hitCount })}` : ""}`
768
+ ),
769
+ (0, import_react.createElement)(
770
+ "div",
771
+ { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
772
+ (0, import_react.createElement)("button", {
773
+ type: "button",
774
+ title: isQuarantined ? t("blockedWhileQuarantined") : record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
775
+ disabled: !injectable,
776
+ onClick: () => {
777
+ void toggleInjected(record);
778
+ },
779
+ style: {
780
+ ...ssid.btn,
781
+ ...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
782
+ opacity: injectable ? 1 : 0.4,
783
+ cursor: injectable ? "pointer" : "not-allowed"
784
+ }
785
+ }, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
786
+ isQuarantined ? (0, import_react.createElement)("button", {
787
+ style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent },
788
+ onClick: () => {
789
+ void release(record);
790
+ }
791
+ }, t("release")) : null,
792
+ record.status === "suggested" && !isQuarantined ? (0, import_react.createElement)("button", {
793
+ style: ssid.btn,
794
+ onClick: () => {
795
+ void memoryApi.confirm(record.id, props.cwd).then(() => reload());
796
+ }
797
+ }, t("confirm")) : null,
798
+ (0, import_react.createElement)("button", {
799
+ style: ssid.btn,
800
+ onClick: () => {
801
+ void memoryApi.forget(record.id, props.cwd).then(() => reload());
802
+ }
803
+ }, t("forget"))
804
+ )
805
+ );
806
+ };
807
+ const foldHeader = (label, count, open, toggle) => (0, import_react.createElement)(
808
+ "div",
809
+ {
810
+ style: { ...ssid.title, cursor: "pointer", marginBottom: 0 },
811
+ onClick: toggle,
812
+ onKeyDown: (event) => {
813
+ if (event.key === "Enter" || event.key === " ") toggle();
814
+ },
815
+ role: "button",
816
+ tabIndex: 0
817
+ },
818
+ (0, import_react.createElement)("span", null, `${open ? "\u25BE" : "\u25B8"} ${label}`),
819
+ (0, import_react.createElement)("span", { style: ssid.count }, String(count))
820
+ );
821
+ const quarantineSection = quarantinedScoped.length === 0 ? null : (0, import_react.createElement)(
822
+ "div",
823
+ { key: "quarantine", style: { display: "flex", flexDirection: "column", gap: 6 } },
824
+ foldHeader(t("groupQuarantine"), quarantinedScoped.length, quarantineOpen, () => {
825
+ setQuarantineOpen((open) => !open);
826
+ }),
827
+ (0, import_react.createElement)("div", { style: { ...ssid.muted, fontSize: 10.5 } }, t("groupQuarantineHint")),
828
+ quarantineOpen ? (0, import_react.createElement)(
829
+ "div",
830
+ { style: { display: "flex", flexDirection: "column", gap: 6 } },
831
+ quarantinedScoped.map((record) => memoryCard(record, { quarantined: true }))
832
+ ) : null
833
+ );
834
+ const coldSection = coldItems.length === 0 ? null : (0, import_react.createElement)(
835
+ "div",
836
+ { key: "cold", style: { display: "flex", flexDirection: "column", gap: 6 } },
837
+ foldHeader(t("groupCold", { days: COLD_DAYS }), coldItems.length, coldOpen, () => {
838
+ setColdOpen((open) => !open);
839
+ }),
840
+ coldOpen ? (0, import_react.createElement)(
841
+ "div",
842
+ { style: { display: "flex", flexDirection: "column", gap: 6 } },
843
+ coldItems.map((record) => memoryCard(record))
844
+ ) : null
845
+ );
846
+ const groupSections = groups.map((group) => (0, import_react.createElement)(
847
+ "div",
848
+ {
849
+ key: group.key,
850
+ style: { display: "flex", flexDirection: "column", gap: 6 }
851
+ },
852
+ (0, import_react.createElement)(
853
+ "div",
854
+ { style: ssid.title },
855
+ (0, import_react.createElement)("span", null, group.label),
856
+ (0, import_react.createElement)(
857
+ "div",
858
+ { style: { display: "flex", alignItems: "center", gap: 6 } },
859
+ group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
860
+ type: "button",
861
+ title: t("confirmAll"),
862
+ onClick: () => {
863
+ void confirmAll();
864
+ },
865
+ style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
866
+ }, t("confirmAll")) : null,
867
+ (0, import_react.createElement)("span", { style: ssid.count }, String(group.items.length))
868
+ )
869
+ ),
870
+ group.items.map((record) => memoryCard(record))
871
+ ));
872
+ const listSection = quarantineSection === null && groupSections.length === 0 && coldSection === null ? (0, import_react.createElement)("div", { key: "empty", style: ssid.empty }, t("empty")) : (0, import_react.createElement)(
873
+ "div",
874
+ { key: "groups", style: { display: "flex", flexDirection: "column", gap: 12 } },
875
+ quarantineSection,
876
+ groupSections,
877
+ coldSection
878
+ );
684
879
  const modeTabs = (0, import_react.createElement)(
685
880
  "div",
686
881
  { style: { display: "flex", gap: 4 } },
@@ -799,83 +994,7 @@ function MemoryView(props) {
799
994
  preview.lines.length === 0 ? (0, import_react.createElement)("div", { style: ssid.muted }, t("empty")) : preview.lines.map((line, index) => (0, import_react.createElement)("div", { key: index, style: { ...ssid.muted, fontSize: 11 } }, line))
800
995
  ) : null,
801
996
  // 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
802
- namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : groups.length === 0 ? (0, import_react.createElement)("div", { style: ssid.empty }, t("empty")) : groups.map((group) => (0, import_react.createElement)(
803
- "div",
804
- { key: group.key, style: { display: "flex", flexDirection: "column", gap: 6 } },
805
- (0, import_react.createElement)(
806
- "div",
807
- { style: ssid.title },
808
- (0, import_react.createElement)("span", null, group.label),
809
- (0, import_react.createElement)(
810
- "div",
811
- { style: { display: "flex", alignItems: "center", gap: 6 } },
812
- group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
813
- type: "button",
814
- title: t("confirmAll"),
815
- onClick: () => {
816
- void confirmAll();
817
- },
818
- style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
819
- }, t("confirmAll")) : null,
820
- (0, import_react.createElement)("span", { style: ssid.count }, String(group.items.length))
821
- )
822
- ),
823
- group.items.map((record) => (0, import_react.createElement)(
824
- "div",
825
- { key: record.id, style: ssid.card },
826
- (0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
827
- // keywords 展示(0.3.5:设计约定 UI 显示 keywords)
828
- record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
829
- "div",
830
- { style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
831
- record.keywords.map((keyword) => (0, import_react.createElement)("span", {
832
- key: keyword,
833
- style: {
834
- fontSize: 10,
835
- padding: "1px 7px",
836
- borderRadius: 8,
837
- background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
838
- color: "var(--dsw-alias-label-secondary, #67748a)"
839
- }
840
- }, keyword))
841
- ) : null,
842
- (0, import_react.createElement)(
843
- "div",
844
- { style: { ...ssid.muted, marginTop: 6 } },
845
- `${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}${record.lastUsedAt !== void 0 ? ` \xB7 \u4E0A\u641C ${fmtDate(record.lastUsedAt)}` : ""}${isCold(record.lastUsedAt) ? ` \xB7 ${t("coldHint")}` : ""}`
846
- ),
847
- (0, import_react.createElement)(
848
- "div",
849
- { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
850
- (0, import_react.createElement)("button", {
851
- type: "button",
852
- title: record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
853
- disabled: record.status !== "approved",
854
- onClick: () => {
855
- void toggleInjected(record);
856
- },
857
- style: {
858
- ...ssid.btn,
859
- ...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
860
- opacity: record.status !== "approved" ? 0.4 : 1,
861
- cursor: record.status !== "approved" ? "not-allowed" : "pointer"
862
- }
863
- }, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
864
- record.status === "suggested" ? (0, import_react.createElement)("button", {
865
- style: ssid.btn,
866
- onClick: () => {
867
- void memoryApi.confirm(record.id, props.cwd).then(() => reload());
868
- }
869
- }, t("confirm")) : null,
870
- (0, import_react.createElement)("button", {
871
- style: ssid.btn,
872
- onClick: () => {
873
- void memoryApi.forget(record.id, props.cwd).then(() => reload());
874
- }
875
- }, t("forget"))
876
- )
877
- ))
878
- ))
997
+ namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : listSection
879
998
  )
880
999
  );
881
1000
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 有效性锚点(2026-09-15 静默记忆机制):把一条记忆绑定到某个**可探测的环境值**上,
3
+ * 值变了就自动失效——解决「版本特性类记忆随环境升级静默变错」的问题。
4
+ *
5
+ * 第一版只做**进程内可探测**的类型:校验跑在会话启动的路径上,探测必须零 IO。
6
+ * 需要跑子进程才能拿到的(如 PowerShell 版本)留待后续——那要先解决「插件直接
7
+ * spawn 会绕过 DSH 的 shell 沙箱」的合规问题,不能顺手开这个口子。
8
+ */
9
+ /** 受控词汇表:写入时只能从这三种里选,不能自创(自创的锚点无法机械校验)。 */
10
+ export type AnchorKind = 'env' | 'tool-list' | 'self-version';
11
+ /** 一条记忆声明的锚点。 */
12
+ export interface MemoryAnchor {
13
+ kind: AnchorKind;
14
+ /** `env` 类型时的变量名;其余类型省略。 */
15
+ name?: string;
16
+ /** 写入时探测到的值。 */
17
+ value: string;
18
+ }
19
+ /** 探测上下文:由调用方提供,便于测试注入与缓存。 */
20
+ export interface AnchorProbes {
21
+ /**
22
+ * 当前可用工具的清单;**拿不到时必须是 `undefined` 而不是空数组**——空数组会被判成
23
+ * 「工具全没了」,让所有 `tool-list` 锚点误失效。
24
+ */
25
+ toolNames?: readonly string[];
26
+ /** dsh-memory 自身版本。 */
27
+ selfVersion: string;
28
+ /** 环境变量读取器;缺省 `process.env`。 */
29
+ env?: (name: string) => string | undefined;
30
+ }
31
+ /**
32
+ * 探测某个锚点当前的取值。
33
+ *
34
+ * @param anchor - 记忆声明的锚点。
35
+ * @param probes - 当前环境的探测上下文。
36
+ * @returns 当前值;**无法探测时返回 `undefined`**(调用方按「未校验」处理,不能判失效)。
37
+ */
38
+ export declare function probeAnchor(anchor: MemoryAnchor, probes: AnchorProbes): string | undefined;
39
+ /**
40
+ * 锚点是否仍然成立。
41
+ *
42
+ * @returns `true` 成立;`false` 失效;`undefined` **未校验**(探测不到,不判失效——探测
43
+ * 失败与「值变了」是两件事,混起来会误伤)。
44
+ */
45
+ export declare function anchorHolds(anchor: MemoryAnchor, probes: AnchorProbes): boolean | undefined;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 有效性锚点(2026-09-15 静默记忆机制):把一条记忆绑定到某个**可探测的环境值**上,
3
+ * 值变了就自动失效——解决「版本特性类记忆随环境升级静默变错」的问题。
4
+ *
5
+ * 第一版只做**进程内可探测**的类型:校验跑在会话启动的路径上,探测必须零 IO。
6
+ * 需要跑子进程才能拿到的(如 PowerShell 版本)留待后续——那要先解决「插件直接
7
+ * spawn 会绕过 DSH 的 shell 沙箱」的合规问题,不能顺手开这个口子。
8
+ */
9
+ /**
10
+ * 探测某个锚点当前的取值。
11
+ *
12
+ * @param anchor - 记忆声明的锚点。
13
+ * @param probes - 当前环境的探测上下文。
14
+ * @returns 当前值;**无法探测时返回 `undefined`**(调用方按「未校验」处理,不能判失效)。
15
+ */
16
+ export function probeAnchor(anchor, probes) {
17
+ switch (anchor.kind) {
18
+ case 'env': {
19
+ if (anchor.name === undefined || anchor.name === '')
20
+ return undefined;
21
+ const read = probes.env ?? ((name) => process.env[name]);
22
+ return read(anchor.name);
23
+ }
24
+ case 'tool-list':
25
+ if (probes.toolNames === undefined)
26
+ return undefined;
27
+ return [...probes.toolNames].sort().join(',');
28
+ case 'self-version':
29
+ return probes.selfVersion;
30
+ default:
31
+ return undefined;
32
+ }
33
+ }
34
+ /**
35
+ * 锚点是否仍然成立。
36
+ *
37
+ * @returns `true` 成立;`false` 失效;`undefined` **未校验**(探测不到,不判失效——探测
38
+ * 失败与「值变了」是两件事,混起来会误伤)。
39
+ */
40
+ export function anchorHolds(anchor, probes) {
41
+ const current = probeAnchor(anchor, probes);
42
+ if (current === undefined)
43
+ return undefined;
44
+ return current === anchor.value;
45
+ }