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/CHANGELOG.md +53 -0
- package/README.md +1 -2
- package/README.zh-CN.md +1 -2
- package/icon.svg +13 -0
- package/lib/client.js +523 -135
- package/lib/client.js.map +1 -1
- package/lib/index.js +701 -7737
- package/lib/typert.host.js +198 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -6
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:
|
|
588
|
-
*
|
|
589
|
-
*
|
|
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
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
703
|
-
*
|
|
704
|
-
*
|
|
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
|
-
(
|
|
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
|
-
(
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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" ?
|
|
1798
|
+
}, tab === "overview" ? (0, react.createElement)(OverviewView, {
|
|
1461
1799
|
t,
|
|
1462
1800
|
texts,
|
|
1463
|
-
showTranslated
|
|
1464
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
"
|
|
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
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
|
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(
|
|
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,
|