dsh-remember 0.1.0 → 0.3.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
@@ -583,62 +583,112 @@ function ModelSelect(props) {
583
583
  style: inputStyle(false)
584
584
  }, options);
585
585
  }
586
+ /** Upper bound for an import traveling base64 through the host remote. */
587
+ const IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
588
+ /** Save bundle bytes: native save dialog where available, download fallback. */
589
+ async function saveBundleFile(name$1, base64) {
590
+ const binary = atob(base64);
591
+ const bytes = new Uint8Array(binary.length);
592
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
593
+ const picker = window.showSaveFilePicker;
594
+ if (picker != null) try {
595
+ const writable = await (await picker.call(window, { suggestedName: name$1 })).createWritable();
596
+ await writable.write(bytes);
597
+ await writable.close();
598
+ return;
599
+ } catch (error) {
600
+ if (error?.name === "AbortError") return;
601
+ }
602
+ const url = URL.createObjectURL(new Blob([bytes]));
603
+ const anchor = document.createElement("a");
604
+ anchor.href = url;
605
+ anchor.download = name$1;
606
+ anchor.click();
607
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
608
+ }
586
609
  /**
587
- * Import/export entry: the host-side transfer runs through the memory-commands
588
- * channel (path field + action), and the outcome comes back as a `transfer:`-
589
- * prefixed status detail line.
610
+ * Import/export entry: export returns the bundle from the host and saves it
611
+ * through the browser's file dialog; import reads a picked file and hands the
612
+ * bytes back to the host. Outcomes come back as `transfer:`-prefixed status
613
+ * detail lines.
590
614
  */
