dsh-remember 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [SemVer](https://semver.org/). Release artifacts: [GitHub Releases](https://github.com/Arborsm/dsh-remember/releases).
4
+
5
+ ## [0.3.1] - 2026-11-01
6
+
7
+ ### Fixed
8
+
9
+ - `memory_export` / `memory_import` declare plain object-rooted JSON Schema parameters; the author-facing shorthand went to the provider verbatim as `type: null`, which made every model request fail with `Invalid schema for function 'memory_export'` ([#1](https://github.com/Arborsm/dsh-remember/issues/1))
10
+ - Dropped the build-time `@deepseek-ai/dsh-tools` alias and its stub, so no tool helper is bundled
11
+
12
+ ## [0.3.0] - 2026-10-31
13
+
14
+ ### Added
15
+
16
+ - Plugin-page registration (`plugins.bundle.config`) so the memory page opens from Plugins → dsh-remember, next to the existing settings section
17
+ - `icon.svg` and `locale/{en,zh}.json` plugin metadata: the plugin manager shows the localized title and one-line description on the card and page
18
+
19
+ ### Changed
20
+
21
+ - Requires dsh `>=0.2.0-rc.1 <0.3.0-0` (`engines.dsh`); the 0.1.x settings-provider API is gone
22
+ - Host and browser halves now talk over a single `memory` remote service exposed through a hand-written `./typert` manifest and mounted with `ctx.remote.$mount`; the removed `memory-status` / `memory-commands` / `memory-view` settings namespaces are gone
23
+ - Settings-page edits persist to `~/.dsh-memory/config.json` as overrides on the loader config, instead of rewriting the profile patch
24
+ - Export returns the bundle over the remote call and the browser saves it with the same file-picker flow
25
+ - Activation looks `remote.session` up through `ctx.get` and downgrades gracefully when the model catalog is unavailable, so an optional client service can never abort web boot
26
+ - Phase-1/phase-2 sub-agent messages use the v4 producer-owned source kind `dsh-remember`; `plugin` is rejected by session format v4
27
+
28
+ ### Removed
29
+
30
+ - `src/settings.ts` (settings provider surface and the four namespaces)
31
+
32
+ ## [0.2.0] - 2026-09-11
33
+
34
+ ### Changed
35
+
36
+ - Renamed the project and npm package to `dsh-remember`
37
+ - Viewer: records tab merged into the overview (searchable table, most-cited top 5, registry size card); the overview no longer duplicates the summary text
38
+ - Settings: import/export use the native file picker / save dialog instead of typed paths
39
+ - Bundle extension is now plain `.dshmem`
40
+
41
+ ### Added
42
+
43
+ - CI workflow (build / typecheck / test)
44
+ - npm trusted-publisher pipeline (OIDC, staged publishing) — releases stage automatically, approved on npmjs.com
45
+
46
+ ## [0.1.0] - 2026-09-10
47
+
48
+ First public release: background extraction from idle sessions, incremental consolidation into a plain-Markdown memory workspace, summary injection on every prompt, citation feedback loop, one-click translation with segment caching, gzip bundle export/import, sidebar status panel, memory viewer, and a settings page.
49
+
50
+ [0.3.1]: https://github.com/Arborsm/dsh-remember/compare/v0.3.0...v0.3.1
51
+ [0.3.0]: https://github.com/Arborsm/dsh-remember/compare/v0.2.0...v0.3.0
52
+ [0.2.0]: https://github.com/Arborsm/dsh-remember/compare/v0.1.0...v0.2.0
53
+ [0.1.0]: https://github.com/Arborsm/dsh-remember/releases/tag/v0.1.0
package/icon.svg ADDED
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <defs>
3
+ <linearGradient id="dshm" x1="10" y1="12" x2="54" y2="52" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#3F6DFF"/>
5
+ <stop offset="1" stop-color="#7A5CFF"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="10" y="13" width="44" height="11" rx="5.5" fill="url(#dshm)"/>
9
+ <rect x="10" y="28" width="34" height="11" rx="5.5" fill="url(#dshm)" opacity="0.85"/>
10
+ <rect x="10" y="43" width="22" height="11" rx="5.5" fill="url(#dshm)" opacity="0.6"/>
11
+ <circle cx="43" cy="48.5" r="5.5" fill="url(#dshm)"/>
12
+ <circle cx="43" cy="48.5" r="2" fill="#fff" opacity="0.92"/>
13
+ </svg>
package/lib/client.js CHANGED
@@ -583,7 +583,7 @@ function ModelSelect(props) {
583
583
  style: inputStyle(false)
584
584
  }, options);
585
585
  }
586
- /** Upper bound for an import traveling base64 through the settings mirror. */
586
+ /** Upper bound for an import traveling base64 through the host remote. */
587
587
  const IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
588
588
  /** Save bundle bytes: native save dialog where available, download fallback. */
589
589
  async function saveBundleFile(name$1, base64) {
@@ -607,32 +607,45 @@ async function saveBundleFile(name$1, base64) {
607
607
  setTimeout(() => URL.revokeObjectURL(url), 1e3);
608
608
  }
609
609
  /**
610
- * Import/export entry: export asks the host for the bundle and saves it via
611
- * the browser's file dialog; import reads a picked file and ships it base64
612
- * through the memory-commands channel. Outcomes come back as `transfer:`-
613
- * prefixed status detail lines.
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.
614
614
  */
615
615
  function TransferSection(props) {
616
616
  const { t, commands } = props;
617
617
  const [requested, setRequested] = (0, react.useState)(null);
618
618
  const [localError, setLocalError] = (0, react.useState)("");
619
619
  const fileInput = (0, react.useRef)(null);
620
- const lastExportAt = (0, react.useRef)(0);
621
- const send = (action, path = "", payload = "") => {
620
+ const exportBundle = async () => {
622
621
  if (commands == null) return;
623
- (async () => {
624
- await commands.set("path", path);
625
- await commands.set("payload", payload);
626
- await commands.set("action", action);
627
- await commands.set("requestedAt", Date.now());
628
- })();
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
+ }
634
+ };
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
+ }
629
648
  };
630
- (0, react.useEffect)(() => {
631
- const bundle = props.exportBundle;
632
- if (bundle == null || bundle.data === "" || bundle.at <= lastExportAt.current) return void 0;
633
- lastExportAt.current = bundle.at;
634
- saveBundleFile(bundle.name, bundle.data).finally(() => send("export-done"));
635
- }, [props.exportBundle?.at ?? 0]);
636
649
  const pickImport = (event) => {
637
650
  const file = event.target.files?.[0];
638
651
  event.target.value = "";
@@ -641,12 +654,10 @@ function TransferSection(props) {
641
654
  setLocalError(t("transfer_tooBig"));
642
655
  return;
643
656
  }
644
- setLocalError("");
645
- setRequested("import");
646
657
  const reader = new FileReader();
647
658
  reader.onload = () => {
648
659
  const result = String(reader.result ?? "");
649
- send("import", "", result.slice(result.indexOf(",") + 1));
660
+ importBundle(result.slice(result.indexOf(",") + 1));
650
661
  };
651
662
  reader.onerror = () => setLocalError(t("transfer_readFailed"));
652
663
  reader.readAsDataURL(file);
@@ -666,7 +677,7 @@ function TransferSection(props) {
666
677
  onClick: () => {
667
678
  setLocalError("");
668
679
  setRequested(action);
669
- if (action === "export") send("export");
680
+ if (action === "export") exportBundle();
670
681
  else fileInput.current?.click();
671
682
  },
672
683
  style: primaryButtonStyle(commands == null)
@@ -738,9 +749,11 @@ function hintText(t, field) {
738
749
  return hint === `hint_${field}` ? "" : hint;
739
750
  }
740
751
  /**
741
- * The dedicated Memory settings page (occupies one `settings.section` seat):
742
- * live status banner, divided-list sections with catalog-backed model pickers
743
- * 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.
744
757
  * Render failures surface inline instead of blanking the panel.
745
758
  */
746
759
  function MemorySettingsPage(props) {
@@ -770,13 +783,6 @@ function renderMemoryPage(props) {
770
783
  } }, t("unavailable"));
771
784
  const disabled = !state.writable;
772
785
  const status = statusSnapshot?.status === "ready" ? statusSnapshot.value : void 0;
773
- const viewSnapshot = props.useMemoryView != null ? props.useMemoryView((value) => value) : void 0;
774
- const viewValue = viewSnapshot?.status === "ready" ? viewSnapshot.value : void 0;
775
- const exportBundle = viewValue != null && (viewValue.exportData ?? "") !== "" ? {
776
- name: viewValue.exportName ?? "memory.dshmem",
777
- data: viewValue.exportData,
778
- at: viewValue.exportAt ?? 0
779
- } : void 0;
780
786
  const active = status?.phase === "phase1" || status?.phase === "phase2";
781
787
  const statusLabel = active ? status?.phase === "phase1" ? t("phase1") : t("phase2") : t("statusIdle");
782
788
  const groups = /* @__PURE__ */ new Map();
@@ -823,15 +829,16 @@ function renderMemoryPage(props) {
823
829
  onReset: () => props.resetField(field.spec.field)
824
830
  }))));
