dsh-memory-recall 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +96 -0
- package/README.md +181 -0
- package/cordis.patch.yml +16 -0
- package/dsh.plugin.json +18 -0
- package/lib/client.js +1090 -0
- package/package.json +60 -0
- package/screenshots.json +6 -0
- package/src/chats.js +160 -0
- package/src/dsh-tools.js +75 -0
- package/src/index.js +156 -0
- package/src/memory-tool.js +136 -0
- package/src/recall-tool.js +203 -0
- package/src/recall.js +282 -0
- package/src/routes.js +153 -0
- package/src/snapshot.js +63 -0
- package/src/store.js +298 -0
- package/src/threats.js +113 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
// dsh-memory-recall — browser half. Hand-written: no build step.
|
|
2
|
+
//
|
|
3
|
+
// tool.call.toolview[memory] the "Saved to memory" chip in the chat
|
|
4
|
+
// tool.call.toolview[session_search] the recall card: waiting for OK → chats found
|
|
5
|
+
// sidebar.footer.action the Memory entry and its panel
|
|
6
|
+
//
|
|
7
|
+
// The Allow/Deny prompt itself is dsh's own (it takes over the message box
|
|
8
|
+
// and shows the reason the host sent), so nothing here draws it.
|
|
9
|
+
window.__ModuleLoader__.load({
|
|
10
|
+
id: "dsh-memory-recall",
|
|
11
|
+
factory: (require) => {
|
|
12
|
+
var module = { exports: {} };
|
|
13
|
+
var exports = module.exports;
|
|
14
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
|
|
16
|
+
const React = require("react");
|
|
17
|
+
const h = React.createElement;
|
|
18
|
+
|
|
19
|
+
const BASE = "/plugin-data/dsh-memory-recall";
|
|
20
|
+
const TARGET_LABEL = { user: "User profile", memory: "Notes" };
|
|
21
|
+
const TARGET_HINT = {
|
|
22
|
+
user: "Who you are — name, role, preferences, how you like to work.",
|
|
23
|
+
memory: "The model's own notes — environment, conventions, lessons.",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/* ---- look ------------------------------------------------------------ */
|
|
27
|
+
|
|
28
|
+
const STYLE_ID = "dsh-memory-recall-style";
|
|
29
|
+
const CSS = [
|
|
30
|
+
"@keyframes dmr-pulse{0%,100%{opacity:.45}50%{opacity:1}}",
|
|
31
|
+
"@keyframes dmr-pop{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}",
|
|
32
|
+
".dmr-pulse{animation:dmr-pulse 1.4s ease-in-out infinite}",
|
|
33
|
+
".dmr-pop{animation:dmr-pop .16s ease-out}",
|
|
34
|
+
// Hermes Desktop's "legendary" memory row (apps/desktop/src/styles.css):
|
|
35
|
+
// its palette and colour-mix recipe, verbatim, scoped to the chip.
|
|
36
|
+
".dmr-chip{--dmr-yellow:#c08532;--dmr-orange:#db704b;--dmr-purple:#9e94d5;"
|
|
37
|
+
+ "--dmr-tertiary:var(--dsw-alias-label-tertiary,rgba(128,128,128,.85));"
|
|
38
|
+
+ "--dmr-from:color-mix(in srgb,var(--dmr-yellow) 72%,#f5d08a);"
|
|
39
|
+
+ "--dmr-mid:color-mix(in srgb,var(--dmr-orange) 55%,var(--dmr-purple));"
|
|
40
|
+
+ "--dmr-to:color-mix(in srgb,var(--dmr-purple) 82%,#c4b5fd);"
|
|
41
|
+
+ "--dmr-icon:color-mix(in srgb,var(--dmr-yellow) 55%,var(--dmr-purple));"
|
|
42
|
+
+ "--dmr-meta:color-mix(in srgb,var(--dmr-purple) 58%,var(--dmr-tertiary));"
|
|
43
|
+
+ "--dmr-glow:color-mix(in srgb,var(--dmr-purple) 38%,transparent)}",
|
|
44
|
+
".dmr-legendary-title{background-image:linear-gradient(105deg,var(--dmr-from) 0%,var(--dmr-mid) 48%,var(--dmr-to) 100%);"
|
|
45
|
+
+ "-webkit-background-clip:text;background-clip:text;color:transparent!important;font-weight:600}",
|
|
46
|
+
".dmr-legendary-glyph{color:var(--dmr-icon);filter:drop-shadow(0 0 .28rem var(--dmr-glow))}",
|
|
47
|
+
".dmr-legendary-meta{color:var(--dmr-meta)}",
|
|
48
|
+
// While saving: the action word shimmers, as Hermes's tw-shimmer does.
|
|
49
|
+
"@keyframes dmr-shimmer{from{background-position:150% 0}to{background-position:-50% 0}}",
|
|
50
|
+
".dmr-shimmer{background-image:linear-gradient(90deg,var(--dmr-tertiary) 0%,var(--dmr-tertiary) 35%,"
|
|
51
|
+
+ "var(--dsw-alias-label-primary,#f5f5f5) 50%,var(--dmr-tertiary) 65%,var(--dmr-tertiary) 100%);"
|
|
52
|
+
+ "background-size:200% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;"
|
|
53
|
+
+ "animation:dmr-shimmer 1.8s linear infinite}",
|
|
54
|
+
// Not in Hermes: one soft bloom of the glow at the moment the save lands.
|
|
55
|
+
"@keyframes dmr-land{0%{filter:drop-shadow(0 0 0 transparent)}40%{filter:drop-shadow(0 0 .7rem var(--dmr-glow)) drop-shadow(0 0 .25rem var(--dmr-from))}"
|
|
56
|
+
+ "100%{filter:drop-shadow(0 0 .28rem var(--dmr-glow))}}",
|
|
57
|
+
".dmr-land{animation:dmr-land .9s ease-out 1}",
|
|
58
|
+
".dmr-row:hover{background:var(--dsw-alias-interactive-bg-hover-solid, rgba(128,128,128,.12))}",
|
|
59
|
+
".dmr-link{cursor:pointer;background:none;border:none;padding:0;color:inherit;font:inherit;text-align:left}",
|
|
60
|
+
".dmr-link:hover{text-decoration:underline}",
|
|
61
|
+
"@media (prefers-reduced-motion:reduce){.dmr-pulse,.dmr-pop,.dmr-land{animation:none!important}"
|
|
62
|
+
+ ".dmr-shimmer{animation:none!important;color:var(--dmr-tertiary);background:none}}",
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
function injectStyles() {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
68
|
+
const el = document.createElement("style");
|
|
69
|
+
el.id = STYLE_ID;
|
|
70
|
+
el.textContent = CSS;
|
|
71
|
+
document.head.appendChild(el);
|
|
72
|
+
} catch { /* no DOM */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDark() {
|
|
76
|
+
try {
|
|
77
|
+
return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const MUTED = "var(--dsw-alias-label-secondary, rgba(128,128,128,1))";
|
|
84
|
+
const FAINT = "var(--dsw-alias-label-tertiary, rgba(128,128,128,.8))";
|
|
85
|
+
const ACCENT = "#a78bfa";
|
|
86
|
+
|
|
87
|
+
/** Brain outline (Lucide "brain", ISC licence), in the text colour. */
|
|
88
|
+
function BrainIcon(size, extra) {
|
|
89
|
+
const paths = [
|
|
90
|
+
"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z",
|
|
91
|
+
"M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z",
|
|
92
|
+
"M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4",
|
|
93
|
+
"M17.599 6.5a3 3 0 0 0 .399-1.375",
|
|
94
|
+
"M6.003 5.125A3 3 0 0 0 6.401 6.5",
|
|
95
|
+
"M3.477 10.896a4 4 0 0 1 .585-.396",
|
|
96
|
+
"M19.938 10.5a4 4 0 0 1 .585.396",
|
|
97
|
+
"M6 18a4 4 0 0 1-1.967-.516",
|
|
98
|
+
"M19.967 17.484A4 4 0 0 1 18 18",
|
|
99
|
+
];
|
|
100
|
+
return h("svg", {
|
|
101
|
+
width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor",
|
|
102
|
+
strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
103
|
+
style: { flex: "none", display: "block" }, className: extra || undefined,
|
|
104
|
+
}, paths.map((d, i) => h("path", { key: i, d })));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The solid brain Hermes Desktop draws in its tool rows — Phosphor Icons
|
|
109
|
+
* "brain", fill weight (MIT), the same path its tool-icon.tsx inlines.
|
|
110
|
+
*/
|
|
111
|
+
function SolidBrainIcon(size, extra) {
|
|
112
|
+
return h("svg", {
|
|
113
|
+
width: size, height: size, viewBox: "0 0 256 256", fill: "currentColor", "aria-hidden": "true",
|
|
114
|
+
style: { flex: "none", display: "block" }, className: extra || undefined,
|
|
115
|
+
}, h("path", {
|
|
116
|
+
d: "M248,124a56.11,56.11,0,0,0-32-50.61V72a48,48,0,0,0-88-26.49A48,48,0,0,0,40,72v1.39a56,56,0,0,0,0,101.22V176a48,48,0,0,0,88,26.49A48,48,0,0,0,216,176v-1.39A56.09,56.09,0,0,0,248,124ZM88,208a32,32,0,0,1-31.81-28.56A55.87,55.87,0,0,0,64,180h8a8,8,0,0,0,0-16H64A40,40,0,0,1,51.2,86.1a8,8,0,0,0,5.6-6.83A32,32,0,0,1,120,72v75.36A47.79,47.79,0,0,0,88,136a8,8,0,0,0,0,16,32,32,0,0,1,32,32A24,24,0,0,1,88,208Zm104-44h-8a8,8,0,0,0,0,16h8a55.87,55.87,0,0,0,7.81-.56A32,32,0,1,1,136,184a32,32,0,0,1,32-32,8,8,0,0,0,0-16,47.79,47.79,0,0,0-32,11.36V72a32,32,0,0,1,63.2-7.27,8,8,0,0,0,5.6,6.83A40,40,0,0,1,192,164Z",
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A clock with a turn-back arrow: looking back through earlier chats. */
|
|
121
|
+
function HistoryIcon(size, extra) {
|
|
122
|
+
return h("svg", {
|
|
123
|
+
width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor",
|
|
124
|
+
strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
125
|
+
style: { flex: "none", display: "block" }, className: extra || undefined,
|
|
126
|
+
}, [
|
|
127
|
+
h("path", { key: "a", d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
128
|
+
h("path", { key: "b", d: "M3 3v5h5" }),
|
|
129
|
+
h("path", { key: "c", d: "M12 7v5l4 2" }),
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Chevron(open) {
|
|
134
|
+
return h("span", { style: { fontSize: 10, opacity: 0.6, marginLeft: 2 } }, open ? "▾" : "▸");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** "**x**" from a snippet becomes bold; nothing else is interpreted. */
|
|
138
|
+
function withBold(text) {
|
|
139
|
+
return String(text ?? "").split("**").map((part, i) => (i % 2 ? h("b", { key: i }, part) : part));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function UsageBar(usage, width) {
|
|
143
|
+
if (!usage || !usage.limit) return null;
|
|
144
|
+
const pct = Math.max(0, Math.min(100, usage.percent));
|
|
145
|
+
const colour = pct >= 90 ? "#f59e0b" : ACCENT;
|
|
146
|
+
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 11, color: FAINT } }, [
|
|
147
|
+
h("div", { key: "bar", style: { width: width || 120, height: 4, borderRadius: 999, background: "rgba(128,128,128,.25)", overflow: "hidden" } },
|
|
148
|
+
h("div", { style: { width: pct + "%", height: "100%", background: colour, transition: "width .3s" } })),
|
|
149
|
+
h("span", { key: "t", style: { fontVariantNumeric: "tabular-nums" } },
|
|
150
|
+
pct + "% · " + Number(usage.chars).toLocaleString("en-US") + "/" + Number(usage.limit).toLocaleString("en-US")),
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ---- reading a tool call ---------------------------------------------- */
|
|
155
|
+
|
|
156
|
+
const isSettled = (block) => block !== undefined && block !== null && "kind" in block;
|
|
157
|
+
|
|
158
|
+
function argsOf(block) {
|
|
159
|
+
if (!block) return {};
|
|
160
|
+
const raw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
|
|
161
|
+
try {
|
|
162
|
+
const v = JSON.parse(raw);
|
|
163
|
+
return v && typeof v === "object" ? v : {};
|
|
164
|
+
} catch {
|
|
165
|
+
return {}; // still streaming, or not JSON
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function textOf(block) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
for (const c of (Array.isArray(block?.content) ? block.content : [])) {
|
|
172
|
+
if (typeof c?.text === "string") parts.push(c.text);
|
|
173
|
+
else if (Array.isArray(c?.content)) for (const inner of c.content) if (typeof inner?.text === "string") parts.push(inner.text);
|
|
174
|
+
}
|
|
175
|
+
return parts.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function usageIn(text) {
|
|
179
|
+
const m = /(\d+)% — ([\d,]+)\/([\d,]+) chars/.exec(text || "");
|
|
180
|
+
if (!m) return null;
|
|
181
|
+
const n = (s) => Number(s.replace(/,/g, ""));
|
|
182
|
+
return { percent: Number(m[1]), chars: n(m[2]), limit: n(m[3]) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The changes a memory call asked for, as rows: + added, ~ replaced, − removed. */
|
|
186
|
+
function changesOf(args) {
|
|
187
|
+
const ops = Array.isArray(args.operations) && args.operations.length ? args.operations
|
|
188
|
+
: args.action ? [{ action: args.action, content: args.content, old_text: args.old_text }] : [];
|
|
189
|
+
return ops.map((op) => {
|
|
190
|
+
const content = op.content ?? op.new_text;
|
|
191
|
+
if (op.action === "add") return { sign: "+", text: content };
|
|
192
|
+
if (op.action === "remove") return { sign: "−", text: op.old_text };
|
|
193
|
+
if (op.action === "replace") return { sign: "~", text: content, from: op.old_text };
|
|
194
|
+
return { sign: "?", text: JSON.stringify(op) };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ---- the "Saved to memory" chip --------------------------------------- */
|
|
199
|
+
|
|
200
|
+
function memoryStatus(block, args) {
|
|
201
|
+
if (!isSettled(block)) return { label: "Saving to memory…", action: "Saving", rest: " to memory", tone: "busy" };
|
|
202
|
+
const text = textOf(block);
|
|
203
|
+
const ops = changesOf(args).map((c) => c.sign);
|
|
204
|
+
// A successful call, even one that changed nothing, lands "legendary" — as in Hermes.
|
|
205
|
+
if (/^Already in memory/.test(text)) return { label: "Already in memory", tone: "ok" };
|
|
206
|
+
if (/^Blocked/.test(text)) return { label: "Memory write blocked", tone: "bad" };
|
|
207
|
+
if (/^Not saved/.test(text)) {
|
|
208
|
+
return /over its budget/.test(text) ? { label: "Memory full — making room", tone: "warn" } : { label: "Not saved to memory", tone: "bad" };
|
|
209
|
+
}
|
|
210
|
+
if (block.isError) return { label: "Not saved to memory", tone: "bad" };
|
|
211
|
+
if (ops.length && ops.every((s) => s === "−")) return { label: "Removed from memory", tone: "ok" };
|
|
212
|
+
if (ops.length && ops.every((s) => s === "+")) return { label: "Saved to memory", tone: "ok" };
|
|
213
|
+
return { label: "Updated memory", tone: "ok" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const TONE = { ok: ACCENT, quiet: MUTED, busy: MUTED, warn: "#f59e0b", bad: "#ef4444" };
|
|
217
|
+
|
|
218
|
+
function MemoryCall(props) {
|
|
219
|
+
const { block } = props;
|
|
220
|
+
const [open, setOpen] = React.useState(false);
|
|
221
|
+
// Drawn while still saving? Then this chip watched the save land, and
|
|
222
|
+
// gets the one bloom. A chip from history just shows the settled look.
|
|
223
|
+
const [watchedLand] = React.useState(() => !isSettled(block));
|
|
224
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
225
|
+
const args = argsOf(block);
|
|
226
|
+
const status = memoryStatus(block, args);
|
|
227
|
+
const legendary = status.tone === "ok";
|
|
228
|
+
const text = isSettled(block) ? textOf(block) : "";
|
|
229
|
+
const usage = usageIn(text);
|
|
230
|
+
const changes = changesOf(args);
|
|
231
|
+
const target = args.target === "user" ? "user profile" : args.target === "memory" ? "notes" : null;
|
|
232
|
+
|
|
233
|
+
const glyphClass = legendary ? "dmr-legendary-glyph" + (watchedLand ? " dmr-land" : "")
|
|
234
|
+
: status.tone === "busy" ? "dmr-pulse" : undefined;
|
|
235
|
+
const title = status.tone === "busy"
|
|
236
|
+
? h("span", { key: "l", style: { color: FAINT } }, [h("span", { key: "a", className: "dmr-shimmer" }, status.action), status.rest])
|
|
237
|
+
: h("span", {
|
|
238
|
+
key: "l",
|
|
239
|
+
className: legendary ? "dmr-legendary-title" : undefined,
|
|
240
|
+
style: legendary ? undefined : { color: TONE[status.tone] },
|
|
241
|
+
}, status.label);
|
|
242
|
+
|
|
243
|
+
return h("div", { "data-plugin": "dsh-memory-recall-chip", className: "dmr-chip", style: { margin: "2px 0", fontSize: 13 } }, [
|
|
244
|
+
h("button", {
|
|
245
|
+
key: "head", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open,
|
|
246
|
+
style: { display: "inline-flex", alignItems: "center", gap: 7, color: MUTED, padding: "2px 0" },
|
|
247
|
+
}, [
|
|
248
|
+
h("span", {
|
|
249
|
+
key: "i", className: glyphClass,
|
|
250
|
+
style: { display: "inline-flex", color: legendary ? undefined : TONE[status.tone] },
|
|
251
|
+
}, SolidBrainIcon(14)),
|
|
252
|
+
title,
|
|
253
|
+
target ? h("span", { key: "t", className: legendary ? "dmr-legendary-meta" : undefined, style: legendary ? { fontSize: 12 } : { color: FAINT, fontSize: 12 } }, "· " + target) : null,
|
|
254
|
+
Chevron(open),
|
|
255
|
+
]),
|
|
256
|
+
open
|
|
257
|
+
? h("div", {
|
|
258
|
+
key: "body", className: "dmr-pop",
|
|
259
|
+
style: { margin: "4px 0 6px 22px", display: "flex", flexDirection: "column", gap: 4, fontSize: 12 },
|
|
260
|
+
}, [
|
|
261
|
+
...changes.map((c, i) => h("div", { key: "c" + i, style: { display: "flex", gap: 6, alignItems: "baseline" } }, [
|
|
262
|
+
h("span", { key: "s", style: { color: c.sign === "−" ? "#ef4444" : c.sign === "+" ? "#22c55e" : ACCENT, fontWeight: 700, width: 10, flex: "none" } }, c.sign),
|
|
263
|
+
h("span", { key: "x", style: { whiteSpace: "pre-wrap", wordBreak: "break-word" } },
|
|
264
|
+
c.from ? [h("span", { key: "f", style: { color: FAINT, textDecoration: "line-through" } }, c.from), " → ", c.text] : c.text),
|
|
265
|
+
])),
|
|
266
|
+
usage ? h("div", { key: "u", style: { marginTop: 2 } }, UsageBar(usage)) : null,
|
|
267
|
+
isSettled(block) && !/^Saved|^Already/.test(text)
|
|
268
|
+
? h("div", { key: "why", style: { color: FAINT, whiteSpace: "pre-wrap" } }, text.split("\n\n")[0])
|
|
269
|
+
: null,
|
|
270
|
+
])
|
|
271
|
+
: null,
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ---- the recall card --------------------------------------------------- */
|
|
276
|
+
|
|
277
|
+
const DONE = new Set(["done", "declined", "error"]);
|
|
278
|
+
|
|
279
|
+
function useRecallRecord(callId, settled) {
|
|
280
|
+
const [record, setRecord] = React.useState(null);
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
if (!callId) return undefined;
|
|
283
|
+
let alive = true;
|
|
284
|
+
let timer = null;
|
|
285
|
+
const pull = async () => {
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch(BASE + "/recall?callId=" + encodeURIComponent(callId), { headers: { accept: "application/json" } });
|
|
288
|
+
const body = res.ok ? await res.json() : null;
|
|
289
|
+
if (!alive) return;
|
|
290
|
+
if (body) setRecord(body);
|
|
291
|
+
if (settled && (!body || DONE.has(body.status))) return; // final read done
|
|
292
|
+
} catch { /* keep polling */ }
|
|
293
|
+
if (alive && !settled) timer = setTimeout(pull, 800);
|
|
294
|
+
};
|
|
295
|
+
pull();
|
|
296
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
297
|
+
}, [callId, settled]);
|
|
298
|
+
return record;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function recallStatus(record, block) {
|
|
302
|
+
const settled = isSettled(block);
|
|
303
|
+
const text = settled ? textOf(block) : "";
|
|
304
|
+
const status = record?.status ?? (settled ? (/^Found|^From|^Most recent|messages\./.test(text) ? "done" : /declined|withdrawn|Nobody could be asked/.test(text) ? "declined" : /^Recall failed/.test(text) ? "error" : "done") : "awaiting-approval");
|
|
305
|
+
switch (status) {
|
|
306
|
+
case "awaiting-approval": return { label: "Waiting for your OK to read past chats", tone: "warn", busy: true };
|
|
307
|
+
case "starting":
|
|
308
|
+
case "searching": return { label: "Searching past chats…", tone: "busy", busy: true };
|
|
309
|
+
case "declined": return { label: "Not allowed — nothing was read", tone: "quiet" };
|
|
310
|
+
case "error": return { label: "Recall failed", tone: "bad" };
|
|
311
|
+
default: return { label: "Recalled from past chats", tone: "ok" };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function describeAsk(args) {
|
|
316
|
+
if (typeof args.query === "string" && args.query.trim()) return "“" + args.query.trim() + "”";
|
|
317
|
+
if (args.session_id) return "one earlier chat";
|
|
318
|
+
return "recent chats";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const dayOf = (ms) => (Number.isFinite(ms) ? new Date(ms).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" }) : "");
|
|
322
|
+
|
|
323
|
+
function makeRecallCall(openChat) {
|
|
324
|
+
return function RecallCall(props) {
|
|
325
|
+
const { callId, block } = props;
|
|
326
|
+
const settled = isSettled(block);
|
|
327
|
+
const record = useRecallRecord(callId, settled);
|
|
328
|
+
const [open, setOpen] = React.useState(true);
|
|
329
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
330
|
+
const args = argsOf(block);
|
|
331
|
+
const status = recallStatus(record, block);
|
|
332
|
+
const summary = record?.summary ?? null;
|
|
333
|
+
const results = summary?.mode === "discover" ? summary.results : summary?.mode === "browse" ? summary.sessions : null;
|
|
334
|
+
const count = summary?.mode === "discover" ? summary.results.length : null;
|
|
335
|
+
|
|
336
|
+
const chatRow = (r, i) => h("div", {
|
|
337
|
+
key: r.sessionId + i, className: "dmr-row",
|
|
338
|
+
style: { display: "flex", flexDirection: "column", gap: 2, padding: "5px 8px", borderRadius: 8 },
|
|
339
|
+
}, [
|
|
340
|
+
h("div", { key: "t", style: { display: "flex", alignItems: "baseline", gap: 8 } }, [
|
|
341
|
+
h("button", {
|
|
342
|
+
key: "o", type: "button", className: "dmr-link", title: "Open this chat",
|
|
343
|
+
onClick: () => openChat(r.sessionId),
|
|
344
|
+
style: { fontWeight: 600, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
345
|
+
}, r.title || "Untitled chat"),
|
|
346
|
+
h("span", { key: "d", style: { marginLeft: "auto", color: FAINT, fontSize: 11, flex: "none" } }, dayOf(r.createdAt)),
|
|
347
|
+
]),
|
|
348
|
+
r.snippet ? h("div", { key: "s", style: { color: MUTED, fontSize: 12, lineHeight: "17px" } }, withBold(r.snippet)) : null,
|
|
349
|
+
r.matchedWords ? h("div", { key: "w", style: { color: FAINT, fontSize: 11 } }, "matched: " + r.matchedWords.join(", ")) : null,
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
let body = null;
|
|
353
|
+
if (open && results && results.length) {
|
|
354
|
+
body = h("div", { key: "body", className: "dmr-pop", style: { marginTop: 4, display: "flex", flexDirection: "column", gap: 1 } }, [
|
|
355
|
+
...results.map(chatRow),
|
|
356
|
+
summary.engine === "scan"
|
|
357
|
+
? h("div", { key: "scan", style: { color: FAINT, fontSize: 11, padding: "2px 8px" } }, "dsh's full-text search is off, so recent chats were scanned instead.")
|
|
358
|
+
: null,
|
|
359
|
+
]);
|
|
360
|
+
} else if (open && summary && (summary.mode === "read" || summary.mode === "scroll")) {
|
|
361
|
+
body = h("div", { key: "body", style: { marginTop: 4 } }, chatRow({ ...summary.session, snippet: summary.mode === "read" ? summary.count + " messages read" : "read around one message" }, 0));
|
|
362
|
+
} else if (open && status.label === "Recalled from past chats" && count === 0) {
|
|
363
|
+
body = h("div", { key: "body", style: { color: FAINT, fontSize: 12, margin: "4px 0 0 22px" } }, "No earlier chat matched.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return h("div", {
|
|
367
|
+
"data-plugin": "dsh-memory-recall-card",
|
|
368
|
+
style: {
|
|
369
|
+
margin: "4px 0", padding: "7px 10px", borderRadius: 10, fontSize: 13,
|
|
370
|
+
border: "1px solid " + (status.tone === "warn" ? "rgba(245,158,11,.45)" : "rgba(128,128,128,.22)"),
|
|
371
|
+
background: status.tone === "warn" ? "rgba(245,158,11,.08)" : "rgba(128,128,128,.05)",
|
|
372
|
+
},
|
|
373
|
+
}, [
|
|
374
|
+
h("button", {
|
|
375
|
+
key: "head", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open,
|
|
376
|
+
style: { display: "flex", alignItems: "center", gap: 8, width: "100%" },
|
|
377
|
+
}, [
|
|
378
|
+
h("span", { key: "i", style: { color: TONE[status.tone], display: "inline-flex" } }, HistoryIcon(15, status.busy ? "dmr-pulse" : undefined)),
|
|
379
|
+
h("span", { key: "l", style: { fontWeight: 600 } }, status.label),
|
|
380
|
+
h("span", { key: "q", style: { color: FAINT, fontSize: 12, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, "· " + describeAsk(args)),
|
|
381
|
+
count !== null && count > 0 ? h("span", { key: "n", style: { marginLeft: "auto", color: FAINT, fontSize: 12, flex: "none" } }, count + " found") : null,
|
|
382
|
+
results && results.length ? Chevron(open) : null,
|
|
383
|
+
]),
|
|
384
|
+
status.label.startsWith("Waiting")
|
|
385
|
+
? h("div", { key: "hint", style: { color: MUTED, fontSize: 12, marginTop: 3, marginLeft: 23 } }, "Allow or deny it in the message box below.")
|
|
386
|
+
: null,
|
|
387
|
+
body,
|
|
388
|
+
]);
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ---- the Memory panel -------------------------------------------------- */
|
|
393
|
+
|
|
394
|
+
async function api(path, init) {
|
|
395
|
+
const res = await fetch(BASE + path, { headers: { accept: "application/json", "content-type": "application/json" }, ...init });
|
|
396
|
+
const body = await res.json().catch(() => ({}));
|
|
397
|
+
if (!res.ok) throw new Error(body?.result?.error || body?.error || "HTTP " + res.status);
|
|
398
|
+
return body;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const PANEL_CSS = [
|
|
402
|
+
// dsh's sidebar footer is a flex ROW that does not wrap, and every entry
|
|
403
|
+
// asks for 100% of it, so the second entry is pushed off its right edge.
|
|
404
|
+
// Measured on the live page: footer row > (display: contents wrapper) >
|
|
405
|
+
// each entry. So the footer is the element whose GRANDCHILD is this
|
|
406
|
+
// entry; wrapping it puts each entry on its own line, like Settings.
|
|
407
|
+
// A CSS rule, not a one-off style, so it holds when dsh re-renders.
|
|
408
|
+
"div:has(> * > [data-plugin=\"dsh-memory-recall-sidebar\"]),div:has(> [data-plugin=\"dsh-memory-recall-sidebar\"]){flex-wrap:wrap}",
|
|
409
|
+
".dmr-panel{--dmr-yellow:#c08532;--dmr-orange:#db704b;--dmr-purple:#9e94d5;"
|
|
410
|
+
+ "--dmr-from:color-mix(in srgb,var(--dmr-yellow) 72%,#f5d08a);"
|
|
411
|
+
+ "--dmr-mid:color-mix(in srgb,var(--dmr-orange) 55%,var(--dmr-purple));"
|
|
412
|
+
+ "--dmr-to:color-mix(in srgb,var(--dmr-purple) 82%,#c4b5fd);"
|
|
413
|
+
+ "--dmr-glow:color-mix(in srgb,var(--dmr-purple) 38%,transparent)}",
|
|
414
|
+
".dmr-card{position:relative;border-radius:10px;padding:9px 10px 8px 12px;transition:background .15s,border-color .15s}",
|
|
415
|
+
".dmr-card::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;"
|
|
416
|
+
+ "background:linear-gradient(180deg,var(--dmr-from),var(--dmr-to));opacity:.75}",
|
|
417
|
+
".dmr-card .dmr-actions{opacity:0;transition:opacity .15s}",
|
|
418
|
+
".dmr-card:hover .dmr-actions,.dmr-card:focus-within .dmr-actions{opacity:1}",
|
|
419
|
+
".dmr-clamp{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}",
|
|
420
|
+
".dmr-icon-btn{cursor:pointer;border:none;background:transparent;color:inherit;width:26px;height:26px;border-radius:7px;"
|
|
421
|
+
+ "display:inline-flex;align-items:center;justify-content:center;opacity:.7}",
|
|
422
|
+
".dmr-icon-btn:hover{opacity:1;background:rgba(128,128,128,.18)}",
|
|
423
|
+
".dmr-tab{cursor:pointer;border:none;background:transparent;color:inherit;font:inherit;display:inline-flex;align-items:center;"
|
|
424
|
+
+ "gap:7px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;opacity:.6;transition:opacity .15s,background .15s}",
|
|
425
|
+
".dmr-tab:hover{opacity:.9}",
|
|
426
|
+
".dmr-tab[aria-selected=\"true\"]{opacity:1;background:rgba(128,128,128,.16)}",
|
|
427
|
+
".dmr-bar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--dmr-from),var(--dmr-mid),var(--dmr-to));transition:width .35s}",
|
|
428
|
+
".dmr-gradient-text{background-image:linear-gradient(105deg,var(--dmr-from) 0%,var(--dmr-mid) 48%,var(--dmr-to) 100%);"
|
|
429
|
+
+ "-webkit-background-clip:text;background-clip:text;color:transparent}",
|
|
430
|
+
".dmr-glyph-glow{color:color-mix(in srgb,var(--dmr-yellow) 55%,var(--dmr-purple));filter:drop-shadow(0 0 .3rem var(--dmr-glow))}",
|
|
431
|
+
".dmr-input:focus{outline:none;border-color:color-mix(in srgb,var(--dmr-purple) 70%,transparent)!important;"
|
|
432
|
+
+ "box-shadow:0 0 0 3px color-mix(in srgb,var(--dmr-purple) 22%,transparent)}",
|
|
433
|
+
].join("\n");
|
|
434
|
+
const PANEL_STYLE_ID = "dsh-memory-recall-panel-style";
|
|
435
|
+
|
|
436
|
+
function injectPanelStyles() {
|
|
437
|
+
try {
|
|
438
|
+
if (typeof document === "undefined" || document.getElementById(PANEL_STYLE_ID)) return;
|
|
439
|
+
const el = document.createElement("style");
|
|
440
|
+
el.id = PANEL_STYLE_ID;
|
|
441
|
+
el.textContent = PANEL_CSS;
|
|
442
|
+
document.head.appendChild(el);
|
|
443
|
+
} catch { /* no DOM */ }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const fmt = (n) => Number(n || 0).toLocaleString("en-US");
|
|
447
|
+
const DELIM_COST = 3; // the "\n§\n" between two entries
|
|
448
|
+
|
|
449
|
+
/** A small usage ring, for the tabs. */
|
|
450
|
+
function Ring(percent, size) {
|
|
451
|
+
const r = 6.5;
|
|
452
|
+
const c = 2 * Math.PI * r;
|
|
453
|
+
const p = Math.max(0, Math.min(100, percent || 0));
|
|
454
|
+
return h("svg", { width: size || 16, height: size || 16, viewBox: "0 0 16 16", "aria-hidden": "true", style: { flex: "none" } }, [
|
|
455
|
+
h("circle", { key: "t", cx: 8, cy: 8, r, fill: "none", stroke: "rgba(128,128,128,.3)", strokeWidth: 2 }),
|
|
456
|
+
h("circle", {
|
|
457
|
+
key: "f", cx: 8, cy: 8, r, fill: "none", strokeWidth: 2, strokeLinecap: "round",
|
|
458
|
+
stroke: p >= 90 ? "#f59e0b" : "#9e94d5", strokeDasharray: c, strokeDashoffset: c * (1 - p / 100),
|
|
459
|
+
transform: "rotate(-90 8 8)",
|
|
460
|
+
}),
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function PencilIcon() {
|
|
465
|
+
return h("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" }, [
|
|
466
|
+
h("path", { key: "a", d: "M12 20h9" }),
|
|
467
|
+
h("path", { key: "b", d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" }),
|
|
468
|
+
]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function TrashIcon() {
|
|
472
|
+
return h("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" }, [
|
|
473
|
+
h("path", { key: "a", d: "M3 6h18" }),
|
|
474
|
+
h("path", { key: "b", d: "M8 6V4h8v2" }),
|
|
475
|
+
h("path", { key: "c", d: "M19 6l-1 14H6L5 6" }),
|
|
476
|
+
]);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** How the budget would look with `text` in place of `replacing` (or added). */
|
|
480
|
+
function budgetAfter(data, text, replacing) {
|
|
481
|
+
const used = data?.usage?.chars ?? 0;
|
|
482
|
+
const limit = data?.usage?.limit ?? 0;
|
|
483
|
+
const count = (data?.entries ?? []).length;
|
|
484
|
+
const next = String(text || "").trim().length;
|
|
485
|
+
let after;
|
|
486
|
+
if (replacing !== undefined) after = used - replacing.length + next;
|
|
487
|
+
else after = used + next + (count > 0 && next > 0 ? DELIM_COST : 0);
|
|
488
|
+
return { after, limit, delta: after - used, over: limit > 0 && after > limit };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function BudgetLine(b) {
|
|
492
|
+
const sign = b.delta > 0 ? "+" : b.delta < 0 ? "−" : "±";
|
|
493
|
+
return h("span", { style: { fontSize: 11, color: b.over ? "#ef4444" : FAINT, fontVariantNumeric: "tabular-nums" } },
|
|
494
|
+
sign + fmt(Math.abs(b.delta)) + " chars · " + fmt(b.after) + " / " + fmt(b.limit) + (b.over ? " — over the limit" : ""));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function EntryCard(props) {
|
|
498
|
+
const { entry, data, dark, onSave, onDelete } = props;
|
|
499
|
+
const [mode, setMode] = React.useState("view"); // view | edit | confirm
|
|
500
|
+
const [draft, setDraft] = React.useState(entry);
|
|
501
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
502
|
+
const [busy, setBusy] = React.useState(false);
|
|
503
|
+
const [error, setError] = React.useState(null);
|
|
504
|
+
const long = entry.length > 240;
|
|
505
|
+
const run = async (fn) => {
|
|
506
|
+
setBusy(true);
|
|
507
|
+
setError(null);
|
|
508
|
+
try {
|
|
509
|
+
await fn();
|
|
510
|
+
return true;
|
|
511
|
+
} catch (e) {
|
|
512
|
+
setError(String(e?.message || e));
|
|
513
|
+
return false;
|
|
514
|
+
} finally {
|
|
515
|
+
setBusy(false);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const surface = dark ? "rgba(255,255,255,.035)" : "rgba(0,0,0,.025)";
|
|
519
|
+
const field = {
|
|
520
|
+
width: "100%", boxSizing: "border-box", resize: "vertical", fontFamily: "inherit", fontSize: 12.5, lineHeight: "18px",
|
|
521
|
+
padding: "7px 9px", borderRadius: 8, border: "1px solid rgba(128,128,128,.35)",
|
|
522
|
+
background: dark ? "rgba(0,0,0,.25)" : "#fff", color: "inherit", minHeight: 72,
|
|
523
|
+
};
|
|
524
|
+
const button = (label, onClick, primary) => h("button", {
|
|
525
|
+
key: label, type: "button", disabled: busy, onClick,
|
|
526
|
+
style: {
|
|
527
|
+
cursor: busy ? "wait" : "pointer", borderRadius: 999, padding: "3px 12px", fontSize: 11.5, fontWeight: 600,
|
|
528
|
+
border: primary ? "none" : "1px solid rgba(128,128,128,.35)",
|
|
529
|
+
background: primary ? "linear-gradient(105deg,var(--dmr-from),var(--dmr-to))" : "transparent",
|
|
530
|
+
color: primary ? "#1b1b1d" : "inherit",
|
|
531
|
+
},
|
|
532
|
+
}, label);
|
|
533
|
+
|
|
534
|
+
if (mode === "edit") {
|
|
535
|
+
const b = budgetAfter(data, draft, entry);
|
|
536
|
+
return h("div", { className: "dmr-card", style: { background: surface, border: "1px solid rgba(158,148,213,.45)" } }, [
|
|
537
|
+
h("textarea", {
|
|
538
|
+
key: "t", className: "dmr-input", value: draft, autoFocus: true, style: field,
|
|
539
|
+
onChange: (e) => setDraft(e.target.value),
|
|
540
|
+
onKeyDown: async (e) => {
|
|
541
|
+
if (e.key === "Escape") { e.stopPropagation(); setMode("view"); setDraft(entry); }
|
|
542
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !b.over) {
|
|
543
|
+
if (await run(() => onSave(entry, draft))) setMode("view");
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
h("div", { key: "b", style: { display: "flex", alignItems: "center", gap: 6, marginTop: 6 } }, [
|
|
548
|
+
BudgetLine(b),
|
|
549
|
+
h("span", { key: "s", style: { flex: 1 } }),
|
|
550
|
+
button("Cancel", () => { setMode("view"); setDraft(entry); }),
|
|
551
|
+
button("Save", async () => { if (!b.over && (await run(() => onSave(entry, draft)))) setMode("view"); }, true),
|
|
552
|
+
]),
|
|
553
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5, marginTop: 4 } }, error) : null,
|
|
554
|
+
]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return h("div", { className: "dmr-card", style: { background: surface, border: "1px solid transparent" } }, [
|
|
558
|
+
h("div", { key: "row", style: { display: "flex", gap: 6, alignItems: "flex-start" } }, [
|
|
559
|
+
h("div", {
|
|
560
|
+
key: "x", className: long && !expanded ? "dmr-clamp" : undefined,
|
|
561
|
+
style: { flex: 1, minWidth: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 12.5, lineHeight: "18px" },
|
|
562
|
+
}, entry),
|
|
563
|
+
mode === "view"
|
|
564
|
+
? h("div", { key: "a", className: "dmr-actions", style: { display: "flex", gap: 2, flex: "none", marginTop: -3 } }, [
|
|
565
|
+
h("button", { key: "e", type: "button", className: "dmr-icon-btn", title: "Edit", "aria-label": "Edit entry", onClick: () => { setDraft(entry); setMode("edit"); } }, PencilIcon()),
|
|
566
|
+
h("button", { key: "d", type: "button", className: "dmr-icon-btn", title: "Delete", "aria-label": "Delete entry", onClick: () => setMode("confirm") }, TrashIcon()),
|
|
567
|
+
])
|
|
568
|
+
: null,
|
|
569
|
+
]),
|
|
570
|
+
h("div", { key: "meta", style: { display: "flex", alignItems: "center", gap: 10, marginTop: 5, fontSize: 11, color: FAINT } }, [
|
|
571
|
+
h("span", { key: "n", style: { fontVariantNumeric: "tabular-nums" } }, fmt(entry.length) + " chars"),
|
|
572
|
+
long ? h("button", { key: "m", type: "button", className: "dmr-link", onClick: () => setExpanded(!expanded), style: { fontSize: 11, color: "var(--dmr-purple)" } }, expanded ? "Show less" : "Show more") : null,
|
|
573
|
+
mode === "confirm"
|
|
574
|
+
? h("span", { key: "c", style: { marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 6, color: "inherit" } }, [
|
|
575
|
+
h("span", { key: "q", style: { fontSize: 11.5 } }, "Delete this entry?"),
|
|
576
|
+
button("Keep", () => setMode("view")),
|
|
577
|
+
h("button", {
|
|
578
|
+
key: "y", type: "button", disabled: busy, onClick: () => run(() => onDelete(entry)),
|
|
579
|
+
style: { cursor: "pointer", borderRadius: 999, padding: "3px 12px", fontSize: 11.5, fontWeight: 600, border: "none", background: "#ef4444", color: "#fff" },
|
|
580
|
+
}, "Delete"),
|
|
581
|
+
])
|
|
582
|
+
: null,
|
|
583
|
+
]),
|
|
584
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5, marginTop: 4 } }, error) : null,
|
|
585
|
+
]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function MemoryTab(props) {
|
|
589
|
+
const { target, data, dark, onEdit } = props;
|
|
590
|
+
const [filter, setFilter] = React.useState("");
|
|
591
|
+
const [adding, setAdding] = React.useState("");
|
|
592
|
+
const [busy, setBusy] = React.useState(false);
|
|
593
|
+
const [error, setError] = React.useState(null);
|
|
594
|
+
const entries = data?.entries ?? [];
|
|
595
|
+
const shown = filter.trim() ? entries.filter((e) => e.toLowerCase().includes(filter.trim().toLowerCase())) : entries;
|
|
596
|
+
const usage = data?.usage ?? { chars: 0, limit: 0, percent: 0 };
|
|
597
|
+
const add = async () => {
|
|
598
|
+
if (!adding.trim() || busy) return;
|
|
599
|
+
setBusy(true);
|
|
600
|
+
setError(null);
|
|
601
|
+
try {
|
|
602
|
+
await onEdit({ target, action: "add", content: adding });
|
|
603
|
+
setAdding("");
|
|
604
|
+
} catch (e) {
|
|
605
|
+
setError(String(e?.message || e));
|
|
606
|
+
} finally {
|
|
607
|
+
setBusy(false);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const b = budgetAfter(data, adding);
|
|
611
|
+
const noun = target === "user" ? "profile entry" : "note";
|
|
612
|
+
|
|
613
|
+
return h("div", { style: { display: "flex", flexDirection: "column", minHeight: 0, flex: 1 } }, [
|
|
614
|
+
h("div", { key: "usage", style: { padding: "2px 16px 10px" } }, [
|
|
615
|
+
h("div", { key: "bar", style: { height: 6, borderRadius: 999, background: "rgba(128,128,128,.2)", overflow: "hidden" } },
|
|
616
|
+
h("div", { className: "dmr-bar-fill", style: { width: Math.min(100, usage.percent) + "%", ...(usage.percent >= 90 ? { background: "#f59e0b" } : {}) } })),
|
|
617
|
+
h("div", { key: "t", style: { display: "flex", justifyContent: "space-between", marginTop: 5, fontSize: 11, color: FAINT, fontVariantNumeric: "tabular-nums" } }, [
|
|
618
|
+
h("span", { key: "a" }, TARGET_HINT[target]),
|
|
619
|
+
h("span", { key: "b", style: { flex: "none", marginLeft: 8 } }, fmt(usage.chars) + " / " + fmt(usage.limit)),
|
|
620
|
+
]),
|
|
621
|
+
]),
|
|
622
|
+
entries.length > 3
|
|
623
|
+
? h("div", { key: "search", style: { padding: "0 16px 8px" } },
|
|
624
|
+
h("input", {
|
|
625
|
+
className: "dmr-input", value: filter, placeholder: "Search " + entries.length + " entries…",
|
|
626
|
+
onChange: (e) => setFilter(e.target.value),
|
|
627
|
+
style: { width: "100%", boxSizing: "border-box", fontFamily: "inherit", fontSize: 12, padding: "6px 10px", borderRadius: 8, border: "1px solid rgba(128,128,128,.3)", background: dark ? "rgba(0,0,0,.2)" : "#fff", color: "inherit" },
|
|
628
|
+
}))
|
|
629
|
+
: null,
|
|
630
|
+
h("div", { key: "list", style: { overflowY: "auto", padding: "0 10px 0 16px", display: "flex", flexDirection: "column", gap: 6, minHeight: 60, flex: 1 } },
|
|
631
|
+
entries.length === 0
|
|
632
|
+
? [h("div", { key: "empty", style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8, padding: "26px 10px", color: FAINT, textAlign: "center" } }, [
|
|
633
|
+
h("span", { key: "i", className: "dmr-glyph-glow", style: { opacity: 0.7, display: "inline-flex" } }, SolidBrainIcon(30)),
|
|
634
|
+
h("div", { key: "t", style: { fontSize: 12.5, fontWeight: 600, color: MUTED } }, "Nothing here yet"),
|
|
635
|
+
h("div", { key: "s", style: { fontSize: 11.5, lineHeight: "16px", maxWidth: 260 } },
|
|
636
|
+
target === "user"
|
|
637
|
+
? "Tell the model about yourself and it saves it here — or add something below."
|
|
638
|
+
: "The model saves what it learns as you work — or add a note below."),
|
|
639
|
+
])]
|
|
640
|
+
: shown.length === 0
|
|
641
|
+
? [h("div", { key: "none", style: { color: FAINT, fontSize: 12, padding: "12px 4px" } }, "No entry matches “" + filter + "”.")]
|
|
642
|
+
: shown.map((entry) => h(EntryCard, {
|
|
643
|
+
key: entry, entry, data, dark,
|
|
644
|
+
onSave: (from, to) => onEdit({ target, action: "replace", old_text: from, content: to }),
|
|
645
|
+
onDelete: (text) => onEdit({ target, action: "remove", old_text: text }),
|
|
646
|
+
}))),
|
|
647
|
+
h("div", { key: "add", style: { padding: "10px 16px 4px", borderTop: "1px solid rgba(128,128,128,.18)", marginTop: 8 } }, [
|
|
648
|
+
h("div", { key: "row", style: { display: "flex", gap: 8, alignItems: "flex-end" } }, [
|
|
649
|
+
h("textarea", {
|
|
650
|
+
key: "t", className: "dmr-input", value: adding, rows: adding.includes("\n") || adding.length > 60 ? 3 : 1,
|
|
651
|
+
placeholder: "Add a " + noun + "…",
|
|
652
|
+
onChange: (e) => setAdding(e.target.value),
|
|
653
|
+
onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (!b.over) add(); } },
|
|
654
|
+
style: { flex: 1, boxSizing: "border-box", resize: "none", fontFamily: "inherit", fontSize: 12.5, lineHeight: "18px", padding: "7px 10px", borderRadius: 10, border: "1px solid rgba(128,128,128,.35)", background: dark ? "rgba(0,0,0,.25)" : "#fff", color: "inherit" },
|
|
655
|
+
}),
|
|
656
|
+
h("button", {
|
|
657
|
+
key: "b", type: "button", disabled: busy || !adding.trim() || b.over, onClick: add,
|
|
658
|
+
style: {
|
|
659
|
+
flex: "none", cursor: busy || !adding.trim() || b.over ? "default" : "pointer", borderRadius: 10, padding: "7px 14px", fontSize: 12, fontWeight: 700, border: "none",
|
|
660
|
+
background: adding.trim() && !b.over ? "linear-gradient(105deg,var(--dmr-from),var(--dmr-to))" : "rgba(128,128,128,.2)",
|
|
661
|
+
color: adding.trim() && !b.over ? "#1b1b1d" : "inherit", opacity: busy ? 0.6 : 1,
|
|
662
|
+
},
|
|
663
|
+
}, "Add"),
|
|
664
|
+
]),
|
|
665
|
+
h("div", { key: "hint", style: { marginTop: 4, minHeight: 15 } },
|
|
666
|
+
adding.trim() ? BudgetLine(b) : h("span", { style: { fontSize: 11, color: FAINT } }, fmt(Math.max(0, usage.limit - usage.chars)) + " characters free · Enter to add, Shift+Enter for a new line")),
|
|
667
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5 } }, error) : null,
|
|
668
|
+
]),
|
|
669
|
+
]);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function MemoryPanel(props) {
|
|
673
|
+
const wide = props?.wide !== false;
|
|
674
|
+
const [open, setOpen] = React.useState(false);
|
|
675
|
+
const [state, setState] = React.useState(null);
|
|
676
|
+
const [error, setError] = React.useState(null);
|
|
677
|
+
const [tab, setTab] = React.useState(null);
|
|
678
|
+
const rootRef = React.useRef(null);
|
|
679
|
+
React.useEffect(() => { injectStyles(); injectPanelStyles(); }, []);
|
|
680
|
+
|
|
681
|
+
const load = React.useCallback(async () => {
|
|
682
|
+
try {
|
|
683
|
+
setState(await api("/state"));
|
|
684
|
+
setError(null);
|
|
685
|
+
} catch (e) {
|
|
686
|
+
setError(String(e?.message || e));
|
|
687
|
+
}
|
|
688
|
+
}, []);
|
|
689
|
+
|
|
690
|
+
// Fresh when opened, and every few seconds while open — the model may
|
|
691
|
+
// be saving in another chat at the same time.
|
|
692
|
+
React.useEffect(() => {
|
|
693
|
+
if (!open) return undefined;
|
|
694
|
+
load();
|
|
695
|
+
const t = setInterval(load, 5000);
|
|
696
|
+
return () => clearInterval(t);
|
|
697
|
+
}, [open, load]);
|
|
698
|
+
// Once at start, for the count on the button.
|
|
699
|
+
React.useEffect(() => { load(); }, [load]);
|
|
700
|
+
|
|
701
|
+
// Escape and a click outside close it.
|
|
702
|
+
React.useEffect(() => {
|
|
703
|
+
if (!open || typeof document === "undefined") return undefined;
|
|
704
|
+
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
705
|
+
const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
|
|
706
|
+
document.addEventListener("keydown", onKey);
|
|
707
|
+
document.addEventListener("mousedown", onDown);
|
|
708
|
+
return () => { document.removeEventListener("keydown", onKey); document.removeEventListener("mousedown", onDown); };
|
|
709
|
+
}, [open]);
|
|
710
|
+
|
|
711
|
+
const edit = async (body) => {
|
|
712
|
+
const out = await api("/entry", { method: "POST", body: JSON.stringify(body) });
|
|
713
|
+
if (out?.state) setState(out.state);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const dark = isDark();
|
|
717
|
+
const counts = state ? { memory: state.memory.entries.length, user: state.user.entries.length } : { memory: 0, user: 0 };
|
|
718
|
+
const total = counts.memory + counts.user;
|
|
719
|
+
const current = tab ?? (counts.memory === 0 && counts.user > 0 ? "user" : "memory");
|
|
720
|
+
|
|
721
|
+
const tabButton = (target, label) => h("button", {
|
|
722
|
+
key: target, type: "button", role: "tab", className: "dmr-tab", "aria-selected": current === target,
|
|
723
|
+
onClick: () => setTab(target),
|
|
724
|
+
}, [
|
|
725
|
+
Ring(state?.[target]?.usage?.percent ?? 0),
|
|
726
|
+
h("span", { key: "l" }, label),
|
|
727
|
+
h("span", { key: "n", style: { fontSize: 11, opacity: 0.6, fontVariantNumeric: "tabular-nums" } }, String(counts[target])),
|
|
728
|
+
]);
|
|
729
|
+
|
|
730
|
+
const panel = open
|
|
731
|
+
? h("div", {
|
|
732
|
+
key: "panel", "data-plugin": "dsh-memory-recall-panel", className: "dmr-pop dmr-panel", role: "dialog", "aria-label": "Memory",
|
|
733
|
+
style: {
|
|
734
|
+
position: "fixed", bottom: 60, left: wide ? 12 : 64, zIndex: 50, width: 440, maxWidth: "calc(100vw - 24px)", height: "min(640px, 78vh)",
|
|
735
|
+
display: "flex", flexDirection: "column", boxSizing: "border-box", fontSize: 12, borderRadius: 16, overflow: "hidden",
|
|
736
|
+
border: "1px solid " + (dark ? "rgba(255,255,255,.12)" : "rgba(0,0,0,.10)"),
|
|
737
|
+
background: dark ? "#19191b" : "#fff", color: dark ? "#ececec" : "#111",
|
|
738
|
+
boxShadow: "0 18px 48px rgba(0,0,0,.35), 0 0 0 1px rgba(158,148,213,.08)",
|
|
739
|
+
},
|
|
740
|
+
}, [
|
|
741
|
+
h("div", { key: "head", style: { display: "flex", alignItems: "center", gap: 10, padding: "14px 14px 10px 16px" } }, [
|
|
742
|
+
h("span", { key: "i", className: "dmr-glyph-glow", style: { display: "inline-flex" } }, SolidBrainIcon(22)),
|
|
743
|
+
h("div", { key: "t", style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 } }, [
|
|
744
|
+
h("span", { key: "a", className: "dmr-gradient-text", style: { fontWeight: 700, fontSize: 16, lineHeight: "20px" } }, "Memory"),
|
|
745
|
+
h("span", { key: "b", style: { fontSize: 11, color: FAINT } }, "Kept in every new chat · changes apply from the next one"),
|
|
746
|
+
]),
|
|
747
|
+
h("button", { key: "x", type: "button", className: "dmr-icon-btn", onClick: () => setOpen(false), "aria-label": "Close", style: { marginLeft: "auto", fontSize: 14 } }, "✕"),
|
|
748
|
+
]),
|
|
749
|
+
h("div", { key: "tabs", role: "tablist", style: { display: "flex", gap: 4, padding: "0 12px 10px" } }, [
|
|
750
|
+
tabButton("memory", "Notes"),
|
|
751
|
+
tabButton("user", "Profile"),
|
|
752
|
+
]),
|
|
753
|
+
error ? h("div", { key: "err", style: { color: "#ef4444", margin: "0 16px 8px" } }, "Could not load memory: " + error) : null,
|
|
754
|
+
state && state.tools && !state.tools.ok
|
|
755
|
+
? h("div", {
|
|
756
|
+
key: "tools",
|
|
757
|
+
style: { color: "#ef4444", background: "rgba(239,68,68,.08)", border: "1px solid rgba(239,68,68,.35)", borderRadius: 8, padding: "6px 8px", margin: "0 16px 10px", lineHeight: "16px" },
|
|
758
|
+
}, "The model's memory tools did not load, so it can read this memory but cannot save to it or search earlier chats. "
|
|
759
|
+
+ "Reason: " + (state.tools.error || "unknown") + ". Restart dsh after fixing; entries added here still work.")
|
|
760
|
+
: null,
|
|
761
|
+
state
|
|
762
|
+
? h(MemoryTab, { key: current, target: current, data: state[current], dark, onEdit: edit })
|
|
763
|
+
: h("div", { key: "loading", style: { padding: 20, color: FAINT } }, "Loading…"),
|
|
764
|
+
state
|
|
765
|
+
? h("div", { key: "dir", style: { padding: "4px 16px 10px", color: FAINT, fontSize: 10, wordBreak: "break-all" } }, "Stored in " + state.dir)
|
|
766
|
+
: null,
|
|
767
|
+
])
|
|
768
|
+
: null;
|
|
769
|
+
|
|
770
|
+
const button = wide
|
|
771
|
+
? h("button", {
|
|
772
|
+
key: "btn", type: "button", className: "dmr-row", onClick: () => setOpen(!open),
|
|
773
|
+
title: "Memory", "aria-label": "Memory", "aria-expanded": open,
|
|
774
|
+
style: {
|
|
775
|
+
// Settings' own row, measured: 42px tall, 2px past the row each side.
|
|
776
|
+
width: "calc(100% + 4px)", margin: "0 -2px", minWidth: 0, height: 42, boxSizing: "border-box",
|
|
777
|
+
color: "var(--dsw-alias-label-primary, inherit)", cursor: "pointer",
|
|
778
|
+
background: open ? "var(--dsw-alias-interactive-bg-hover-solid, rgba(128,128,128,.12))" : "0 0",
|
|
779
|
+
border: "none", borderRadius: 12, alignItems: "center", gap: 8, padding: "0 10px 0 8px",
|
|
780
|
+
fontFamily: "inherit", fontSize: 14, display: "inline-flex", overflow: "hidden",
|
|
781
|
+
},
|
|
782
|
+
}, [
|
|
783
|
+
h("span", { key: "i", className: total ? "dmr-glyph-glow" : undefined, style: { display: "inline-flex", alignItems: "center", flex: "none" } }, SolidBrainIcon(16)),
|
|
784
|
+
h("span", { key: "l", style: { textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0, overflow: "hidden" } }, "Memory"),
|
|
785
|
+
total ? h("span", {
|
|
786
|
+
key: "n",
|
|
787
|
+
style: {
|
|
788
|
+
marginLeft: "auto", minWidth: 18, height: 18, padding: "0 5px", borderRadius: 999, boxSizing: "border-box",
|
|
789
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 10.5, fontWeight: 700,
|
|
790
|
+
background: "linear-gradient(105deg,#e2b56d,#a58fd9)", color: "#1b1b1d", fontVariantNumeric: "tabular-nums",
|
|
791
|
+
},
|
|
792
|
+
}, String(total)) : null,
|
|
793
|
+
])
|
|
794
|
+
: h("button", {
|
|
795
|
+
key: "btn", type: "button", className: "dmr-icon-btn", onClick: () => setOpen(!open),
|
|
796
|
+
title: total ? "Memory (" + total + ")" : "Memory", "aria-label": "Memory", "aria-expanded": open,
|
|
797
|
+
style: { width: 36, height: 36, borderRadius: 10, opacity: 1, color: "var(--dsw-alias-label-primary, inherit)" },
|
|
798
|
+
}, h("span", { className: total ? "dmr-glyph-glow" : undefined, style: { display: "inline-flex" } }, SolidBrainIcon(16)));
|
|
799
|
+
|
|
800
|
+
return h("div", {
|
|
801
|
+
ref: rootRef, "data-plugin": "dsh-memory-recall-sidebar", className: "dmr-panel",
|
|
802
|
+
style: wide
|
|
803
|
+
? { flex: "0 0 100%", minWidth: 0, alignItems: "center", height: 50, margin: 0, display: "flex", position: "relative" }
|
|
804
|
+
: { display: "flex", justifyContent: "center", alignItems: "center", position: "relative", width: "100%", height: 50 },
|
|
805
|
+
}, [button, panel]);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* ---- @ chats, grouped by workspace --------------------------------------- */
|
|
809
|
+
//
|
|
810
|
+
// dsh's own @ list shows at most 50 sessions and includes sub-agent
|
|
811
|
+
// sessions (untitled, shown as raw ids), so real chats fall off it. This
|
|
812
|
+
// source lists every one of the user's own chats, under the workspace
|
|
813
|
+
// titles the sidebar uses, and every chat with a readable name. Its rows
|
|
814
|
+
// replace dsh's session rows in the menu; dsh's file rows stay.
|
|
815
|
+
//
|
|
816
|
+
// Picking a chat inserts dsh's own mention, so dsh attaches that chat
|
|
817
|
+
// exactly as it does for its native rows. Picking a workspace inserts a
|
|
818
|
+
// chip that reaches the model as [workspace "Name" — folder], which
|
|
819
|
+
// session_search takes as its `workspace`.
|
|
820
|
+
|
|
821
|
+
const SOURCE = "dsh-memory-recall";
|
|
822
|
+
const DRILL = /^ws:([0-9a-zA-Z-]+)\/(.*)$/;
|
|
823
|
+
const WS_REF = "dmr-workspace:";
|
|
824
|
+
const RECENT_HERE = 6;
|
|
825
|
+
const PER_GROUP = 8;
|
|
826
|
+
|
|
827
|
+
/** base64url of the UTF-8 bytes, as Node's Buffer "base64url" writes it. */
|
|
828
|
+
function base64url(text) {
|
|
829
|
+
const bytes = new TextEncoder().encode(text);
|
|
830
|
+
let binary = "";
|
|
831
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
832
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** dsh-session-reference's canonical mention: @[label](dsh-session:<base64url(JSON(id))>). */
|
|
836
|
+
function sessionMention(sessionId, label) {
|
|
837
|
+
const escaped = String(label).replace(/[\\\]]/g, (m) => "\\" + m);
|
|
838
|
+
return "@[" + escaped + "](dsh-session:" + base64url(JSON.stringify(sessionId)) + ")";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function workspaceRef(ws) {
|
|
842
|
+
return WS_REF + base64url(JSON.stringify({ title: ws.title, path: ws.path }));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function workspaceText(ref) {
|
|
846
|
+
try {
|
|
847
|
+
const b64 = ref.slice(WS_REF.length).replace(/-/g, "+").replace(/_/g, "/");
|
|
848
|
+
const bytes = Uint8Array.from(atob(b64 + "===".slice((b64.length + 3) % 4)), (c) => c.charCodeAt(0));
|
|
849
|
+
const ws = JSON.parse(new TextDecoder().decode(bytes));
|
|
850
|
+
return "[workspace \"" + ws.title + "\" — " + ws.path + "]";
|
|
851
|
+
} catch {
|
|
852
|
+
return ref;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function ago(ms, now) {
|
|
857
|
+
const s = Math.max(0, (now - ms) / 1000);
|
|
858
|
+
if (s < 60) return "now";
|
|
859
|
+
if (s < 3600) return Math.floor(s / 60) + "m";
|
|
860
|
+
if (s < 86400) return Math.floor(s / 3600) + "h";
|
|
861
|
+
if (s < 86400 * 30) return Math.floor(s / 86400) + "d";
|
|
862
|
+
if (s < 86400 * 365) return Math.floor(s / (86400 * 30)) + "mo";
|
|
863
|
+
return Math.floor(s / (86400 * 365)) + "y";
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** A folder with the home directory shortened, for a workspace row. */
|
|
867
|
+
function shortPath(path) {
|
|
868
|
+
return String(path || "").replace(/^[A-Za-z]:\\Users\\[^\\]+/, "~").replace(/^\/(?:home|Users)\/[^/]+/, "~");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function createChatSource(ctx) {
|
|
872
|
+
const cache = new Map(); // chat id → {at, data}
|
|
873
|
+
let last = null; // the latest data, for the synchronous breadcrumb
|
|
874
|
+
|
|
875
|
+
async function load(sessionId, signal) {
|
|
876
|
+
const hit = cache.get(sessionId);
|
|
877
|
+
if (hit && Date.now() - hit.at < 10_000) return hit.data;
|
|
878
|
+
const res = await fetch(BASE + "/chats?session=" + encodeURIComponent(sessionId || ""), { signal, headers: { accept: "application/json" } });
|
|
879
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
880
|
+
const data = await res.json();
|
|
881
|
+
cache.set(sessionId, { at: Date.now(), data });
|
|
882
|
+
last = data;
|
|
883
|
+
return data;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function updatedAtOf(chat) {
|
|
887
|
+
try {
|
|
888
|
+
return ctx.get?.("sessions")?.list?.getSnapshot?.()?.byId?.[chat.sessionId]?.updatedAt ?? chat.createdAt;
|
|
889
|
+
} catch {
|
|
890
|
+
return chat.createdAt;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const chatRow = (chat, section, now) => {
|
|
895
|
+
const label = chat.title || "Untitled chat";
|
|
896
|
+
return {
|
|
897
|
+
name: label,
|
|
898
|
+
description: ago(updatedAtOf(chat), now),
|
|
899
|
+
icon: "session",
|
|
900
|
+
section,
|
|
901
|
+
value: JSON.stringify({ kind: "chat", id: chat.sessionId, label }),
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
const workspaceRow = (ws, section) => ({
|
|
905
|
+
name: ws.title,
|
|
906
|
+
description: ws.chats.length + (ws.chats.length === 1 ? " chat" : " chats") + " · " + shortPath(ws.path),
|
|
907
|
+
icon: "folder",
|
|
908
|
+
section,
|
|
909
|
+
drill: true,
|
|
910
|
+
value: JSON.stringify({ kind: "workspace", id: ws.id, title: ws.title, path: ws.path }),
|
|
911
|
+
});
|
|
912
|
+
const matches = (text, q) => String(text || "").toLowerCase().includes(q);
|
|
913
|
+
|
|
914
|
+
function listing(data, query, now) {
|
|
915
|
+
const current = data.workspaces.find((w) => w.id === data.currentWorkspaceId) ?? null;
|
|
916
|
+
const others = data.workspaces.filter((w) => w !== current);
|
|
917
|
+
const drilled = DRILL.exec(query);
|
|
918
|
+
if (drilled) {
|
|
919
|
+
const ws = data.workspaces.find((w) => w.id.startsWith(drilled[1]));
|
|
920
|
+
if (!ws) return [];
|
|
921
|
+
const q = drilled[2].trim().toLowerCase();
|
|
922
|
+
return ws.chats.filter((c) => !q || matches(c.title, q)).map((c) => chatRow(c, ws.title, now));
|
|
923
|
+
}
|
|
924
|
+
const q = query.trim().toLowerCase();
|
|
925
|
+
if (!q) {
|
|
926
|
+
const rows = [];
|
|
927
|
+
if (current) {
|
|
928
|
+
rows.push(workspaceRow(current, "This workspace"));
|
|
929
|
+
rows.push(...current.chats.slice(0, RECENT_HERE).map((c) => chatRow(c, "This workspace", now)));
|
|
930
|
+
}
|
|
931
|
+
rows.push(...others.map((w) => workspaceRow(w, "Workspaces")));
|
|
932
|
+
if (data.ungrouped.length) rows.push(...data.ungrouped.slice(0, PER_GROUP).map((c) => chatRow(c, "No workspace", now)));
|
|
933
|
+
return rows;
|
|
934
|
+
}
|
|
935
|
+
const rows = data.workspaces.filter((w) => matches(w.title, q) || matches(w.path, q)).map((w) => workspaceRow(w, "Workspaces"));
|
|
936
|
+
for (const ws of current ? [current, ...others] : others) {
|
|
937
|
+
const hits = ws.chats.filter((c) => matches(c.title, q));
|
|
938
|
+
rows.push(...hits.slice(0, PER_GROUP).map((c) => chatRow(c, ws.title, now)));
|
|
939
|
+
}
|
|
940
|
+
rows.push(...data.ungrouped.filter((c) => matches(c.title, q)).slice(0, PER_GROUP).map((c) => chatRow(c, "No workspace", now)));
|
|
941
|
+
return rows;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
trigger: "@",
|
|
946
|
+
name: SOURCE,
|
|
947
|
+
order: 10,
|
|
948
|
+
showGroupTitle: false,
|
|
949
|
+
warm(session) {
|
|
950
|
+
load(session.sessionId).catch(() => {});
|
|
951
|
+
},
|
|
952
|
+
async candidates(session, req) {
|
|
953
|
+
if (req.quoted === true) return []; // a quoted @"path" is a file
|
|
954
|
+
let data;
|
|
955
|
+
try {
|
|
956
|
+
data = await load(session.sessionId, req.signal);
|
|
957
|
+
} catch {
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
if (req.signal?.aborted) return [];
|
|
961
|
+
return listing(data, String(req.query || ""), Date.now());
|
|
962
|
+
},
|
|
963
|
+
header(_session, req) {
|
|
964
|
+
const drilled = DRILL.exec(String(req.query || ""));
|
|
965
|
+
if (!drilled || !last) return undefined;
|
|
966
|
+
const ws = last.workspaces.find((w) => w.id.startsWith(drilled[1]));
|
|
967
|
+
if (!ws) return undefined;
|
|
968
|
+
return [
|
|
969
|
+
{ label: "All workspaces", value: JSON.stringify({ kind: "root" }) },
|
|
970
|
+
{ label: ws.title, value: JSON.stringify({ kind: "workspace", id: ws.id, title: ws.title, path: ws.path }), current: true },
|
|
971
|
+
];
|
|
972
|
+
},
|
|
973
|
+
onPick({ candidate, action }) {
|
|
974
|
+
let v;
|
|
975
|
+
try {
|
|
976
|
+
v = JSON.parse(candidate.value);
|
|
977
|
+
} catch {
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
if (v.kind === "root") return { text: "@", continue: true };
|
|
981
|
+
if (v.kind === "workspace" && action === "drill") return { text: "@ws:" + v.id.slice(0, 8) + "/", continue: true };
|
|
982
|
+
if (v.kind === "workspace") {
|
|
983
|
+
const ref = workspaceRef(v);
|
|
984
|
+
return { insert: { source: SOURCE, ref, label: v.title, appearance: "folder", clipboardText: workspaceText(ref) } };
|
|
985
|
+
}
|
|
986
|
+
if (v.kind === "chat") {
|
|
987
|
+
const mention = sessionMention(v.id, v.label);
|
|
988
|
+
return { insert: { source: SOURCE, ref: mention, label: v.label, appearance: "session", clipboardText: mention } };
|
|
989
|
+
}
|
|
990
|
+
return undefined;
|
|
991
|
+
},
|
|
992
|
+
codec: {
|
|
993
|
+
clipboardText: (ref) => (ref.startsWith(WS_REF) ? workspaceText(ref) : ref),
|
|
994
|
+
serialize: (ref) => Promise.resolve(ref.startsWith(WS_REF) ? workspaceText(ref) : ref),
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Drop the session rows from dsh's own @ source, keeping its file rows —
|
|
1001
|
+
* this plugin's grouped chats replace them. The source registry offers no
|
|
1002
|
+
* way to disable one group, so the native source's `candidates` is wrapped,
|
|
1003
|
+
* and restored when this plugin unloads. If the native source is not
|
|
1004
|
+
* found (renamed, or not mounted), nothing is touched and both lists show.
|
|
1005
|
+
* @returns a disposer, or null when there was nothing to wrap yet.
|
|
1006
|
+
*/
|
|
1007
|
+
function hideNativeChats(inputTriggers) {
|
|
1008
|
+
const sources = inputTriggers?.live?.sources;
|
|
1009
|
+
const native = Array.isArray(sources) ? sources.find((s) => s && s.trigger === "@" && s.name === "reference") : undefined;
|
|
1010
|
+
if (!native || typeof native.candidates !== "function") return null;
|
|
1011
|
+
if (native.__dmrOriginal) return () => {};
|
|
1012
|
+
const original = native.candidates;
|
|
1013
|
+
native.__dmrOriginal = original;
|
|
1014
|
+
native.candidates = async function (...args) {
|
|
1015
|
+
const rows = await original.apply(this, args);
|
|
1016
|
+
return (rows || []).filter((row) => {
|
|
1017
|
+
try {
|
|
1018
|
+
return JSON.parse(row.value).kind !== "session";
|
|
1019
|
+
} catch {
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
};
|
|
1024
|
+
return () => {
|
|
1025
|
+
if (native.__dmrOriginal === original) {
|
|
1026
|
+
native.candidates = original;
|
|
1027
|
+
delete native.__dmrOriginal;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* ---- mounting ----------------------------------------------------------- */
|
|
1033
|
+
|
|
1034
|
+
const inject = ["slots"];
|
|
1035
|
+
|
|
1036
|
+
function apply(ctx) {
|
|
1037
|
+
const openChat = (sessionId) => {
|
|
1038
|
+
try {
|
|
1039
|
+
const sessions = ctx.get?.("sessions");
|
|
1040
|
+
if (sessions && typeof sessions.open === "function" && sessionId) sessions.open(sessionId);
|
|
1041
|
+
} catch { /* an unknown chat: nothing to open */ }
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// tool.call.toolview is a CHILD slot: it must go through slots.inject, or
|
|
1045
|
+
// registering before the parent's children table commits throws and takes
|
|
1046
|
+
// the whole plugin down. Keyed by tool name, each replaces dsh's own row.
|
|
1047
|
+
ctx.slots.inject("tool.call.toolview", () =>
|
|
1048
|
+
ctx.slots.register({ name: "tool.call.toolview", key: "memory" }, MemoryCall));
|
|
1049
|
+
const RecallCall = makeRecallCall(openChat);
|
|
1050
|
+
ctx.slots.inject("tool.call.toolview", () =>
|
|
1051
|
+
ctx.slots.register({ name: "tool.call.toolview", key: "session_search" }, RecallCall));
|
|
1052
|
+
|
|
1053
|
+
// sidebar.footer.action is a LIST slot: id + order. Scheduled is 20;
|
|
1054
|
+
// Memory sits just above it. The slot passes {wide}: false when the
|
|
1055
|
+
// sidebar is collapsed to its icon rail.
|
|
1056
|
+
ctx.slots.inject("sidebar.footer.action", () =>
|
|
1057
|
+
ctx.slots.register({ name: "sidebar.footer.action", id: "dsh-memory-recall", order: 15 }, (slotProps) => h(MemoryPanel, slotProps || {})));
|
|
1058
|
+
|
|
1059
|
+
// The @ source. Injected rather than required, so a composition without
|
|
1060
|
+
// the @ menu still loads the rest of the plugin.
|
|
1061
|
+
if (typeof ctx.inject === "function") {
|
|
1062
|
+
ctx.inject(["inputTriggers"], (scope) => {
|
|
1063
|
+
const triggers = scope.inputTriggers;
|
|
1064
|
+
const source = createChatSource(ctx);
|
|
1065
|
+
const warm = source.warm;
|
|
1066
|
+
let restore = hideNativeChats(triggers);
|
|
1067
|
+
// dsh's own source may register after this one: retry on first use.
|
|
1068
|
+
source.warm = (session) => {
|
|
1069
|
+
if (!restore) restore = hideNativeChats(triggers);
|
|
1070
|
+
warm(session);
|
|
1071
|
+
};
|
|
1072
|
+
scope.effect(() => triggers.registerSource(source), "dsh-memory-recall: @ chats by workspace");
|
|
1073
|
+
scope.effect(() => () => { if (restore) restore(); }, "dsh-memory-recall: restore dsh's @ chats");
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
exports.apply = apply;
|
|
1079
|
+
exports.inject = inject;
|
|
1080
|
+
exports.memoryStatus = memoryStatus;
|
|
1081
|
+
exports.changesOf = changesOf;
|
|
1082
|
+
exports.usageIn = usageIn;
|
|
1083
|
+
exports.recallStatus = recallStatus;
|
|
1084
|
+
exports.createChatSource = createChatSource;
|
|
1085
|
+
exports.sessionMention = sessionMention;
|
|
1086
|
+
exports.hideNativeChats = hideNativeChats;
|
|
1087
|
+
exports.workspaceText = workspaceText;
|
|
1088
|
+
return module.exports;
|
|
1089
|
+
},
|
|
1090
|
+
});
|