591
615
  function TransferSection(props) {
592
616
  const { t, commands } = props;
593
- const [exportPath, setExportPath] = (0, react.useState)("");
594
- const [importPath, setImportPath] = (0, react.useState)("");
595
617
  const [requested, setRequested] = (0, react.useState)(null);
596
- const send = (action, file) => {
597
- if (commands == null || file.trim() === "") return;
598
- setRequested(action);
599
- (async () => {
600
- await commands.set("path", file.trim());
601
- await commands.set("action", action);
602
- await commands.set("requestedAt", Date.now());
603
- })();
618
+ const [localError, setLocalError] = (0, react.useState)("");
619
+ const fileInput = (0, react.useRef)(null);
620
+ const exportBundle = async () => {
621
+ if (commands == null) return;
622
+ setLocalError("");
623
+ setRequested("export");
624
+ try {
625
+ const result = await commands.run({ action: "export" });
626
+ if (result.bundle != null && result.bundle.data !== "") {
627
+ await saveBundleFile(result.bundle.name, result.bundle.data);
628
+ return;
629
+ }
630
+ if (!result.ok) setLocalError(result.detail);
631
+ } catch (error) {
632
+ setLocalError(String(error));
633
+ }
604
634
  };
605
- const feedback = props.statusDetail?.startsWith("transfer:") === true ? props.statusDetail.slice(9).trim() : null;
606
- const feedbackIsError = feedback != null && /fail|need|must|unsupported|error/iu.test(feedback);
607
- const transferRow = (value, setValue, labelKey, action) => {
608
- const empty = value.trim() === "";
609
- return (0, react.createElement)("div", {
610
- className: "dshm-row",
611
- style: rowStyle()
612
- }, (0, react.createElement)("div", { style: labelStackStyle() }, (0, react.createElement)("span", { style: { fontSize: 13 } }, t(labelKey))), (0, react.createElement)("div", { style: {
613
- display: "flex",
614
- alignItems: "center",
615
- gap: 8,
616
- flexShrink: 0
617
- } }, (0, react.createElement)("input", {
618
- type: "text",
619
- value,
620
- spellCheck: false,
621
- className: "dshm-input",
622
- placeholder: "C:\\…\\memory.dshmem.json",
623
- onChange: (event) => {
624
- setValue(event.target.value);
625
- setRequested(null);
626
- },
627
- style: inputStyle(false)
628
- }), (0, react.createElement)("button", {
629
- type: "button",
630
- disabled: commands == null || empty,
631
- className: "dshm-btn",
632
- onClick: () => send(action, value),
633
- style: primaryButtonStyle(commands == null || empty)
634
- }, t(`transfer_${action}`))));
635
+ const importBundle = async (payload) => {
636
+ if (commands == null) return;
637
+ setLocalError("");
638
+ setRequested("import");
639
+ try {
640
+ const result = await commands.run({
641
+ action: "import",
642
+ payload
643
+ });
644
+ if (!result.ok) setLocalError(result.detail);
645
+ } catch (error) {
646
+ setLocalError(String(error));
647
+ }
635
648
  };
636
- return (0, react.createElement)("section", { style: sectionStyle() }, (0, react.createElement)("h3", { style: sectionHeaderStyle() }, t("group_transfer")), (0, react.createElement)("div", { style: { borderTop: `1px solid ${BORDER$1}` } }, transferRow(exportPath, setExportPath, "transfer_exportPath", "export"), transferRow(importPath, setImportPath, "transfer_importPath", "import")), (0, react.createElement)("p", { style: {
649
+ const pickImport = (event) => {
650
+ const file = event.target.files?.[0];
651
+ event.target.value = "";
652
+ if (file == null) return;
653
+ if (file.size > IMPORT_LIMIT_BYTES) {
654
+ setLocalError(t("transfer_tooBig"));
655
+ return;
656
+ }
657
+ const reader = new FileReader();
658
+ reader.onload = () => {
659
+ const result = String(reader.result ?? "");
660
+ importBundle(result.slice(result.indexOf(",") + 1));
661
+ };
662
+ reader.onerror = () => setLocalError(t("transfer_readFailed"));
663
+ reader.readAsDataURL(file);
664
+ };
665
+ const feedback = localError !== "" ? localError : props.statusDetail?.startsWith("transfer:") === true ? props.statusDetail.slice(9).trim() : null;
666
+ const feedbackIsError = feedback != null && (localError !== "" || /fail|need|must|unsupported|error/iu.test(feedback));
667
+ const transferRow = (labelKey, descKey, action) => (0, react.createElement)("div", {
668
+ className: "dshm-row",
669
+ style: rowStyle()
670
+ }, (0, react.createElement)("div", { style: labelStackStyle() }, (0, react.createElement)("span", { style: { fontSize: 13 } }, t(labelKey)), (0, react.createElement)("span", { style: {
637
671
  fontSize: 11.5,
638
- opacity: .5,
639
- margin: "8px 2px 0",
640
- lineHeight: 1.5
641
- } }, t("transfer_hint")), (0, react.createElement)("div", { style: {
672
+ opacity: .5
673
+ } }, t(descKey))), (0, react.createElement)("button", {
674
+ type: "button",
675
+ disabled: commands == null,
676
+ className: "dshm-btn",
677
+ onClick: () => {
678
+ setLocalError("");
679
+ setRequested(action);
680
+ if (action === "export") exportBundle();
681
+ else fileInput.current?.click();
682
+ },
683
+ style: primaryButtonStyle(commands == null)
684
+ }, t(`transfer_${action}`)));
685
+ return (0, react.createElement)("section", { style: sectionStyle() }, (0, react.createElement)("h3", { style: sectionHeaderStyle() }, t("group_transfer")), (0, react.createElement)("div", { style: { borderTop: `1px solid ${BORDER$1}` } }, transferRow("transfer_exportLabel", "transfer_exportDesc", "export"), transferRow("transfer_importLabel", "transfer_importDesc", "import")), (0, react.createElement)("input", {
686
+ type: "file",
687
+ accept: ".json,.gz,.dshmem",
688
+ ref: fileInput,
689
+ style: { display: "none" },
690
+ onChange: pickImport
691
+ }), (0, react.createElement)("div", { style: {
642
692
  minHeight: 18,
643
693
  margin: "4px 2px 0"
644
694
  } }, feedback != null ? (0, react.createElement)("span", { style: {
@@ -699,9 +749,11 @@ function hintText(t, field) {
699
749
  return hint === `hint_${field}` ? "" : hint;
700
750
  }
701
751
  /**
702
- * The dedicated Memory settings page (occupies one `settings.section` seat):
703
- * live status banner, divided-list sections with catalog-backed model pickers
704
- * and toggle switches, transfer actions, and a collapsed advanced section.
752
+ * The dedicated Memory settings page: live status banner, divided-list sections
753
+ * with catalog-backed model pickers and toggle switches, transfer actions, and a
754
+ * collapsed advanced section. Rendered both as its own `settings.section` seat
755
+ * and inside the plugin page's `plugins.bundle.config` seat, where `embedded`
756
+ * drops the heading the page already supplies.
705
757
  * Render failures surface inline instead of blanking the panel.
706
758
  */
707
759
  function MemorySettingsPage(props) {
@@ -777,15 +829,16 @@ function renderMemoryPage(props) {
777
829
  onReset: () => props.resetField(field.spec.field)
778
830
  }))));
779
831
  };
832
+ const embedded = props.embedded === true;
780
833
  return (0, react.createElement)("div", { style: {
781
- maxWidth: 640,
782
- paddingBottom: 32
783
- } }, (0, react.createElement)("h2", { style: {
834
+ maxWidth: embedded ? void 0 : 640,
835
+ paddingBottom: embedded ? 0 : 32
836
+ } }, embedded ? null : (0, react.createElement)("h2", { style: {
784
837
  fontSize: 19,
785
838
  fontWeight: 650,
786
839
  margin: "0 0 4px",
787
840
  letterSpacing: .2
788
- } }, t("title")), (0, react.createElement)("p", { style: {
841
+ } }, t("title")), embedded ? null : (0, react.createElement)("p", { style: {
789
842
  fontSize: 13,
790
843
  opacity: .6,
791
844
  margin: "0 0 18px",
@@ -852,6 +905,293 @@ function renderMemoryPage(props) {
852
905
  } }, t("readOnly")) : null));
853
906
  }
854
907
 
908
+ //#endregion
909
+ //#region src/client/remote.ts
910
+ /**
911
+ * Result codecs exist only to satisfy the strict-codec contract; the gateway
912
+ * already delivered parsed JSON, so the browser side does no re-parsing.
913
+ */
914
+ const passthrough = (name$1) => ({
915
+ mode: "strict",
916
+ typeSymbol: `dsh-remember#${name$1}`,
917
+ create: () => ({ parse: (value) => value })
918
+ });
919
+ const jsonParameter = (name$1, typeName) => ({
920
+ name: name$1,
921
+ wire: name$1,
922
+ source: "json",
923
+ codec: passthrough(typeName)
924
+ });
925
+ const descriptor = (method, parameters, resultName) => ({
926
+ id: `dsh-remember#memory/${method}`,
927
+ service: "memory",
928
+ namespace: "memory",
929
+ method,
930
+ invocation: { kind: "direct" },
931
+ parameters,
932
+ result: passthrough(resultName)
933
+ });
934
+ /** Mirrors MEMORY_INVOCATIONS in src/typert.ts (method names and wires must match). */
935
+ const CONTRIBUTION = {
936
+ package: "dsh-remember",
937
+ descriptors: [
938
+ descriptor("getStatus", [], "MemoryStatus"),
939
+ descriptor("getConfig", [], "ConfigSnapshot"),
940
+ descriptor("updateConfig", [jsonParameter("patch", "ConfigPatch")], "ConfigSnapshot"),
941
+ descriptor("unsetConfig", [jsonParameter("field", "ConfigField")], "ConfigSnapshot"),
942
+ descriptor("getView", [], "MemoryView"),
943
+ descriptor("runCommand", [jsonParameter("request", "CommandRequest")], "CommandResult")
944
+ ]
945
+ };
946
+ const STATUS_POLL_MS = 2500;
947
+ const CONFIG_POLL_MS = 15e3;
948
+ const VIEW_POLL_MS = 5e3;
949
+ /** A snapshot store with change notification, in the Shape components expect. */
950
+ var BridgeStore = class {
951
+ snapshot;
952
+ listeners = /* @__PURE__ */ new Set();
953
+ /** Fired when the subscriber count crosses zero; used for lazy polling. */
954
+ onActiveChange;
955
+ constructor(initial) {
956
+ this.snapshot = initial;
957
+ }
958
+ getSnapshot = () => this.snapshot;
959
+ subscribe = (listener) => {
960
+ this.listeners.add(listener);
961
+ if (this.listeners.size === 1) this.onActiveChange?.(true);
962
+ return () => {
963
+ this.listeners.delete(listener);
964
+ if (this.listeners.size === 0) this.onActiveChange?.(false);
965
+ };
966
+ };
967
+ set(snapshot) {
968
+ this.snapshot = snapshot;
969
+ for (const listener of this.listeners) listener();
970
+ }
971
+ };
972
+ const loadingSnapshot = () => ({
973
+ status: "loading",
974
+ value: void 0,
975
+ base: void 0,
976
+ user: void 0,
977
+ revision: void 0,
978
+ writable: false,
979
+ mode: "host"
980
+ });
981
+ const unavailableSnapshot = () => ({
982
+ status: "unavailable",
983
+ value: void 0,
984
+ base: void 0,
985
+ user: void 0,
986
+ revision: void 0,
987
+ writable: false,
988
+ mode: "host"
989
+ });
990
+ function failureDetail(error) {
991
+ if (error == null) return "memory host call failed";
992
+ return `${error.code ?? "error"}: ${error.message ?? "unknown failure"}`;
993
+ }
994
+ /**
995
+ * Owns the mounted namespace plus the snapshot stores. Created synchronously
996
+ * (slots need it immediately) and attached once `$mount` resolves.
997
+ */
998
+ var MemoryBridge = class {
999
+ remote = null;
1000
+ statusTimer;
1001
+ configTimer;
1002
+ viewTimer;
1003
+ configStore = new BridgeStore(loadingSnapshot());
1004
+ statusStore = new BridgeStore(loadingSnapshot());
1005
+ viewStore = new BridgeStore(loadingSnapshot());
1006
+ /** Settings page scope: `set`/`unset` persist config overrides on the host. */
1007
+ configScope = {
1008
+ getSnapshot: () => this.configStore.getSnapshot(),
1009
+ subscribe: (listener) => this.configStore.subscribe(listener),
1010
+ set: async (field, value) => {
1011
+ await this.updateConfig({ [field]: value });
1012
+ },
1013
+ unset: async (field) => {
1014
+ await this.unsetConfig(field);
1015
+ }
1016
+ };
1017
+ /** Selector-hook source for the sidebar pill and the status banner. */
1018
+ statusSource = {
1019
+ getSnapshot: () => this.statusStore.getSnapshot(),
1020
+ subscribe: (listener) => this.statusStore.subscribe(listener)
1021
+ };
1022
+ /** Selector-hook source for the viewer page (polls only while mounted). */
1023
+ viewSource = {
1024
+ getSnapshot: () => this.viewStore.getSnapshot(),
1025
+ subscribe: (listener) => this.viewStore.subscribe(listener)
1026
+ };
1027
+ /** The single action runner every widget uses. */
1028
+ commands = { run: (request) => this.runCommand(request) };
1029
+ constructor() {
1030
+ this.viewStore.onActiveChange = (active) => {
1031
+ if (active) {
1032
+ this.refreshView();
1033
+ this.viewTimer = setInterval(() => {
1034
+ this.refreshView();
1035
+ }, VIEW_POLL_MS);
1036
+ } else {
1037
+ this.stopTimer(this.viewTimer);
1038
+ this.viewTimer = void 0;
1039
+ }
1040
+ };
1041
+ }
1042
+ attach(remote) {
1043
+ this.remote = remote;
1044
+ this.refreshStatus();
1045
+ this.refreshConfig();
1046
+ this.statusTimer = setInterval(() => {
1047
+ this.refreshStatus();
1048
+ }, STATUS_POLL_MS);
1049
+ this.configTimer = setInterval(() => {
1050
+ this.refreshConfig();
1051
+ }, CONFIG_POLL_MS);
1052
+ }
1053
+ detach() {
1054
+ this.stopTimer(this.statusTimer);
1055
+ this.stopTimer(this.configTimer);
1056
+ this.stopTimer(this.viewTimer);
1057
+ this.statusTimer = void 0;
1058
+ this.configTimer = void 0;
1059
+ this.viewTimer = void 0;
1060
+ this.remote = null;
1061
+ }
1062
+ async updateConfig(patch) {
1063
+ const result = await this.requireRemote().updateConfig(patch);
1064
+ if (!result.ok) throw new Error(failureDetail(result.error));
1065
+ this.applyConfig(result.value);
1066
+ this.refreshStatus();
1067
+ }
1068
+ async unsetConfig(field) {
1069
+ const result = await this.requireRemote().unsetConfig(field);
1070
+ if (!result.ok) throw new Error(failureDetail(result.error));
1071
+ this.applyConfig(result.value);
1072
+ }
1073
+ async runCommand(request) {
1074
+ const remote = this.remote;
1075
+ if (remote == null) return {
1076
+ ok: false,
1077
+ detail: "memory host unavailable"
1078
+ };
1079
+ let result;
1080
+ try {
1081
+ result = await remote.runCommand(request);
1082
+ } catch (error) {
1083
+ return {
1084
+ ok: false,
1085
+ detail: String(error)
1086
+ };
1087
+ }
1088
+ if (!result.ok) return {
1089
+ ok: false,
1090
+ detail: failureDetail(result.error)
1091
+ };
1092
+ if (result.value.view !== void 0) this.applyView(result.value.view);
1093
+ else this.refreshView();
1094
+ this.refreshStatus();
1095
+ this.refreshConfig();
1096
+ return result.value;
1097
+ }
1098
+ async refreshView() {
1099
+ const remote = this.remote;
1100
+ if (remote == null) return;
1101
+ try {
1102
+ const result = await remote.getView();
1103
+ if (!result.ok) throw new Error(failureDetail(result.error));
1104
+ this.applyView(result.value);
1105
+ } catch {
1106
+ this.viewStore.set(unavailableSnapshot());
1107
+ }
1108
+ }
1109
+ applyView(view) {
1110
+ this.viewStore.set({
1111
+ status: "ready",
1112
+ value: view,
1113
+ base: void 0,
1114
+ user: void 0,
1115
+ revision: view.updatedAt,
1116
+ writable: false,
1117
+ mode: "host"
1118
+ });
1119
+ }
1120
+ applyConfig(config) {
1121
+ this.configStore.set({
1122
+ status: "ready",
1123
+ value: config.value,
1124
+ base: config.base,
1125
+ user: config.user,
1126
+ revision: config.revision,
1127
+ writable: true,
1128
+ mode: "host"
1129
+ });
1130
+ }
1131
+ async refreshConfig() {
1132
+ const remote = this.remote;
1133
+ if (remote == null) return;
1134
+ try {
1135
+ const result = await remote.getConfig();
1136
+ if (!result.ok) throw new Error(failureDetail(result.error));
1137
+ this.applyConfig(result.value);
1138
+ } catch {
1139
+ this.configStore.set(unavailableSnapshot());
1140
+ }
1141
+ }
1142
+ async refreshStatus() {
1143
+ const remote = this.remote;
1144
+ if (remote == null) return;
1145
+ try {
1146
+ const result = await remote.getStatus();
1147
+ if (!result.ok) throw new Error(failureDetail(result.error));
1148
+ const status = result.value;
1149
+ this.statusStore.set({
1150
+ status: "ready",
1151
+ value: status,
1152
+ base: void 0,
1153
+ user: void 0,
1154
+ revision: status.updatedAt,
1155
+ writable: false,
1156
+ mode: "host"
1157
+ });
1158
+ } catch {
1159
+ this.statusStore.set(unavailableSnapshot());
1160
+ }
1161
+ }
1162
+ requireRemote() {
1163
+ if (this.remote == null) throw new Error("memory host unavailable");
1164
+ return this.remote;
1165
+ }
1166
+ stopTimer(timer) {
1167
+ if (timer !== void 0) clearInterval(timer);
1168
+ }
1169
+ };
1170
+ /**
1171
+ * Mount the contribution and hand the resulting namespace to the bridge.
1172
+ * Returns the disposer the caller runs from its effect cleanup.
1173
+ */
1174
+ async function mountMemoryBridge(bridge, ctx) {
1175
+ const remote = ctx.remote;
1176
+ if (remote == null || typeof remote.$mount !== "function") throw new Error("the remote service is not available in this composition");
1177
+ let dispose;
1178
+ try {
1179
+ dispose = await remote.$mount(CONTRIBUTION);
1180
+ } catch (error) {
1181
+ throw new Error(`mounting the memory remote contribution failed: ${String(error)}`);
1182
+ }
1183
+ const namespace = ctx.get("remote.memory");
1184
+ if (namespace == null) {
1185
+ await dispose();
1186
+ throw new Error("remote.memory is missing after mounting the contribution");
1187
+ }
1188
+ bridge.attach(namespace);
1189
+ return async () => {
1190
+ bridge.detach();
1191
+ await dispose();
1192
+ };
1193
+ }
1194
+
855
1195
  //#endregion
856
1196
  //#region src/client/sidebar-action.ts
857
1197
  let styleInjected$1 = false;
@@ -956,10 +1296,7 @@ function renderAction(props) {
956
1296
  const sendCommand = (action) => {
957
1297
  const commands = props.commands;
958
1298
  if (commands == null) return;
959
- (async () => {
960
- await commands.set("action", action);
961
- await commands.set("requestedAt", Date.now());
962
- })();
1299
+ commands.run({ action });
963
1300
  };
964
1301
  const toggleSetting = (field, value) => {
965
1302
  props.settings?.set(field, value);
@@ -1322,7 +1659,11 @@ function ensureViewerStyles() {
1322
1659
  ".dshmv-usage { display: flex; align-items: center; gap: 6px; }",
1323
1660
  ".dshmv-usage-bar { height: 5px; border-radius: 3px; background: var(--dsw-alias-brand-primary, #4d6bfe); min-width: 2px; transition: width 200ms ease; }",
1324
1661
  ".dshmv-empty { color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.45)); padding: 24px 0; text-align: center; font-size: 12px; }",
1325
- ".dshmv-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.45)); }"
1662
+ ".dshmv-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.45)); }",
1663
+ ".dshmv-section { margin: 2px 0 8px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, rgba(0,0,0,0.65)); }",
1664
+ ".dshmv-toprow { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 8px; border: 0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.1)); background: var(--dsw-alias-fill-tertiary, rgba(0,0,0,0.04)); margin-bottom: 6px; }",
1665
+ ".dshmv-toprow-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-size: 12px; }",
1666
+ ".dshmv-toprow-meta { flex: 0 0 auto; font-size: 11px; color: var(--dsw-alias-label-tertiary, rgba(0,0,0,0.45)); font-variant-numeric: tabular-nums; }"
1326
1667
  ].join("\n");
1327
1668
  document.head.appendChild(tag);
1328
1669
  styleInjected = true;
@@ -1399,19 +1740,16 @@ function renderViewer(props) {
1399
1740
  const sendCommand = (action, arg = "", force = false) => {
1400
1741
  const commands = props.commands;
1401
1742
  if (commands == null) return;
1402
- (async () => {
1403
- if (arg !== "") await commands.set("arg", arg);
1404
- if (force) await commands.set("force", true);
1405
- await commands.set("action", action);
1406
- await commands.set("requestedAt", Date.now());
1407
- })();
1743
+ commands.run({
1744
+ action,
1745
+ arg: arg === "" ? void 0 : arg,
1746
+ force: force || void 0
1747
+ });
1408
1748
  };
1409
- const effectiveText = showTranslated && hasTranslation ? texts.translatedSummary : texts.summary;
1410
1749
  return (0, react.createElement)("div", { className: "dshmv-root" }, (0, react.createElement)("div", { className: "dshmv-tabs" }, ...[
1411
1750
  "overview",
1412
1751
  "summary",
1413
- "index",
1414
- "records"
1752
+ "index"
1415
1753
  ].map((id) => (0, react.createElement)("button", {
1416
1754
  key: id,
1417
1755
  type: "button",
@@ -1457,30 +1795,54 @@ function renderViewer(props) {
1457
1795
  }, t("viewer_rebuild")), showTranslated && hasTranslation && view.translatedStale === true ? (0, react.createElement)("span", { className: "dshmv-badge dshmv-badge-stale" }, t("viewer_stale")) : null, showTranslated && hasTranslation && view.translatedStale !== true ? (0, react.createElement)("span", { className: "dshmv-badge dshmv-badge-ok" }, t("viewer_fresh")) : null, (0, react.createElement)("span", { className: "dshmv-status" }, status.detail ?? "")), (0, react.createElement)("div", {
1458
1796
  className: "dshmv-body",
1459
1797
  "data-fill": "true"
1460
- }, tab === "overview" ? renderOverview(t, texts, effectiveText, showTranslated, status, records) : tab === "summary" ? (0, react.createElement)(SummaryView, {
1798
+ }, tab === "overview" ? (0, react.createElement)(OverviewView, {
1461
1799
  t,
1462
1800
  texts,
1463
- showTranslated
1464
- }) : tab === "index" ? renderIndex(t, texts, showTranslated) : (0, react.createElement)(RecordsView, {
1465
- t,
1801
+ showTranslated,
1802
+ status,
1466
1803
  records
1467
- })));
1804
+ }) : tab === "summary" ? (0, react.createElement)(SummaryView, {
1805
+ t,
1806
+ texts,
1807
+ showTranslated
1808
+ }) : renderIndex(t, texts, showTranslated)));
1468
1809
  }
1469
- function renderOverview(t, texts, effectiveText, showTranslated, status, records) {
1810
+ /** Overview dashboard: stat cards, status, most-cited, and the records table. */
1811
+ function OverviewView(props) {
1812
+ const { t, texts, showTranslated, status, records } = props;
1813
+ const [query, setQuery] = (0, react.useState)("");
1470
1814
  const totalUsage = records.reduce((sum, record) => sum + record.usageCount, 0);
1471
1815
  const consolidated = records.filter((record) => record.selectedForPhase2).length;
1816
+ const topCited = records.filter((record) => record.usageCount > 0).sort((a, b) => b.usageCount - a.usageCount).slice(0, 5);
1472
1817
  const phase = status.phase ?? "idle";
1473
1818
  const active = phase === "phase1" || phase === "phase2";
1819
+ const needle = query.trim().toLowerCase();
1820
+ const filtered = needle === "" ? records : records.filter((record) => record.sessionId.toLowerCase().includes(needle) || record.rolloutSlug.toLowerCase().includes(needle) || record.workspacePath.toLowerCase().includes(needle));
1821
+ const maxUsage = Math.max(1, ...records.map((record) => record.usageCount));
1474
1822
  const card = (num, label) => (0, react.createElement)("div", {
1475
1823
  className: "dshmv-card",
1476
1824
  key: label
1477
1825
  }, (0, react.createElement)("div", { className: "dshmv-card-num" }, num), (0, react.createElement)("div", { className: "dshmv-card-label" }, label));
1478
- return (0, react.createElement)("div", { className: "dshmv-fill" }, (0, react.createElement)("div", { className: "dshmv-cards" }, card(records.length, t("viewer_stat_records")), card(consolidated, t("viewer_stat_consolidated")), card(totalUsage, t("viewer_stat_citations")), card(formatBytes(texts.summary.length), t("viewer_stat_summarySize"))), (0, react.createElement)("div", { style: {
1826
+ return (0, react.createElement)("div", { className: "dshmv-scroll-fill" }, (0, react.createElement)("div", { className: "dshmv-cards" }, card(records.length, t("viewer_stat_records")), card(consolidated, t("viewer_stat_consolidated")), card(totalUsage, t("viewer_stat_citations")), card(formatBytes(texts.summary.length), t("viewer_stat_summarySize")), card(formatBytes(texts.index.length), t("viewer_stat_indexSize"))), (0, react.createElement)("div", { style: {
1479
1827
  display: "flex",
1480
1828
  alignItems: "center",
1481
1829
  gap: 8,
1482
1830
  marginBottom: 10
1483
- } }, (0, react.createElement)("span", { className: `dshmv-badge ${active ? "dshmv-badge-stale" : "dshmv-badge-ok"}` }, active ? phase === "phase1" ? t("phase1") : t("phase2") : t("statusIdle")), status.updatedAt != null && status.updatedAt > 0 ? (0, react.createElement)("span", { className: "dshmv-mono" }, new Date(status.updatedAt).toLocaleString()) : null, showTranslated ? (0, react.createElement)("span", { className: "dshmv-badge dshmv-badge-ok" }, t("viewer_badgeTranslated")) : null), effectiveText !== "" || texts.summary !== "" ? (0, react.createElement)("div", { className: "dshmv-md dshmv-md-fill" }, effectiveText !== "" ? effectiveText : texts.summary) : (0, react.createElement)("div", { className: "dshmv-empty dshmv-md-fill" }, texts.failed ? t("viewer_decodeFailed") : t("viewer_empty")));
1831
+ } }, (0, react.createElement)("span", { className: `dshmv-badge ${active ? "dshmv-badge-stale" : "dshmv-badge-ok"}` }, active ? phase === "phase1" ? t("phase1") : t("phase2") : t("statusIdle")), status.updatedAt != null && status.updatedAt > 0 ? (0, react.createElement)("span", { className: "dshmv-mono" }, new Date(status.updatedAt).toLocaleString()) : null, showTranslated ? (0, react.createElement)("span", { className: "dshmv-badge dshmv-badge-ok" }, t("viewer_badgeTranslated")) : null), records.length === 0 ? null : (0, react.createElement)("div", { className: "dshmv-section" }, t("viewer_topCited")), ...topCited.map((record) => (0, react.createElement)("div", {
1832
+ className: "dshmv-toprow",
1833
+ key: record.sessionId
1834
+ }, (0, react.createElement)("span", { className: "dshmv-toprow-name" }, record.rolloutSlug || record.sessionId.slice(0, 8)), (0, react.createElement)("span", { className: "dshmv-toprow-meta" }, `${record.usageCount} · ${formatTime(record.lastUsage ?? record.sourceUpdatedAt)}`))), records.length > 0 && topCited.length === 0 ? (0, react.createElement)("div", { className: "dshmv-empty" }, t("viewer_noCitations")) : null, records.length === 0 ? null : (0, react.createElement)("div", {
1835
+ className: "dshmv-section",
1836
+ style: { marginTop: 14 }
1837
+ }, t("viewer_tab_records")), records.length === 0 ? null : (0, react.createElement)("input", {
1838
+ className: "dshmv-search",
1839
+ placeholder: t("viewer_search"),
1840
+ value: query,
1841
+ onChange: (event) => setQuery(event.target.value)
1842
+ }), records.length === 0 ? (0, react.createElement)("div", { className: "dshmv-empty" }, t("viewer_empty")) : (0, react.createElement)("table", { className: "dshmv-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, t("viewer_col_session")), (0, react.createElement)("th", null, t("viewer_col_usage")), (0, react.createElement)("th", null, t("viewer_col_updated")))), (0, react.createElement)("tbody", null, ...filtered.map((record) => (0, react.createElement)("tr", { key: record.sessionId }, (0, react.createElement)("td", null, (0, react.createElement)("div", { style: { fontWeight: 600 } }, record.rolloutSlug || record.sessionId.slice(0, 8)), (0, react.createElement)("div", { className: "dshmv-mono" }, record.workspacePath)), (0, react.createElement)("td", null, (0, react.createElement)("div", { className: "dshmv-usage" }, (0, react.createElement)("span", {
1843
+ className: "dshmv-usage-bar",
1844
+ style: { width: `${Math.round(record.usageCount / maxUsage * 48)}px` }
1845
+ }), (0, react.createElement)("span", null, String(record.usageCount))), record.lastUsage != null ? (0, react.createElement)("div", { className: "dshmv-mono" }, `${t("viewer_col_lastUsed")}: ${formatTime(record.lastUsage)}`) : null), (0, react.createElement)("td", { className: "dshmv-mono" }, formatTime(record.sourceUpdatedAt)))))));
1484
1846
  }
1485
1847
  function SummaryView(props) {
1486
1848
  const { t, texts, showTranslated } = props;
@@ -1491,22 +1853,6 @@ function renderIndex(t, texts, showTranslated) {
1491
1853
  const text = showTranslated && texts.translatedIndex !== "" ? texts.translatedIndex : texts.index;
1492
1854
  return text !== "" ? (0, react.createElement)("div", { className: "dshmv-md dshmv-md-fill" }, text) : (0, react.createElement)("div", { className: "dshmv-empty dshmv-fill" }, texts.failed ? t("viewer_decodeFailed") : t("viewer_empty"));
1493
1855
  }
1494
- function RecordsView(props) {
1495
- const { t, records } = props;
1496
- const [query, setQuery] = (0, react.useState)("");
1497
- const needle = query.trim().toLowerCase();
1498
- const filtered = needle === "" ? records : records.filter((record) => record.sessionId.toLowerCase().includes(needle) || record.rolloutSlug.toLowerCase().includes(needle) || record.workspacePath.toLowerCase().includes(needle));
1499
- const maxUsage = Math.max(1, ...records.map((record) => record.usageCount));
1500
- return (0, react.createElement)("div", { className: "dshmv-fill" }, (0, react.createElement)("input", {
1501
- className: "dshmv-search",
1502
- placeholder: t("viewer_search"),
1503
- value: query,
1504
- onChange: (event) => setQuery(event.target.value)
1505
- }), (0, react.createElement)("div", { className: "dshmv-scroll-fill" }, filtered.length === 0 ? (0, react.createElement)("div", { className: "dshmv-empty" }, t("viewer_empty")) : (0, react.createElement)("table", { className: "dshmv-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, t("viewer_col_session")), (0, react.createElement)("th", null, t("viewer_col_usage")), (0, react.createElement)("th", null, t("viewer_col_updated")))), (0, react.createElement)("tbody", null, ...filtered.map((record) => (0, react.createElement)("tr", { key: record.sessionId }, (0, react.createElement)("td", null, (0, react.createElement)("div", { style: { fontWeight: 600 } }, record.rolloutSlug || record.sessionId.slice(0, 8)), (0, react.createElement)("div", { className: "dshmv-mono" }, record.workspacePath)), (0, react.createElement)("td", null, (0, react.createElement)("div", { className: "dshmv-usage" }, (0, react.createElement)("span", {
1506
- className: "dshmv-usage-bar",
1507
- style: { width: `${Math.round(record.usageCount / maxUsage * 48)}px` }
1508
- }), (0, react.createElement)("span", null, String(record.usageCount))), record.lastUsage != null ? (0, react.createElement)("div", { className: "dshmv-mono" }, `${t("viewer_col_lastUsed")}: ${formatTime(record.lastUsage)}`) : null), (0, react.createElement)("td", { className: "dshmv-mono" }, formatTime(record.sourceUpdatedAt))))))));
1509
- }
1510
1856
  /** Title chip for the viewer tab. */