825
831
  };
832
+ const embedded = props.embedded === true;
826
833
  return (0, react.createElement)("div", { style: {
827
- maxWidth: 640,
828
- paddingBottom: 32
829
- } }, (0, react.createElement)("h2", { style: {
834
+ maxWidth: embedded ? void 0 : 640,
835
+ paddingBottom: embedded ? 0 : 32
836
+ } }, embedded ? null : (0, react.createElement)("h2", { style: {
830
837
  fontSize: 19,
831
838
  fontWeight: 650,
832
839
  margin: "0 0 4px",
833
840
  letterSpacing: .2
834
- } }, t("title")), (0, react.createElement)("p", { style: {
841
+ } }, t("title")), embedded ? null : (0, react.createElement)("p", { style: {
835
842
  fontSize: 13,
836
843
  opacity: .6,
837
844
  margin: "0 0 18px",
@@ -870,8 +877,7 @@ function renderMemoryPage(props) {
870
877
  } }, status?.updatedAt ? new Date(status.updatedAt).toLocaleTimeString() : "")), fieldSection("general"), fieldSection("models"), fieldSection("schedule"), fieldSection("recall"), (0, react.createElement)(TransferSection, {
871
878
  t,
872
879
  commands: props.commands,
873
- statusDetail: status?.detail,
874
- exportBundle
880
+ statusDetail: status?.detail
875
881
  }), (0, react.createElement)(AdvancedSection, {
876
882
  t,
877
883
  fields: groups.get("advanced") ?? [],
@@ -899,6 +905,293 @@ function renderMemoryPage(props) {
899
905
  } }, t("readOnly")) : null));