1511
1857
  function MemoryViewerTitle(props) {
1512
1858
  return (0, react.createElement)("span", null, props.t("viewer_title"));
@@ -1548,9 +1894,7 @@ const name = "dsh-remember";
1548
1894
  const inject = [
1549
1895
  "slots",
1550
1896
  "locale",
1551
- "settingsScope",
1552
- "remote",
1553
- "remote.session"
1897
+ "remote"
1554
1898
  ];
1555
1899
  const NS = "dsh-memory";
1556
1900
  const ZH = {
@@ -1617,12 +1961,15 @@ const ZH = {
1617
1961
  phase1: "记忆提取中",
1618
1962
  phase2: "记忆整理中",
1619
1963
  processing: "记忆处理中",
1620
- transfer_hint: "导出生成单个 gzip 压缩的 bundle 文件;导入按内容哈希去重合并,冲突以本地为准,未压缩的 JSON 也能读。",
1621
- transfer_exportPath: "导出到文件",
1622
- transfer_importPath: "从文件导入",
1964
+ transfer_exportLabel: "导出记忆",
1965
+ transfer_exportDesc: "保存为单个 bundle 文件,位置在浏览器里选",
1966
+ transfer_importLabel: "导入记忆",
1967
+ transfer_importDesc: "选择一个 bundle 文件合并进当前记忆库",
1623
1968
  transfer_export: "导出",
1624
1969
  transfer_import: "导入",
1625
1970
  transfer_requested: "已发送,结果见上方状态栏",
1971
+ transfer_tooBig: "文件超过 32 MB,请改用 memory_export 工具直接导出到磁盘",
1972
+ transfer_readFailed: "文件读取失败",
1626
1973
  viewer_title: "记忆浏览",
1627
1974
  viewer_refresh: "刷新",
1628
1975
  viewer_translate: "翻译记忆",
@@ -1635,6 +1982,9 @@ const ZH = {
1635
1982
  viewer_stat_consolidated: "已整理",
1636
1983
  viewer_stat_citations: "被引用次数",
1637
1984
  viewer_stat_summarySize: "摘要大小",
1985
+ viewer_stat_indexSize: "注册表大小",
1986
+ viewer_topCited: "最常引用",
1987
+ viewer_noCitations: "还没有引用记录",
1638
1988
  viewer_original: "显示原文",
1639
1989
  viewer_translated: "显示译文",
1640
1990
  viewer_showTranslated: "显示译文",
@@ -1718,12 +2068,15 @@ const EN = {
1718
2068
  phase1: "Extracting memories",
1719
2069
  phase2: "Consolidating memories",
1720
2070
  processing: "Memory processing",
1721
- transfer_hint: "Export writes one gzip-compressed bundle file; import merges by content hash (conflicts keep the local copy) and also accepts uncompressed JSON.",
1722
- transfer_exportPath: "Export to file",
1723
- transfer_importPath: "Import from file",
2071
+ transfer_exportLabel: "Export memory",
2072
+ transfer_exportDesc: "Save as a single bundle file — pick the location in the browser",
2073
+ transfer_importLabel: "Import memory",
2074
+ transfer_importDesc: "Pick a bundle file to merge into the current memory store",
1724
2075
  transfer_export: "Export",
1725
2076
  transfer_import: "Import",
1726
2077
  transfer_requested: "Sent — result appears in the status bar above",
2078
+ transfer_tooBig: "File exceeds 32 MB — use the memory_export tool to write it to disk instead",
2079
+ transfer_readFailed: "Failed to read the file",
1727
2080
  viewer_title: "Memory",
1728
2081
  viewer_refresh: "Refresh",
1729
2082
  viewer_translate: "Translate memory",
@@ -1736,6 +2089,9 @@ const EN = {
1736
2089
  viewer_stat_consolidated: "Consolidated",
1737
2090
  viewer_stat_citations: "Citations",
1738
2091
  viewer_stat_summarySize: "Summary size",
2092
+ viewer_stat_indexSize: "Registry size",
2093
+ viewer_topCited: "Most cited",
2094
+ viewer_noCitations: "No citations yet",
1739
2095
  viewer_original: "Show original",
1740
2096
  viewer_translated: "Show translation",
1741
2097
  viewer_showTranslated: "Translation",
@@ -1757,34 +2113,42 @@ const EN = {
1757
2113
  };
1758
2114
  /** Activate the browser half: register the Memory settings page and the status pill. */
1759
2115
  function apply(ctx) {
2116
+ try {
2117
+ activate(ctx);
2118
+ } catch (error) {
2119
+ console.error("[dsh-memory] activation failed", error);
2120
+ throw error;
2121
+ }
2122
+ }
2123
+ function activate(ctx) {
1760
2124
  ctx.effect(() => ctx.locale.register(NS, {
1761
2125
  zh: ZH,
1762
2126
  en: EN
1763
2127
  }), "dsh-memory: dictionaries");
1764
2128
  ctx.effect(() => installCitationStripper(), "dsh-memory: citation stripper");
1765
- const cardScope = ctx.settingsScope.bind({ namespace: "memory" });
1766
- const statusScope = ctx.settingsScope.bind({ namespace: "memory-status" });
1767
- const commandsScope = ctx.settingsScope.bind({ namespace: "memory-commands" });
1768
- const viewerScope = ctx.settingsScope.bind({ namespace: "memory-view" });
1769
- const card = new MemoryCardController(cardScope, catalogLoader(ctx));
1770
- const statusSource = {
1771
- getSnapshot: () => statusScope.getSnapshot(),
1772
- subscribe: (listener) => statusScope.subscribe(listener)
1773
- };
1774
- const viewSource = {
1775
- getSnapshot: () => viewerScope.getSnapshot(),
1776
- subscribe: (listener) => viewerScope.subscribe(listener)
1777
- };
1778
- const commandsView = {
1779
- set: (field, value) => commandsScope.set(field, value),
1780
- unset: (field) => commandsScope.unset(field)
1781
- };
1782
- const memoryView = {
1783
- set: (field, value) => cardScope.set(field, value),
1784
- unset: (field) => cardScope.unset(field),
1785
- getSnapshot: () => cardScope.getSnapshot(),
1786
- subscribe: (listener) => cardScope.subscribe(listener)
1787
- };
2129
+ const bridge = new MemoryBridge();
2130
+ ctx.effect(() => {
2131
+ let dispose;
2132
+ let cancelled = false;
2133
+ mountMemoryBridge(bridge, ctx).then((mounted) => {
2134
+ if (cancelled) {
2135
+ mounted();
2136
+ return;
2137
+ }
2138
+ dispose = mounted;
2139
+ }).catch((error) => {
2140
+ console.warn("[dsh-memory] memory remote unavailable:", error);
2141
+ });
2142
+ return () => {
2143
+ cancelled = true;
2144
+ if (dispose != null) dispose();
2145
+ };
2146
+ }, "dsh-memory: memory remote");
2147
+ const card = new MemoryCardController(bridge.configScope, catalogLoader(ctx));
2148
+ const statusSource = bridge.statusSource;
2149
+ const viewSource = bridge.viewSource;
2150
+ const commandsView = bridge.commands;
2151
+ const memoryView = bridge.configScope;
1788
2152
  let sidebarRight;
1789
2153
  ctx.inject(["sidebarRightTabs", "sidebarRight"], (sctx) => {
1790
2154
  sidebarRight = sctx.sidebarRight;
@@ -1817,20 +2181,29 @@ function apply(ctx) {
1817
2181
  sidebarRight?.openTab(MEMORY_VIEWER_KIND, {});
1818
2182
  } catch {}
1819
2183
  };
2184
+ const pageFace = (embedded) => () => ({
2185
+ ...card.inject(),
2186
+ commands: commandsView,
2187
+ embedded,
2188
+ hooks: {
2189
+ memoryPage: card.pageStore(),
2190
+ memoryStatus: statusSource,
2191
+ memoryView: viewSource
2192
+ }
2193
+ });
1820
2194
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1821
2195
  name: "settings.section",
1822
2196
  id: "memory",
1823
2197
  order: 16,
1824
2198
  label: () => t_nav(ctx),
1825
2199
  locale: NS,
1826
- inject: () => ({
1827
- ...card.inject(),
1828
- commands: commandsView,
1829
- hooks: {
1830
- memoryPage: card.pageStore(),
1831
- memoryStatus: statusSource
1832
- }
1833
- })
2200
+ inject: pageFace(false)
2201
+ }, MemorySettingsPage));
2202
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
2203
+ name: "plugins.bundle.config",
2204
+ key: "dsh-remember",
2205
+ locale: NS,
2206
+ inject: pageFace(true)
1834
2207
  }, MemorySettingsPage));
1835
2208
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1836
2209
  name: "sidebar.footer.action",
@@ -1848,12 +2221,27 @@ function apply(ctx) {
1848
2221
  function t_nav(ctx) {
1849
2222
  return ctx.locale.bind(NS)("nav");
1850
2223
  }
2224
+ /**
2225
+ * The catalog is optional: `remote.session` is not in `inject`, because a
2226
+ * composition without the session controller must still activate the panel
2227
+ * (users can type `provider/model` by hand). Looking the service up through
2228
+ * `ctx.get` keeps activation independent of the controller's ordering.
2229
+ */
2230
+ function lookupSession(ctx) {
2231
+ try {
2232
+ const value = ctx.get("remote.session");
2233
+ return typeof value === "object" && value !== null ? value : null;
2234
+ } catch {
2235
+ return null;
2236
+ }
2237
+ }
1851
2238
  /** Query the Host model catalog and flatten it into selectable routes. */
1852
2239
  function catalogLoader(ctx) {
1853
- const call = ctx.remote?.session?.modelCatalog;
2240
+ const session = lookupSession(ctx);
2241
+ const call = session?.modelCatalog;
1854
2242
  if (call == null) return null;
1855
2243
  return async () => {
1856
- const result = await call.call(ctx.remote.session);
2244
+ const result = await call.call(session);
1857
2245
  if (!result?.ok || result.value == null) throw new Error("model catalog unavailable");
1858
2246
  return result.value.groups.flatMap((group) => group.models.map((model) => ({
1859
2247
  provider: group.id,