900
906
  }
901
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
+
902
1195
  //#endregion
903
1196
  //#region src/client/sidebar-action.ts
904
1197
  let styleInjected$1 = false;
@@ -1003,10 +1296,7 @@ function renderAction(props) {
1003
1296
  const sendCommand = (action) => {
1004
1297
  const commands = props.commands;
1005
1298
  if (commands == null) return;
1006
- (async () => {
1007
- await commands.set("action", action);
1008
- await commands.set("requestedAt", Date.now());
1009
- })();
1299
+ commands.run({ action });
1010
1300
  };
1011
1301
  const toggleSetting = (field, value) => {
1012
1302
  props.settings?.set(field, value);
@@ -1450,12 +1740,11 @@ function renderViewer(props) {
1450
1740
  const sendCommand = (action, arg = "", force = false) => {
1451
1741
  const commands = props.commands;
1452
1742
  if (commands == null) return;
1453
- (async () => {
1454
- if (arg !== "") await commands.set("arg", arg);
1455
- if (force) await commands.set("force", true);
1456
- await commands.set("action", action);
1457
- await commands.set("requestedAt", Date.now());
1458
- })();
1743
+ commands.run({
1744
+ action,
1745
+ arg: arg === "" ? void 0 : arg,
1746
+ force: force || void 0
1747
+ });
1459
1748
  };
1460
1749
  return (0, react.createElement)("div", { className: "dshmv-root" }, (0, react.createElement)("div", { className: "dshmv-tabs" }, ...[
1461
1750
  "overview",
@@ -1605,9 +1894,7 @@ const name = "dsh-remember";
1605
1894
  const inject = [
1606
1895
  "slots",
1607
1896
  "locale",
1608
- "settingsScope",
1609
- "remote",
1610
- "remote.session"
1897
+ "remote"
1611
1898
  ];
1612
1899
  const NS = "dsh-memory";
1613
1900
  const ZH = {
@@ -1826,34 +2113,42 @@ const EN = {
1826
2113
  };
1827
2114
  /** Activate the browser half: register the Memory settings page and the status pill. */
1828
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) {
1829
2124
  ctx.effect(() => ctx.locale.register(NS, {
1830
2125
  zh: ZH,
1831
2126
  en: EN
1832
2127
  }), "dsh-memory: dictionaries");
1833
2128
  ctx.effect(() => installCitationStripper(), "dsh-memory: citation stripper");
1834
- const cardScope = ctx.settingsScope.bind({ namespace: "memory" });
1835
- const statusScope = ctx.settingsScope.bind({ namespace: "memory-status" });
1836
- const commandsScope = ctx.settingsScope.bind({ namespace: "memory-commands" });
1837
- const viewerScope = ctx.settingsScope.bind({ namespace: "memory-view" });
1838
- const card = new MemoryCardController(cardScope, catalogLoader(ctx));
1839
- const statusSource = {
1840
- getSnapshot: () => statusScope.getSnapshot(),
1841
- subscribe: (listener) => statusScope.subscribe(listener)
1842
- };
1843
- const viewSource = {
1844
- getSnapshot: () => viewerScope.getSnapshot(),
1845
- subscribe: (listener) => viewerScope.subscribe(listener)
1846
- };
1847
- const commandsView = {
1848
- set: (field, value) => commandsScope.set(field, value),
1849
- unset: (field) => commandsScope.unset(field)
1850
- };
1851
- const memoryView = {
1852
- set: (field, value) => cardScope.set(field, value),
1853
- unset: (field) => cardScope.unset(field),
1854
- getSnapshot: () => cardScope.getSnapshot(),
1855
- subscribe: (listener) => cardScope.subscribe(listener)
1856
- };
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;
1857
2152
  let sidebarRight;
1858
2153
  ctx.inject(["sidebarRightTabs", "sidebarRight"], (sctx) => {
1859
2154
  sidebarRight = sctx.sidebarRight;
@@ -1886,21 +2181,29 @@ function apply(ctx) {
1886
2181
  sidebarRight?.openTab(MEMORY_VIEWER_KIND, {});
1887
2182
  } catch {}
1888
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
+ });
1889
2194
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1890
2195
  name: "settings.section",
1891
2196
  id: "memory",
1892
2197
  order: 16,
1893
2198
  label: () => t_nav(ctx),
1894
2199
  locale: NS,
1895
- inject: () => ({
1896
- ...card.inject(),
1897
- commands: commandsView,
1898
- hooks: {
1899
- memoryPage: card.pageStore(),
1900
- memoryStatus: statusSource,
1901
- memoryView: viewSource
1902
- }
1903
- })
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)
1904
2207
  }, MemorySettingsPage));
1905
2208
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1906
2209
  name: "sidebar.footer.action",
@@ -1918,12 +2221,27 @@ function apply(ctx) {
1918
2221
  function t_nav(ctx) {
1919
2222
  return ctx.locale.bind(NS)("nav");
1920
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
+ }
1921
2238
  /** Query the Host model catalog and flatten it into selectable routes. */
1922
2239
  function catalogLoader(ctx) {
1923
- const call = ctx.remote?.session?.modelCatalog;
2240
+ const session = lookupSession(ctx);
2241
+ const call = session?.modelCatalog;
1924
2242
  if (call == null) return null;
1925
2243
  return async () => {
1926
- const result = await call.call(ctx.remote.session);
2244
+ const result = await call.call(session);
1927
2245
  if (!result?.ok || result.value == null) throw new Error("model catalog unavailable");
1928
2246
  return result.value.groups.flatMap((group) => group.models.map((model) => ({
1929
2247
  provider: group.id,