dsh-memory-recall 0.1.0 → 0.2.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/README.md +14 -9
- package/dsh.plugin.json +2 -2
- package/lib/client.js +1490 -1090
- package/package.json +2 -2
- package/src/index.js +185 -156
- package/src/inject.js +87 -0
- package/src/memory-tool.js +167 -136
- package/src/routes.js +205 -153
- package/src/snapshot.js +71 -63
package/lib/client.js
CHANGED
|
@@ -1,1090 +1,1490 @@
|
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
style: {
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
const [
|
|
591
|
-
const [
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
+
/** A small on/off switch, the knob sliding over the memory gradient when on. */
|
|
134
|
+
function Switch(key, on, onChange, label) {
|
|
135
|
+
return h("button", {
|
|
136
|
+
key: "sw-" + key, type: "button", role: "switch", "aria-checked": on, "aria-label": label, "data-switch": key,
|
|
137
|
+
onClick: () => onChange(!on),
|
|
138
|
+
style: {
|
|
139
|
+
flex: "none", position: "relative", width: 34, height: 19, borderRadius: 999, border: "none", padding: 0, cursor: "pointer",
|
|
140
|
+
background: on ? "linear-gradient(105deg,#c08532,#9e94d5)" : "rgba(128,128,128,.35)", transition: "background .2s",
|
|
141
|
+
},
|
|
142
|
+
}, h("span", {
|
|
143
|
+
style: {
|
|
144
|
+
position: "absolute", top: 2, left: on ? 17 : 2, width: 15, height: 15, borderRadius: 999, background: "#fff",
|
|
145
|
+
boxShadow: "0 1px 3px rgba(0,0,0,.3)", transition: "left .18s",
|
|
146
|
+
},
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function GearIcon(size) {
|
|
151
|
+
return h("svg", {
|
|
152
|
+
width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2,
|
|
153
|
+
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
154
|
+
}, [
|
|
155
|
+
h("circle", { key: "c", cx: 12, cy: 12, r: 3 }),
|
|
156
|
+
h("path", { key: "p", d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" }),
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function Chevron(open) {
|
|
161
|
+
return h("span", { style: { fontSize: 10, opacity: 0.6, marginLeft: 2 } }, open ? "▾" : "▸");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** "**x**" from a snippet becomes bold; nothing else is interpreted. */
|
|
165
|
+
function withBold(text) {
|
|
166
|
+
return String(text ?? "").split("**").map((part, i) => (i % 2 ? h("b", { key: i }, part) : part));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function UsageBar(usage, width) {
|
|
170
|
+
if (!usage || !usage.limit) return null;
|
|
171
|
+
const pct = Math.max(0, Math.min(100, usage.percent));
|
|
172
|
+
const colour = pct >= 90 ? "#f59e0b" : ACCENT;
|
|
173
|
+
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 11, color: FAINT } }, [
|
|
174
|
+
h("div", { key: "bar", style: { width: width || 120, height: 4, borderRadius: 999, background: "rgba(128,128,128,.25)", overflow: "hidden" } },
|
|
175
|
+
h("div", { style: { width: pct + "%", height: "100%", background: colour, transition: "width .3s" } })),
|
|
176
|
+
h("span", { key: "t", style: { fontVariantNumeric: "tabular-nums" } },
|
|
177
|
+
pct + "% · " + Number(usage.chars).toLocaleString("en-US") + "/" + Number(usage.limit).toLocaleString("en-US")),
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ---- display settings ------------------------------------------------------ */
|
|
182
|
+
//
|
|
183
|
+
// What the plugin adds to dsh's own UI can be switched off from the Memory
|
|
184
|
+
// panel. Per browser (localStorage), applied at once through a window event,
|
|
185
|
+
// so nothing needs a reload. Memory itself keeps working either way.
|
|
186
|
+
|
|
187
|
+
const UI_KEY = "dsh-memory-recall:ui";
|
|
188
|
+
const UI_EVENT = "dsh-memory-recall:ui";
|
|
189
|
+
const UI_DEFAULTS = { composerButton: true, atMenu: true };
|
|
190
|
+
const UI_SWITCHES = [
|
|
191
|
+
{ key: "composerButton", label: "Brain button in the message box", note: "Shows whether this chat has memory, and when it got it." },
|
|
192
|
+
{ key: "atMenu", label: "Chats & workspaces in @", note: "Every chat, grouped by workspace. Off: dsh\u2019s own @ chat list." },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
function readUi() {
|
|
196
|
+
try {
|
|
197
|
+
const raw = window.localStorage && window.localStorage.getItem(UI_KEY);
|
|
198
|
+
const saved = raw ? JSON.parse(raw) : {};
|
|
199
|
+
const out = { ...UI_DEFAULTS };
|
|
200
|
+
for (const k of Object.keys(UI_DEFAULTS)) if (typeof saved?.[k] === "boolean") out[k] = saved[k];
|
|
201
|
+
return out;
|
|
202
|
+
} catch {
|
|
203
|
+
return { ...UI_DEFAULTS };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function writeUi(patch) {
|
|
208
|
+
const next = { ...readUi(), ...patch };
|
|
209
|
+
try { if (window.localStorage) window.localStorage.setItem(UI_KEY, JSON.stringify(next)); } catch { /* private window: this tab only */ }
|
|
210
|
+
try { if (typeof window.dispatchEvent === "function") window.dispatchEvent(new Event(UI_EVENT)); } catch { /* no events: next render */ }
|
|
211
|
+
return next;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The current display settings, re-read when they change (here or in another tab). */
|
|
215
|
+
function useUi() {
|
|
216
|
+
const [ui, setUi] = React.useState(readUi);
|
|
217
|
+
React.useEffect(() => {
|
|
218
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
|
|
219
|
+
const onChange = () => setUi(readUi());
|
|
220
|
+
window.addEventListener(UI_EVENT, onChange);
|
|
221
|
+
window.addEventListener("storage", onChange);
|
|
222
|
+
return () => { window.removeEventListener(UI_EVENT, onChange); window.removeEventListener("storage", onChange); };
|
|
223
|
+
}, []);
|
|
224
|
+
return ui;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ---- reading a tool call ---------------------------------------------- */
|
|
228
|
+
|
|
229
|
+
const isSettled = (block) => block !== undefined && block !== null && "kind" in block;
|
|
230
|
+
|
|
231
|
+
function argsOf(block) {
|
|
232
|
+
if (!block) return {};
|
|
233
|
+
const raw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
|
|
234
|
+
try {
|
|
235
|
+
const v = JSON.parse(raw);
|
|
236
|
+
return v && typeof v === "object" ? v : {};
|
|
237
|
+
} catch {
|
|
238
|
+
return {}; // still streaming, or not JSON
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function textOf(block) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
for (const c of (Array.isArray(block?.content) ? block.content : [])) {
|
|
245
|
+
if (typeof c?.text === "string") parts.push(c.text);
|
|
246
|
+
else if (Array.isArray(c?.content)) for (const inner of c.content) if (typeof inner?.text === "string") parts.push(inner.text);
|
|
247
|
+
}
|
|
248
|
+
return parts.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function usageIn(text) {
|
|
252
|
+
const m = /(\d+)% — ([\d,]+)\/([\d,]+) chars/.exec(text || "");
|
|
253
|
+
if (!m) return null;
|
|
254
|
+
const n = (s) => Number(s.replace(/,/g, ""));
|
|
255
|
+
return { percent: Number(m[1]), chars: n(m[2]), limit: n(m[3]) };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The changes a memory call asked for, as rows: + added, ~ replaced, − removed. */
|
|
259
|
+
function changesOf(args) {
|
|
260
|
+
const ops = Array.isArray(args.operations) && args.operations.length ? args.operations
|
|
261
|
+
: args.action ? [{ action: args.action, content: args.content, old_text: args.old_text }] : [];
|
|
262
|
+
return ops.filter((op) => op && op.action !== "read").map((op) => {
|
|
263
|
+
const content = op.content ?? op.new_text;
|
|
264
|
+
if (op.action === "add") return { sign: "+", text: content };
|
|
265
|
+
if (op.action === "remove") return { sign: "−", text: op.old_text };
|
|
266
|
+
if (op.action === "replace") return { sign: "~", text: content, from: op.old_text };
|
|
267
|
+
return { sign: "?", text: JSON.stringify(op) };
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ---- the "Saved to memory" chip --------------------------------------- */
|
|
272
|
+
|
|
273
|
+
function memoryStatus(block, args) {
|
|
274
|
+
if (args && args.action === "read") {
|
|
275
|
+
if (!isSettled(block)) return { label: "Reading memory…", action: "Reading", rest: " memory", tone: "busy" };
|
|
276
|
+
return /^Read memory/.test(textOf(block)) && !block.isError ? { label: "Read memory", tone: "ok" } : { label: "Could not read memory", tone: "bad" };
|
|
277
|
+
}
|
|
278
|
+
if (!isSettled(block)) return { label: "Saving to memory…", action: "Saving", rest: " to memory", tone: "busy" };
|
|
279
|
+
const text = textOf(block);
|
|
280
|
+
const ops = changesOf(args).map((c) => c.sign);
|
|
281
|
+
// A successful call, even one that changed nothing, lands "legendary" — as in Hermes.
|
|
282
|
+
if (/^Already in memory/.test(text)) return { label: "Already in memory", tone: "ok" };
|
|
283
|
+
if (/^Blocked/.test(text)) return { label: "Memory write blocked", tone: "bad" };
|
|
284
|
+
if (/^Not saved/.test(text)) {
|
|
285
|
+
return /over its budget/.test(text) ? { label: "Memory full — making room", tone: "warn" } : { label: "Not saved to memory", tone: "bad" };
|
|
286
|
+
}
|
|
287
|
+
if (block.isError) return { label: "Not saved to memory", tone: "bad" };
|
|
288
|
+
if (ops.length && ops.every((s) => s === "−")) return { label: "Removed from memory", tone: "ok" };
|
|
289
|
+
if (ops.length && ops.every((s) => s === "+")) return { label: "Saved to memory", tone: "ok" };
|
|
290
|
+
return { label: "Updated memory", tone: "ok" };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const TONE = { ok: ACCENT, quiet: MUTED, busy: MUTED, warn: "#f59e0b", bad: "#ef4444" };
|
|
294
|
+
|
|
295
|
+
function MemoryCall(props) {
|
|
296
|
+
const { block } = props;
|
|
297
|
+
const [open, setOpen] = React.useState(false);
|
|
298
|
+
// Drawn while still saving? Then this chip watched the save land, and
|
|
299
|
+
// gets the one bloom. A chip from history just shows the settled look.
|
|
300
|
+
const [watchedLand] = React.useState(() => !isSettled(block));
|
|
301
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
302
|
+
const args = argsOf(block);
|
|
303
|
+
const status = memoryStatus(block, args);
|
|
304
|
+
const legendary = status.tone === "ok";
|
|
305
|
+
const text = isSettled(block) ? textOf(block) : "";
|
|
306
|
+
const usage = usageIn(text);
|
|
307
|
+
const changes = changesOf(args);
|
|
308
|
+
// A read names what it loaded, from its result's first line.
|
|
309
|
+
const readCounts = args.action === "read" ? /^Read memory: (\d+) profile entr(?:y|ies), (\d+) notes?/.exec(text) : null;
|
|
310
|
+
const target = args.action === "read"
|
|
311
|
+
? (readCounts ? readCounts[1] + " profile · " + readCounts[2] + " notes" : null)
|
|
312
|
+
: args.target === "user" ? "user profile" : args.target === "memory" ? "notes" : null;
|
|
313
|
+
// What a read loaded: its entry lines, shown when the card is opened.
|
|
314
|
+
const readLines = args.action === "read" && text
|
|
315
|
+
? text.split("\n").filter((l) => /^(USER PROFILE|NOTES):|^\d+\. /.test(l))
|
|
316
|
+
: [];
|
|
317
|
+
|
|
318
|
+
const glyphClass = legendary ? "dmr-legendary-glyph" + (watchedLand ? " dmr-land" : "")
|
|
319
|
+
: status.tone === "busy" ? "dmr-pulse" : undefined;
|
|
320
|
+
const title = status.tone === "busy"
|
|
321
|
+
? h("span", { key: "l", style: { color: FAINT } }, [h("span", { key: "a", className: "dmr-shimmer" }, status.action), status.rest])
|
|
322
|
+
: h("span", {
|
|
323
|
+
key: "l",
|
|
324
|
+
className: legendary ? "dmr-legendary-title" : undefined,
|
|
325
|
+
style: legendary ? undefined : { color: TONE[status.tone] },
|
|
326
|
+
}, status.label);
|
|
327
|
+
|
|
328
|
+
return h("div", { "data-plugin": "dsh-memory-recall-chip", className: "dmr-chip", style: { margin: "2px 0", fontSize: 13 } }, [
|
|
329
|
+
h("button", {
|
|
330
|
+
key: "head", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open,
|
|
331
|
+
style: { display: "inline-flex", alignItems: "center", gap: 7, color: MUTED, padding: "2px 0" },
|
|
332
|
+
}, [
|
|
333
|
+
h("span", {
|
|
334
|
+
key: "i", className: glyphClass,
|
|
335
|
+
style: { display: "inline-flex", color: legendary ? undefined : TONE[status.tone] },
|
|
336
|
+
}, SolidBrainIcon(14)),
|
|
337
|
+
title,
|
|
338
|
+
target ? h("span", { key: "t", className: legendary ? "dmr-legendary-meta" : undefined, style: legendary ? { fontSize: 12 } : { color: FAINT, fontSize: 12 } }, "· " + target) : null,
|
|
339
|
+
Chevron(open),
|
|
340
|
+
]),
|
|
341
|
+
open
|
|
342
|
+
? h("div", {
|
|
343
|
+
key: "body", className: "dmr-pop",
|
|
344
|
+
style: { margin: "4px 0 6px 22px", display: "flex", flexDirection: "column", gap: 4, fontSize: 12 },
|
|
345
|
+
}, [
|
|
346
|
+
...changes.map((c, i) => h("div", { key: "c" + i, style: { display: "flex", gap: 6, alignItems: "baseline" } }, [
|
|
347
|
+
h("span", { key: "s", style: { color: c.sign === "−" ? "#ef4444" : c.sign === "+" ? "#22c55e" : ACCENT, fontWeight: 700, width: 10, flex: "none" } }, c.sign),
|
|
348
|
+
h("span", { key: "x", style: { whiteSpace: "pre-wrap", wordBreak: "break-word" } },
|
|
349
|
+
c.from ? [h("span", { key: "f", style: { color: FAINT, textDecoration: "line-through" } }, c.from), " → ", c.text] : c.text),
|
|
350
|
+
])),
|
|
351
|
+
usage ? h("div", { key: "u", style: { marginTop: 2 } }, UsageBar(usage)) : null,
|
|
352
|
+
...readLines.map((l, i) => /^\d+\. /.test(l)
|
|
353
|
+
? h("div", { key: "r" + i, style: { whiteSpace: "pre-wrap", wordBreak: "break-word", paddingLeft: 10 } }, l)
|
|
354
|
+
: h("div", { key: "r" + i, style: { color: FAINT, fontSize: 10.5, fontWeight: 700, letterSpacing: ".4px", marginTop: i ? 6 : 0 } }, l.replace(/:.*/, ""))),
|
|
355
|
+
isSettled(block) && !/^Saved|^Already|^Read memory/.test(text)
|
|
356
|
+
? h("div", { key: "why", style: { color: FAINT, whiteSpace: "pre-wrap" } }, text.split("\n\n")[0])
|
|
357
|
+
: null,
|
|
358
|
+
])
|
|
359
|
+
: null,
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ---- the recall card --------------------------------------------------- */
|
|
364
|
+
|
|
365
|
+
const DONE = new Set(["done", "declined", "error"]);
|
|
366
|
+
|
|
367
|
+
function useRecallRecord(callId, settled) {
|
|
368
|
+
const [record, setRecord] = React.useState(null);
|
|
369
|
+
React.useEffect(() => {
|
|
370
|
+
if (!callId) return undefined;
|
|
371
|
+
let alive = true;
|
|
372
|
+
let timer = null;
|
|
373
|
+
const pull = async () => {
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch(BASE + "/recall?callId=" + encodeURIComponent(callId), { headers: { accept: "application/json" } });
|
|
376
|
+
const body = res.ok ? await res.json() : null;
|
|
377
|
+
if (!alive) return;
|
|
378
|
+
if (body) setRecord(body);
|
|
379
|
+
if (settled && (!body || DONE.has(body.status))) return; // final read done
|
|
380
|
+
} catch { /* keep polling */ }
|
|
381
|
+
if (alive && !settled) timer = setTimeout(pull, 800);
|
|
382
|
+
};
|
|
383
|
+
pull();
|
|
384
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
385
|
+
}, [callId, settled]);
|
|
386
|
+
return record;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function recallStatus(record, block) {
|
|
390
|
+
const settled = isSettled(block);
|
|
391
|
+
const text = settled ? textOf(block) : "";
|
|
392
|
+
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");
|
|
393
|
+
switch (status) {
|
|
394
|
+
case "awaiting-approval": return { label: "Waiting for your OK to read past chats", tone: "warn", busy: true };
|
|
395
|
+
case "starting":
|
|
396
|
+
case "searching": return { label: "Searching past chats…", tone: "busy", busy: true };
|
|
397
|
+
case "declined": return { label: "Not allowed — nothing was read", tone: "quiet" };
|
|
398
|
+
case "error": return { label: "Recall failed", tone: "bad" };
|
|
399
|
+
default: return { label: "Recalled from past chats", tone: "ok" };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function describeAsk(args) {
|
|
404
|
+
if (typeof args.query === "string" && args.query.trim()) return "“" + args.query.trim() + "”";
|
|
405
|
+
if (args.session_id) return "one earlier chat";
|
|
406
|
+
return "recent chats";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const dayOf = (ms) => (Number.isFinite(ms) ? new Date(ms).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" }) : "");
|
|
410
|
+
|
|
411
|
+
function makeRecallCall(openChat) {
|
|
412
|
+
return function RecallCall(props) {
|
|
413
|
+
const { callId, block } = props;
|
|
414
|
+
const settled = isSettled(block);
|
|
415
|
+
const record = useRecallRecord(callId, settled);
|
|
416
|
+
const [open, setOpen] = React.useState(true);
|
|
417
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
418
|
+
const args = argsOf(block);
|
|
419
|
+
const status = recallStatus(record, block);
|
|
420
|
+
const summary = record?.summary ?? null;
|
|
421
|
+
const results = summary?.mode === "discover" ? summary.results : summary?.mode === "browse" ? summary.sessions : null;
|
|
422
|
+
const count = summary?.mode === "discover" ? summary.results.length : null;
|
|
423
|
+
|
|
424
|
+
const chatRow = (r, i) => h("div", {
|
|
425
|
+
key: r.sessionId + i, className: "dmr-row",
|
|
426
|
+
style: { display: "flex", flexDirection: "column", gap: 2, padding: "5px 8px", borderRadius: 8 },
|
|
427
|
+
}, [
|
|
428
|
+
h("div", { key: "t", style: { display: "flex", alignItems: "baseline", gap: 8 } }, [
|
|
429
|
+
h("button", {
|
|
430
|
+
key: "o", type: "button", className: "dmr-link", title: "Open this chat",
|
|
431
|
+
onClick: () => openChat(r.sessionId),
|
|
432
|
+
style: { fontWeight: 600, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
433
|
+
}, r.title || "Untitled chat"),
|
|
434
|
+
h("span", { key: "d", style: { marginLeft: "auto", color: FAINT, fontSize: 11, flex: "none" } }, dayOf(r.createdAt)),
|
|
435
|
+
]),
|
|
436
|
+
r.snippet ? h("div", { key: "s", style: { color: MUTED, fontSize: 12, lineHeight: "17px" } }, withBold(r.snippet)) : null,
|
|
437
|
+
r.matchedWords ? h("div", { key: "w", style: { color: FAINT, fontSize: 11 } }, "matched: " + r.matchedWords.join(", ")) : null,
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
let body = null;
|
|
441
|
+
if (open && results && results.length) {
|
|
442
|
+
body = h("div", { key: "body", className: "dmr-pop", style: { marginTop: 4, display: "flex", flexDirection: "column", gap: 1 } }, [
|
|
443
|
+
...results.map(chatRow),
|
|
444
|
+
summary.engine === "scan"
|
|
445
|
+
? 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.")
|
|
446
|
+
: null,
|
|
447
|
+
]);
|
|
448
|
+
} else if (open && summary && (summary.mode === "read" || summary.mode === "scroll")) {
|
|
449
|
+
body = h("div", { key: "body", style: { marginTop: 4 } }, chatRow({ ...summary.session, snippet: summary.mode === "read" ? summary.count + " messages read" : "read around one message" }, 0));
|
|
450
|
+
} else if (open && status.label === "Recalled from past chats" && count === 0) {
|
|
451
|
+
body = h("div", { key: "body", style: { color: FAINT, fontSize: 12, margin: "4px 0 0 22px" } }, "No earlier chat matched.");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return h("div", {
|
|
455
|
+
"data-plugin": "dsh-memory-recall-card",
|
|
456
|
+
style: {
|
|
457
|
+
margin: "4px 0", padding: "7px 10px", borderRadius: 10, fontSize: 13,
|
|
458
|
+
border: "1px solid " + (status.tone === "warn" ? "rgba(245,158,11,.45)" : "rgba(128,128,128,.22)"),
|
|
459
|
+
background: status.tone === "warn" ? "rgba(245,158,11,.08)" : "rgba(128,128,128,.05)",
|
|
460
|
+
},
|
|
461
|
+
}, [
|
|
462
|
+
h("button", {
|
|
463
|
+
key: "head", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open,
|
|
464
|
+
style: { display: "flex", alignItems: "center", gap: 8, width: "100%" },
|
|
465
|
+
}, [
|
|
466
|
+
h("span", { key: "i", style: { color: TONE[status.tone], display: "inline-flex" } }, HistoryIcon(15, status.busy ? "dmr-pulse" : undefined)),
|
|
467
|
+
h("span", { key: "l", style: { fontWeight: 600 } }, status.label),
|
|
468
|
+
h("span", { key: "q", style: { color: FAINT, fontSize: 12, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, "· " + describeAsk(args)),
|
|
469
|
+
count !== null && count > 0 ? h("span", { key: "n", style: { marginLeft: "auto", color: FAINT, fontSize: 12, flex: "none" } }, count + " found") : null,
|
|
470
|
+
results && results.length ? Chevron(open) : null,
|
|
471
|
+
]),
|
|
472
|
+
status.label.startsWith("Waiting")
|
|
473
|
+
? h("div", { key: "hint", style: { color: MUTED, fontSize: 12, marginTop: 3, marginLeft: 23 } }, "Allow or deny it in the message box below.")
|
|
474
|
+
: null,
|
|
475
|
+
body,
|
|
476
|
+
]);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* ---- the Memory panel -------------------------------------------------- */
|
|
481
|
+
|
|
482
|
+
async function api(path, init) {
|
|
483
|
+
const res = await fetch(BASE + path, { headers: { accept: "application/json", "content-type": "application/json" }, ...init });
|
|
484
|
+
const body = await res.json().catch(() => ({}));
|
|
485
|
+
if (!res.ok) throw new Error(body?.result?.error || body?.error || "HTTP " + res.status);
|
|
486
|
+
return body;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const PANEL_CSS = [
|
|
490
|
+
// dsh's sidebar footer is a flex ROW that does not wrap, and every entry
|
|
491
|
+
// asks for 100% of it, so the second entry is pushed off its right edge.
|
|
492
|
+
// Measured on the live page: footer row > (display: contents wrapper) >
|
|
493
|
+
// each entry. So the footer is the element whose GRANDCHILD is this
|
|
494
|
+
// entry; wrapping it puts each entry on its own line, like Settings.
|
|
495
|
+
// A CSS rule, not a one-off style, so it holds when dsh re-renders.
|
|
496
|
+
"div:has(> * > [data-plugin=\"dsh-memory-recall-sidebar\"]),div:has(> [data-plugin=\"dsh-memory-recall-sidebar\"]){flex-wrap:wrap}",
|
|
497
|
+
".dmr-panel{--dmr-yellow:#c08532;--dmr-orange:#db704b;--dmr-purple:#9e94d5;"
|
|
498
|
+
+ "--dmr-from:color-mix(in srgb,var(--dmr-yellow) 72%,#f5d08a);"
|
|
499
|
+
+ "--dmr-mid:color-mix(in srgb,var(--dmr-orange) 55%,var(--dmr-purple));"
|
|
500
|
+
+ "--dmr-to:color-mix(in srgb,var(--dmr-purple) 82%,#c4b5fd);"
|
|
501
|
+
+ "--dmr-glow:color-mix(in srgb,var(--dmr-purple) 38%,transparent)}",
|
|
502
|
+
".dmr-card{position:relative;border-radius:10px;padding:9px 10px 8px 12px;transition:background .15s,border-color .15s}",
|
|
503
|
+
".dmr-card::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;"
|
|
504
|
+
+ "background:linear-gradient(180deg,var(--dmr-from),var(--dmr-to));opacity:.75}",
|
|
505
|
+
".dmr-card .dmr-actions{opacity:0;transition:opacity .15s}",
|
|
506
|
+
".dmr-card:hover .dmr-actions,.dmr-card:focus-within .dmr-actions{opacity:1}",
|
|
507
|
+
".dmr-clamp{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}",
|
|
508
|
+
".dmr-icon-btn{cursor:pointer;border:none;background:transparent;color:inherit;width:26px;height:26px;border-radius:7px;"
|
|
509
|
+
+ "display:inline-flex;align-items:center;justify-content:center;opacity:.7}",
|
|
510
|
+
".dmr-icon-btn:hover{opacity:1;background:rgba(128,128,128,.18)}",
|
|
511
|
+
".dmr-tab{cursor:pointer;border:none;background:transparent;color:inherit;font:inherit;display:inline-flex;align-items:center;"
|
|
512
|
+
+ "gap:7px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;opacity:.6;transition:opacity .15s,background .15s}",
|
|
513
|
+
".dmr-tab:hover{opacity:.9}",
|
|
514
|
+
".dmr-tab[aria-selected=\"true\"]{opacity:1;background:rgba(128,128,128,.16)}",
|
|
515
|
+
".dmr-bar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--dmr-from),var(--dmr-mid),var(--dmr-to));transition:width .35s}",
|
|
516
|
+
".dmr-gradient-text{background-image:linear-gradient(105deg,var(--dmr-from) 0%,var(--dmr-mid) 48%,var(--dmr-to) 100%);"
|
|
517
|
+
+ "-webkit-background-clip:text;background-clip:text;color:transparent}",
|
|
518
|
+
".dmr-glyph-glow{color:color-mix(in srgb,var(--dmr-yellow) 55%,var(--dmr-purple));filter:drop-shadow(0 0 .3rem var(--dmr-glow))}",
|
|
519
|
+
".dmr-input:focus{outline:none;border-color:color-mix(in srgb,var(--dmr-purple) 70%,transparent)!important;"
|
|
520
|
+
+ "box-shadow:0 0 0 3px color-mix(in srgb,var(--dmr-purple) 22%,transparent)}",
|
|
521
|
+
].join("\n");
|
|
522
|
+
const PANEL_STYLE_ID = "dsh-memory-recall-panel-style";
|
|
523
|
+
|
|
524
|
+
function injectPanelStyles() {
|
|
525
|
+
try {
|
|
526
|
+
if (typeof document === "undefined" || document.getElementById(PANEL_STYLE_ID)) return;
|
|
527
|
+
const el = document.createElement("style");
|
|
528
|
+
el.id = PANEL_STYLE_ID;
|
|
529
|
+
el.textContent = PANEL_CSS;
|
|
530
|
+
document.head.appendChild(el);
|
|
531
|
+
} catch { /* no DOM */ }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const fmt = (n) => Number(n || 0).toLocaleString("en-US");
|
|
535
|
+
const DELIM_COST = 3; // the "\n§\n" between two entries
|
|
536
|
+
|
|
537
|
+
/** A small usage ring, for the tabs. */
|
|
538
|
+
function Ring(percent, size) {
|
|
539
|
+
const r = 6.5;
|
|
540
|
+
const c = 2 * Math.PI * r;
|
|
541
|
+
const p = Math.max(0, Math.min(100, percent || 0));
|
|
542
|
+
return h("svg", { width: size || 16, height: size || 16, viewBox: "0 0 16 16", "aria-hidden": "true", style: { flex: "none" } }, [
|
|
543
|
+
h("circle", { key: "t", cx: 8, cy: 8, r, fill: "none", stroke: "rgba(128,128,128,.3)", strokeWidth: 2 }),
|
|
544
|
+
h("circle", {
|
|
545
|
+
key: "f", cx: 8, cy: 8, r, fill: "none", strokeWidth: 2, strokeLinecap: "round",
|
|
546
|
+
stroke: p >= 90 ? "#f59e0b" : "#9e94d5", strokeDasharray: c, strokeDashoffset: c * (1 - p / 100),
|
|
547
|
+
transform: "rotate(-90 8 8)",
|
|
548
|
+
}),
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function PencilIcon() {
|
|
553
|
+
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" }, [
|
|
554
|
+
h("path", { key: "a", d: "M12 20h9" }),
|
|
555
|
+
h("path", { key: "b", d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" }),
|
|
556
|
+
]);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function TrashIcon() {
|
|
560
|
+
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" }, [
|
|
561
|
+
h("path", { key: "a", d: "M3 6h18" }),
|
|
562
|
+
h("path", { key: "b", d: "M8 6V4h8v2" }),
|
|
563
|
+
h("path", { key: "c", d: "M19 6l-1 14H6L5 6" }),
|
|
564
|
+
]);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** How the budget would look with `text` in place of `replacing` (or added). */
|
|
568
|
+
function budgetAfter(data, text, replacing) {
|
|
569
|
+
const used = data?.usage?.chars ?? 0;
|
|
570
|
+
const limit = data?.usage?.limit ?? 0;
|
|
571
|
+
const count = (data?.entries ?? []).length;
|
|
572
|
+
const next = String(text || "").trim().length;
|
|
573
|
+
let after;
|
|
574
|
+
if (replacing !== undefined) after = used - replacing.length + next;
|
|
575
|
+
else after = used + next + (count > 0 && next > 0 ? DELIM_COST : 0);
|
|
576
|
+
return { after, limit, delta: after - used, over: limit > 0 && after > limit };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function BudgetLine(b) {
|
|
580
|
+
const sign = b.delta > 0 ? "+" : b.delta < 0 ? "−" : "±";
|
|
581
|
+
return h("span", { style: { fontSize: 11, color: b.over ? "#ef4444" : FAINT, fontVariantNumeric: "tabular-nums" } },
|
|
582
|
+
sign + fmt(Math.abs(b.delta)) + " chars · " + fmt(b.after) + " / " + fmt(b.limit) + (b.over ? " — over the limit" : ""));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function EntryCard(props) {
|
|
586
|
+
const { entry, data, dark, onSave, onDelete } = props;
|
|
587
|
+
const [mode, setMode] = React.useState("view"); // view | edit | confirm
|
|
588
|
+
const [draft, setDraft] = React.useState(entry);
|
|
589
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
590
|
+
const [busy, setBusy] = React.useState(false);
|
|
591
|
+
const [error, setError] = React.useState(null);
|
|
592
|
+
const long = entry.length > 240;
|
|
593
|
+
const run = async (fn) => {
|
|
594
|
+
setBusy(true);
|
|
595
|
+
setError(null);
|
|
596
|
+
try {
|
|
597
|
+
await fn();
|
|
598
|
+
return true;
|
|
599
|
+
} catch (e) {
|
|
600
|
+
setError(String(e?.message || e));
|
|
601
|
+
return false;
|
|
602
|
+
} finally {
|
|
603
|
+
setBusy(false);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const surface = dark ? "rgba(255,255,255,.035)" : "rgba(0,0,0,.025)";
|
|
607
|
+
const field = {
|
|
608
|
+
width: "100%", boxSizing: "border-box", resize: "vertical", fontFamily: "inherit", fontSize: 12.5, lineHeight: "18px",
|
|
609
|
+
padding: "7px 9px", borderRadius: 8, border: "1px solid rgba(128,128,128,.35)",
|
|
610
|
+
background: dark ? "rgba(0,0,0,.25)" : "#fff", color: "inherit", minHeight: 72,
|
|
611
|
+
};
|
|
612
|
+
const button = (label, onClick, primary) => h("button", {
|
|
613
|
+
key: label, type: "button", disabled: busy, onClick,
|
|
614
|
+
style: {
|
|
615
|
+
cursor: busy ? "wait" : "pointer", borderRadius: 999, padding: "3px 12px", fontSize: 11.5, fontWeight: 600,
|
|
616
|
+
border: primary ? "none" : "1px solid rgba(128,128,128,.35)",
|
|
617
|
+
background: primary ? "linear-gradient(105deg,var(--dmr-from),var(--dmr-to))" : "transparent",
|
|
618
|
+
color: primary ? "#1b1b1d" : "inherit",
|
|
619
|
+
},
|
|
620
|
+
}, label);
|
|
621
|
+
|
|
622
|
+
if (mode === "edit") {
|
|
623
|
+
const b = budgetAfter(data, draft, entry);
|
|
624
|
+
return h("div", { className: "dmr-card", style: { background: surface, border: "1px solid rgba(158,148,213,.45)" } }, [
|
|
625
|
+
h("textarea", {
|
|
626
|
+
key: "t", className: "dmr-input", value: draft, autoFocus: true, style: field,
|
|
627
|
+
onChange: (e) => setDraft(e.target.value),
|
|
628
|
+
onKeyDown: async (e) => {
|
|
629
|
+
if (e.key === "Escape") { e.stopPropagation(); setMode("view"); setDraft(entry); }
|
|
630
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !b.over) {
|
|
631
|
+
if (await run(() => onSave(entry, draft))) setMode("view");
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
h("div", { key: "b", style: { display: "flex", alignItems: "center", gap: 6, marginTop: 6 } }, [
|
|
636
|
+
BudgetLine(b),
|
|
637
|
+
h("span", { key: "s", style: { flex: 1 } }),
|
|
638
|
+
button("Cancel", () => { setMode("view"); setDraft(entry); }),
|
|
639
|
+
button("Save", async () => { if (!b.over && (await run(() => onSave(entry, draft)))) setMode("view"); }, true),
|
|
640
|
+
]),
|
|
641
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5, marginTop: 4 } }, error) : null,
|
|
642
|
+
]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return h("div", { className: "dmr-card", style: { background: surface, border: "1px solid transparent" } }, [
|
|
646
|
+
h("div", { key: "row", style: { display: "flex", gap: 6, alignItems: "flex-start" } }, [
|
|
647
|
+
h("div", {
|
|
648
|
+
key: "x", className: long && !expanded ? "dmr-clamp" : undefined,
|
|
649
|
+
style: { flex: 1, minWidth: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 12.5, lineHeight: "18px" },
|
|
650
|
+
}, entry),
|
|
651
|
+
mode === "view"
|
|
652
|
+
? h("div", { key: "a", className: "dmr-actions", style: { display: "flex", gap: 2, flex: "none", marginTop: -3 } }, [
|
|
653
|
+
h("button", { key: "e", type: "button", className: "dmr-icon-btn", title: "Edit", "aria-label": "Edit entry", onClick: () => { setDraft(entry); setMode("edit"); } }, PencilIcon()),
|
|
654
|
+
h("button", { key: "d", type: "button", className: "dmr-icon-btn", title: "Delete", "aria-label": "Delete entry", onClick: () => setMode("confirm") }, TrashIcon()),
|
|
655
|
+
])
|
|
656
|
+
: null,
|
|
657
|
+
]),
|
|
658
|
+
h("div", { key: "meta", style: { display: "flex", alignItems: "center", gap: 10, marginTop: 5, fontSize: 11, color: FAINT } }, [
|
|
659
|
+
h("span", { key: "n", style: { fontVariantNumeric: "tabular-nums" } }, fmt(entry.length) + " chars"),
|
|
660
|
+
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,
|
|
661
|
+
mode === "confirm"
|
|
662
|
+
? h("span", { key: "c", style: { marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 6, color: "inherit" } }, [
|
|
663
|
+
h("span", { key: "q", style: { fontSize: 11.5 } }, "Delete this entry?"),
|
|
664
|
+
button("Keep", () => setMode("view")),
|
|
665
|
+
h("button", {
|
|
666
|
+
key: "y", type: "button", disabled: busy, onClick: () => run(() => onDelete(entry)),
|
|
667
|
+
style: { cursor: "pointer", borderRadius: 999, padding: "3px 12px", fontSize: 11.5, fontWeight: 600, border: "none", background: "#ef4444", color: "#fff" },
|
|
668
|
+
}, "Delete"),
|
|
669
|
+
])
|
|
670
|
+
: null,
|
|
671
|
+
]),
|
|
672
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5, marginTop: 4 } }, error) : null,
|
|
673
|
+
]);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function MemoryTab(props) {
|
|
677
|
+
const { target, data, dark, onEdit } = props;
|
|
678
|
+
const [filter, setFilter] = React.useState("");
|
|
679
|
+
const [adding, setAdding] = React.useState("");
|
|
680
|
+
const [busy, setBusy] = React.useState(false);
|
|
681
|
+
const [error, setError] = React.useState(null);
|
|
682
|
+
const entries = data?.entries ?? [];
|
|
683
|
+
const shown = filter.trim() ? entries.filter((e) => e.toLowerCase().includes(filter.trim().toLowerCase())) : entries;
|
|
684
|
+
const usage = data?.usage ?? { chars: 0, limit: 0, percent: 0 };
|
|
685
|
+
const add = async () => {
|
|
686
|
+
if (!adding.trim() || busy) return;
|
|
687
|
+
setBusy(true);
|
|
688
|
+
setError(null);
|
|
689
|
+
try {
|
|
690
|
+
await onEdit({ target, action: "add", content: adding });
|
|
691
|
+
setAdding("");
|
|
692
|
+
} catch (e) {
|
|
693
|
+
setError(String(e?.message || e));
|
|
694
|
+
} finally {
|
|
695
|
+
setBusy(false);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const b = budgetAfter(data, adding);
|
|
699
|
+
const noun = target === "user" ? "profile entry" : "note";
|
|
700
|
+
|
|
701
|
+
return h("div", { style: { display: "flex", flexDirection: "column", minHeight: 0, flex: 1 } }, [
|
|
702
|
+
h("div", { key: "usage", style: { padding: "2px 16px 10px" } }, [
|
|
703
|
+
h("div", { key: "bar", style: { height: 6, borderRadius: 999, background: "rgba(128,128,128,.2)", overflow: "hidden" } },
|
|
704
|
+
h("div", { className: "dmr-bar-fill", style: { width: Math.min(100, usage.percent) + "%", ...(usage.percent >= 90 ? { background: "#f59e0b" } : {}) } })),
|
|
705
|
+
h("div", { key: "t", style: { display: "flex", justifyContent: "space-between", marginTop: 5, fontSize: 11, color: FAINT, fontVariantNumeric: "tabular-nums" } }, [
|
|
706
|
+
h("span", { key: "a" }, TARGET_HINT[target]),
|
|
707
|
+
h("span", { key: "b", style: { flex: "none", marginLeft: 8 } }, fmt(usage.chars) + " / " + fmt(usage.limit)),
|
|
708
|
+
]),
|
|
709
|
+
]),
|
|
710
|
+
entries.length > 3
|
|
711
|
+
? h("div", { key: "search", style: { padding: "0 16px 8px" } },
|
|
712
|
+
h("input", {
|
|
713
|
+
className: "dmr-input", value: filter, placeholder: "Search " + entries.length + " entries…",
|
|
714
|
+
onChange: (e) => setFilter(e.target.value),
|
|
715
|
+
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" },
|
|
716
|
+
}))
|
|
717
|
+
: null,
|
|
718
|
+
h("div", { key: "list", style: { overflowY: "auto", padding: "0 10px 0 16px", display: "flex", flexDirection: "column", gap: 6, minHeight: 60, flex: 1 } },
|
|
719
|
+
entries.length === 0
|
|
720
|
+
? [h("div", { key: "empty", style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8, padding: "26px 10px", color: FAINT, textAlign: "center" } }, [
|
|
721
|
+
h("span", { key: "i", className: "dmr-glyph-glow", style: { opacity: 0.7, display: "inline-flex" } }, SolidBrainIcon(30)),
|
|
722
|
+
h("div", { key: "t", style: { fontSize: 12.5, fontWeight: 600, color: MUTED } }, "Nothing here yet"),
|
|
723
|
+
h("div", { key: "s", style: { fontSize: 11.5, lineHeight: "16px", maxWidth: 260 } },
|
|
724
|
+
target === "user"
|
|
725
|
+
? "Tell the model about yourself and it saves it here — or add something below."
|
|
726
|
+
: "The model saves what it learns as you work — or add a note below."),
|
|
727
|
+
])]
|
|
728
|
+
: shown.length === 0
|
|
729
|
+
? [h("div", { key: "none", style: { color: FAINT, fontSize: 12, padding: "12px 4px" } }, "No entry matches “" + filter + "”.")]
|
|
730
|
+
: shown.map((entry) => h(EntryCard, {
|
|
731
|
+
key: entry, entry, data, dark,
|
|
732
|
+
onSave: (from, to) => onEdit({ target, action: "replace", old_text: from, content: to }),
|
|
733
|
+
onDelete: (text) => onEdit({ target, action: "remove", old_text: text }),
|
|
734
|
+
}))),
|
|
735
|
+
h("div", { key: "add", style: { padding: "10px 16px 4px", borderTop: "1px solid rgba(128,128,128,.18)", marginTop: 8 } }, [
|
|
736
|
+
h("div", { key: "row", style: { display: "flex", gap: 8, alignItems: "flex-end" } }, [
|
|
737
|
+
h("textarea", {
|
|
738
|
+
key: "t", className: "dmr-input", value: adding, rows: adding.includes("\n") || adding.length > 60 ? 3 : 1,
|
|
739
|
+
placeholder: "Add a " + noun + "…",
|
|
740
|
+
onChange: (e) => setAdding(e.target.value),
|
|
741
|
+
onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (!b.over) add(); } },
|
|
742
|
+
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" },
|
|
743
|
+
}),
|
|
744
|
+
h("button", {
|
|
745
|
+
key: "b", type: "button", disabled: busy || !adding.trim() || b.over, onClick: add,
|
|
746
|
+
style: {
|
|
747
|
+
flex: "none", cursor: busy || !adding.trim() || b.over ? "default" : "pointer", borderRadius: 10, padding: "7px 14px", fontSize: 12, fontWeight: 700, border: "none",
|
|
748
|
+
background: adding.trim() && !b.over ? "linear-gradient(105deg,var(--dmr-from),var(--dmr-to))" : "rgba(128,128,128,.2)",
|
|
749
|
+
color: adding.trim() && !b.over ? "#1b1b1d" : "inherit", opacity: busy ? 0.6 : 1,
|
|
750
|
+
},
|
|
751
|
+
}, "Add"),
|
|
752
|
+
]),
|
|
753
|
+
h("div", { key: "hint", style: { marginTop: 4, minHeight: 15 } },
|
|
754
|
+
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")),
|
|
755
|
+
error ? h("div", { key: "e", style: { color: "#ef4444", fontSize: 11.5 } }, error) : null,
|
|
756
|
+
]),
|
|
757
|
+
]);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function MemoryPanel(props) {
|
|
761
|
+
const wide = props?.wide !== false;
|
|
762
|
+
const [open, setOpen] = React.useState(false);
|
|
763
|
+
const [state, setState] = React.useState(null);
|
|
764
|
+
const [error, setError] = React.useState(null);
|
|
765
|
+
const [tab, setTab] = React.useState(null);
|
|
766
|
+
const [showSettings, setShowSettings] = React.useState(false);
|
|
767
|
+
const ui = useUi();
|
|
768
|
+
const rootRef = React.useRef(null);
|
|
769
|
+
React.useEffect(() => { injectStyles(); injectPanelStyles(); }, []);
|
|
770
|
+
|
|
771
|
+
const load = React.useCallback(async () => {
|
|
772
|
+
try {
|
|
773
|
+
setState(await api("/state"));
|
|
774
|
+
setError(null);
|
|
775
|
+
} catch (e) {
|
|
776
|
+
setError(String(e?.message || e));
|
|
777
|
+
}
|
|
778
|
+
}, []);
|
|
779
|
+
|
|
780
|
+
// Fresh when opened, and every few seconds while open — the model may
|
|
781
|
+
// be saving in another chat at the same time.
|
|
782
|
+
React.useEffect(() => {
|
|
783
|
+
if (!open) return undefined;
|
|
784
|
+
load();
|
|
785
|
+
const t = setInterval(load, 5000);
|
|
786
|
+
return () => clearInterval(t);
|
|
787
|
+
}, [open, load]);
|
|
788
|
+
// Once at start, for the count on the button.
|
|
789
|
+
React.useEffect(() => { load(); }, [load]);
|
|
790
|
+
// The composer chip's "Open Memory panel".
|
|
791
|
+
React.useEffect(() => {
|
|
792
|
+
if (typeof window === "undefined" || !window.addEventListener) return undefined;
|
|
793
|
+
const onOpen = () => setOpen(true);
|
|
794
|
+
window.addEventListener(OPEN_EVENT, onOpen);
|
|
795
|
+
return () => window.removeEventListener(OPEN_EVENT, onOpen);
|
|
796
|
+
}, []);
|
|
797
|
+
|
|
798
|
+
// Escape and a click outside close it.
|
|
799
|
+
React.useEffect(() => {
|
|
800
|
+
if (!open || typeof document === "undefined") return undefined;
|
|
801
|
+
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
802
|
+
const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
|
|
803
|
+
document.addEventListener("keydown", onKey);
|
|
804
|
+
document.addEventListener("mousedown", onDown);
|
|
805
|
+
return () => { document.removeEventListener("keydown", onKey); document.removeEventListener("mousedown", onDown); };
|
|
806
|
+
}, [open]);
|
|
807
|
+
|
|
808
|
+
const edit = async (body) => {
|
|
809
|
+
const out = await api("/entry", { method: "POST", body: JSON.stringify(body) });
|
|
810
|
+
if (out?.state) setState(out.state);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const dark = isDark();
|
|
814
|
+
const counts = state ? { memory: state.memory.entries.length, user: state.user.entries.length } : { memory: 0, user: 0 };
|
|
815
|
+
const total = counts.memory + counts.user;
|
|
816
|
+
const current = tab ?? (counts.memory === 0 && counts.user > 0 ? "user" : "memory");
|
|
817
|
+
|
|
818
|
+
const tabButton = (target, label) => h("button", {
|
|
819
|
+
key: target, type: "button", role: "tab", className: "dmr-tab", "aria-selected": current === target,
|
|
820
|
+
onClick: () => setTab(target),
|
|
821
|
+
}, [
|
|
822
|
+
Ring(state?.[target]?.usage?.percent ?? 0),
|
|
823
|
+
h("span", { key: "l" }, label),
|
|
824
|
+
h("span", { key: "n", style: { fontSize: 11, opacity: 0.6, fontVariantNumeric: "tabular-nums" } }, String(counts[target])),
|
|
825
|
+
]);
|
|
826
|
+
|
|
827
|
+
const panel = open
|
|
828
|
+
? h("div", {
|
|
829
|
+
key: "panel", "data-plugin": "dsh-memory-recall-panel", className: "dmr-pop dmr-panel", role: "dialog", "aria-label": "Memory",
|
|
830
|
+
style: {
|
|
831
|
+
position: "fixed", bottom: 60, left: wide ? 12 : 64, zIndex: 50, width: 440, maxWidth: "calc(100vw - 24px)", height: "min(640px, 78vh)",
|
|
832
|
+
display: "flex", flexDirection: "column", boxSizing: "border-box", fontSize: 12, borderRadius: 16, overflow: "hidden",
|
|
833
|
+
border: "1px solid " + (dark ? "rgba(255,255,255,.12)" : "rgba(0,0,0,.10)"),
|
|
834
|
+
background: dark ? "#19191b" : "#fff", color: dark ? "#ececec" : "#111",
|
|
835
|
+
boxShadow: "0 18px 48px rgba(0,0,0,.35), 0 0 0 1px rgba(158,148,213,.08)",
|
|
836
|
+
},
|
|
837
|
+
}, [
|
|
838
|
+
h("div", { key: "head", style: { display: "flex", alignItems: "center", gap: 10, padding: "14px 14px 10px 16px" } }, [
|
|
839
|
+
h("span", { key: "i", className: "dmr-glyph-glow", style: { display: "inline-flex" } }, SolidBrainIcon(22)),
|
|
840
|
+
h("div", { key: "t", style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 } }, [
|
|
841
|
+
h("span", { key: "a", className: "dmr-gradient-text", style: { fontWeight: 700, fontSize: 16, lineHeight: "20px" } }, "Memory"),
|
|
842
|
+
h("span", { key: "b", style: { fontSize: 11, color: FAINT } }, "Given to every chat \u00b7 a change reaches open chats on their next step"),
|
|
843
|
+
]),
|
|
844
|
+
h("button", {
|
|
845
|
+
key: "s", type: "button", className: "dmr-icon-btn", onClick: () => setShowSettings(!showSettings),
|
|
846
|
+
"aria-label": "Display settings", "aria-expanded": showSettings,
|
|
847
|
+
style: { marginLeft: "auto", opacity: showSettings ? 1 : undefined, background: showSettings ? "rgba(128,128,128,.18)" : undefined },
|
|
848
|
+
}, GearIcon(15)),
|
|
849
|
+
h("button", { key: "x", type: "button", className: "dmr-icon-btn", onClick: () => setOpen(false), "aria-label": "Close", style: { fontSize: 14 } }, "✕"),
|
|
850
|
+
]),
|
|
851
|
+
showSettings
|
|
852
|
+
? h("div", {
|
|
853
|
+
key: "settings", "data-plugin": "dsh-memory-recall-settings", className: "dmr-pop",
|
|
854
|
+
style: {
|
|
855
|
+
margin: "0 12px 10px", padding: "8px 10px", borderRadius: 12, display: "flex", flexDirection: "column", gap: 2,
|
|
856
|
+
background: dark ? "rgba(255,255,255,.035)" : "rgba(0,0,0,.03)", border: "1px solid " + (dark ? "rgba(255,255,255,.08)" : "rgba(0,0,0,.07)"),
|
|
857
|
+
},
|
|
858
|
+
}, [
|
|
859
|
+
h("div", { key: "t", style: { fontSize: 10.5, fontWeight: 700, letterSpacing: ".4px", color: FAINT, margin: "0 0 4px" } }, "SHOW IN DSH"),
|
|
860
|
+
...UI_SWITCHES.map((sw) => h("div", { key: sw.key, style: { display: "flex", alignItems: "center", gap: 10, padding: "5px 0" } }, [
|
|
861
|
+
h("div", { key: "t", style: { display: "flex", flexDirection: "column", gap: 1, minWidth: 0, flex: 1 } }, [
|
|
862
|
+
h("span", { key: "l", style: { fontWeight: 600 } }, sw.label),
|
|
863
|
+
h("span", { key: "n", style: { fontSize: 11, color: FAINT, lineHeight: "15px" } }, sw.note),
|
|
864
|
+
]),
|
|
865
|
+
Switch(sw.key, ui[sw.key] !== false, (on) => writeUi({ [sw.key]: on }), sw.label),
|
|
866
|
+
])),
|
|
867
|
+
h("div", { key: "f", style: { fontSize: 10.5, color: FAINT, marginTop: 4 } }, "Saved in this browser. Memory itself keeps working either way."),
|
|
868
|
+
])
|
|
869
|
+
: null,
|
|
870
|
+
h("div", { key: "tabs", role: "tablist", style: { display: "flex", gap: 4, padding: "0 12px 10px" } }, [
|
|
871
|
+
tabButton("memory", "Notes"),
|
|
872
|
+
tabButton("user", "Profile"),
|
|
873
|
+
]),
|
|
874
|
+
error ? h("div", { key: "err", style: { color: "#ef4444", margin: "0 16px 8px" } }, "Could not load memory: " + error) : null,
|
|
875
|
+
state && state.tools && !state.tools.ok
|
|
876
|
+
? h("div", {
|
|
877
|
+
key: "tools",
|
|
878
|
+
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" },
|
|
879
|
+
}, "The model's memory tools did not load, so it can read this memory but cannot save to it or search earlier chats. "
|
|
880
|
+
+ "Reason: " + (state.tools.error || "unknown") + ". Restart dsh after fixing; entries added here still work.")
|
|
881
|
+
: null,
|
|
882
|
+
state
|
|
883
|
+
? h(MemoryTab, { key: current, target: current, data: state[current], dark, onEdit: edit })
|
|
884
|
+
: h("div", { key: "loading", style: { padding: 20, color: FAINT } }, "Loading…"),
|
|
885
|
+
state
|
|
886
|
+
? h("div", { key: "dir", style: { padding: "4px 16px 10px", color: FAINT, fontSize: 10, wordBreak: "break-all" } }, "Stored in " + state.dir)
|
|
887
|
+
: null,
|
|
888
|
+
])
|
|
889
|
+
: null;
|
|
890
|
+
|
|
891
|
+
const button = wide
|
|
892
|
+
? h("button", {
|
|
893
|
+
key: "btn", type: "button", className: "dmr-row", onClick: () => setOpen(!open),
|
|
894
|
+
title: "Memory", "aria-label": "Memory", "aria-expanded": open,
|
|
895
|
+
style: {
|
|
896
|
+
// Settings' own row, measured: 42px tall, 2px past the row each side.
|
|
897
|
+
width: "calc(100% + 4px)", margin: "0 -2px", minWidth: 0, height: 42, boxSizing: "border-box",
|
|
898
|
+
color: "var(--dsw-alias-label-primary, inherit)", cursor: "pointer",
|
|
899
|
+
background: open ? "var(--dsw-alias-interactive-bg-hover-solid, rgba(128,128,128,.12))" : "0 0",
|
|
900
|
+
border: "none", borderRadius: 12, alignItems: "center", gap: 8, padding: "0 10px 0 8px",
|
|
901
|
+
fontFamily: "inherit", fontSize: 14, display: "inline-flex", overflow: "hidden",
|
|
902
|
+
},
|
|
903
|
+
}, [
|
|
904
|
+
h("span", { key: "i", className: total ? "dmr-glyph-glow" : undefined, style: { display: "inline-flex", alignItems: "center", flex: "none" } }, SolidBrainIcon(16)),
|
|
905
|
+
h("span", { key: "l", style: { textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0, overflow: "hidden" } }, "Memory"),
|
|
906
|
+
total ? h("span", {
|
|
907
|
+
key: "n",
|
|
908
|
+
style: {
|
|
909
|
+
marginLeft: "auto", minWidth: 18, height: 18, padding: "0 5px", borderRadius: 999, boxSizing: "border-box",
|
|
910
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 10.5, fontWeight: 700,
|
|
911
|
+
background: "linear-gradient(105deg,#e2b56d,#a58fd9)", color: "#1b1b1d", fontVariantNumeric: "tabular-nums",
|
|
912
|
+
},
|
|
913
|
+
}, String(total)) : null,
|
|
914
|
+
])
|
|
915
|
+
: h("button", {
|
|
916
|
+
key: "btn", type: "button", className: "dmr-icon-btn", onClick: () => setOpen(!open),
|
|
917
|
+
title: total ? "Memory (" + total + ")" : "Memory", "aria-label": "Memory", "aria-expanded": open,
|
|
918
|
+
style: { width: 36, height: 36, borderRadius: 10, opacity: 1, color: "var(--dsw-alias-label-primary, inherit)" },
|
|
919
|
+
}, h("span", { className: total ? "dmr-glyph-glow" : undefined, style: { display: "inline-flex" } }, SolidBrainIcon(16)));
|
|
920
|
+
|
|
921
|
+
return h("div", {
|
|
922
|
+
ref: rootRef, "data-plugin": "dsh-memory-recall-sidebar", className: "dmr-panel",
|
|
923
|
+
style: wide
|
|
924
|
+
? { flex: "0 0 100%", minWidth: 0, alignItems: "center", height: 50, margin: 0, display: "flex", position: "relative" }
|
|
925
|
+
: { display: "flex", justifyContent: "center", alignItems: "center", position: "relative", width: "100%", height: 50 },
|
|
926
|
+
}, [button, panel]);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* ---- the memory context row -------------------------------------------------- */
|
|
930
|
+
//
|
|
931
|
+
// Memory reaches the model as a context message (src/inject.js), which dsh
|
|
932
|
+
// logs and draws with its generic "Context injection" row. This draws that
|
|
933
|
+
// one row the way the plugin's other cards look — the brain, the gradient
|
|
934
|
+
// title, the counts, and the entries laid out when opened.
|
|
935
|
+
//
|
|
936
|
+
// `conversation.chat.node` is a KEYED slot and dsh's renderer owns key
|
|
937
|
+
// "context" for EVERY context row (system prompt, skill catalog, …). So this
|
|
938
|
+
// registers the same key at priority -10 (lower renders; the same priority
|
|
939
|
+
// would make the slot host fail dsh's own entry) and, for every row that is
|
|
940
|
+
// not ours, renders dsh's own component — found in the slot's entries — with
|
|
941
|
+
// the very props it would have had. Same locale namespace ("chat"), so its
|
|
942
|
+
// `t` is dsh's own. If dsh's component cannot be found, a plain row stands in.
|
|
943
|
+
|
|
944
|
+
const NODE_PRIORITY = -10;
|
|
945
|
+
const RULE_LINE = /^[\u2550=]{6,}\s*$/;
|
|
946
|
+
|
|
947
|
+
/** The entries a memory context message carries, by store (pure, for tests). */
|
|
948
|
+
function parseMemoryText(text) {
|
|
949
|
+
const out = { user: [], memory: [] };
|
|
950
|
+
let current = null;
|
|
951
|
+
let buffer = [];
|
|
952
|
+
const flush = () => {
|
|
953
|
+
if (current && buffer.length) {
|
|
954
|
+
out[current].push(...buffer.join("\n").split(/\n\u00a7\n/).map((e) => e.trim()).filter(Boolean));
|
|
955
|
+
}
|
|
956
|
+
buffer = [];
|
|
957
|
+
};
|
|
958
|
+
for (const line of String(text || "").split("\n")) {
|
|
959
|
+
if (/^USER PROFILE \(/.test(line)) { flush(); current = "user"; continue; }
|
|
960
|
+
if (/^MEMORY \(/.test(line)) { flush(); current = "memory"; continue; }
|
|
961
|
+
if (RULE_LINE.test(line)) { if (buffer.length) flush(); continue; }
|
|
962
|
+
if (/^<\/?system-reminder>$/.test(line)) { flush(); current = null; continue; }
|
|
963
|
+
if (current) buffer.push(line);
|
|
964
|
+
}
|
|
965
|
+
flush();
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const isMemorySource = (source) => !!source && source.kind === "plugin" && source.plugin === "memory";
|
|
970
|
+
|
|
971
|
+
function MemoryInjectionRow({ data }) {
|
|
972
|
+
const [open, setOpen] = React.useState(false);
|
|
973
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
974
|
+
const source = data?.source || {};
|
|
975
|
+
const text = (Array.isArray(data?.content) ? data.content : []).map((c) => (typeof c?.text === "string" ? c.text : "")).join("\n");
|
|
976
|
+
const entries = parseMemoryText(text);
|
|
977
|
+
const title = source.reason === "compaction" ? "Memory reloaded" : source.reason === "update" ? "Memory updated" : "Memory loaded";
|
|
978
|
+
const counts = entries.user.length + " profile · " + entries.memory.length + (entries.memory.length === 1 ? " note" : " notes");
|
|
979
|
+
const meta = (source.reason === "compaction" ? "after compaction · " : "") + counts;
|
|
980
|
+
const section = (key, label, list) => list.length
|
|
981
|
+
? [
|
|
982
|
+
h("div", { key: key + "h", style: { color: FAINT, fontSize: 10.5, fontWeight: 700, letterSpacing: ".4px", marginTop: key === "u" ? 0 : 6 } }, label),
|
|
983
|
+
...list.map((e, i) => h("div", { key: key + i, style: { whiteSpace: "pre-wrap", wordBreak: "break-word", paddingLeft: 10 } }, (i + 1) + ". " + e)),
|
|
984
|
+
]
|
|
985
|
+
: [];
|
|
986
|
+
return h("div", { "data-plugin": "dsh-memory-recall-context", className: "dmr-chip", style: { margin: "2px 0", fontSize: 13 } }, [
|
|
987
|
+
h("button", {
|
|
988
|
+
key: "head", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open,
|
|
989
|
+
style: { display: "inline-flex", alignItems: "center", gap: 7, color: MUTED, padding: "2px 0" },
|
|
990
|
+
}, [
|
|
991
|
+
h("span", { key: "i", className: "dmr-legendary-glyph", style: { display: "inline-flex" } }, SolidBrainIcon(14)),
|
|
992
|
+
h("span", { key: "l", className: "dmr-legendary-title" }, title),
|
|
993
|
+
h("span", { key: "t", className: "dmr-legendary-meta", style: { fontSize: 12 } }, "· " + meta),
|
|
994
|
+
Chevron(open),
|
|
995
|
+
]),
|
|
996
|
+
open
|
|
997
|
+
? h("div", { key: "body", className: "dmr-pop", style: { margin: "4px 0 6px 22px", display: "flex", flexDirection: "column", gap: 4, fontSize: 12 } },
|
|
998
|
+
entries.user.length + entries.memory.length
|
|
999
|
+
? [...section("u", "USER PROFILE", entries.user), ...section("m", "NOTES", entries.memory)]
|
|
1000
|
+
: [h("div", { key: "none", style: { color: FAINT } }, "Nothing is saved yet.")])
|
|
1001
|
+
: null,
|
|
1002
|
+
]);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** Stand-in for dsh's own context row, used only if it cannot be found. */
|
|
1006
|
+
function PlainContextRow(props) {
|
|
1007
|
+
const [open, setOpen] = React.useState(false);
|
|
1008
|
+
const data = props?.node?.data || {};
|
|
1009
|
+
const label = data.provenance?.label || data.source?.kind || "context";
|
|
1010
|
+
const text = (Array.isArray(data.content) ? data.content : []).map((c) => (typeof c?.text === "string" ? c.text : "")).join("\n");
|
|
1011
|
+
return h("div", { style: { margin: "2px 0", fontSize: 13 } }, [
|
|
1012
|
+
h("button", { key: "h", type: "button", className: "dmr-link", onClick: () => setOpen(!open), "aria-expanded": open, style: { color: MUTED } },
|
|
1013
|
+
"Context injection · " + label),
|
|
1014
|
+
open ? h("pre", { key: "b", style: { whiteSpace: "pre-wrap", fontSize: 11.5, color: MUTED, margin: "4px 0 0 12px", maxHeight: 240, overflow: "auto" } }, text) : null,
|
|
1015
|
+
]);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function makeContextNode(ctx) {
|
|
1019
|
+
let shipped = null;
|
|
1020
|
+
const findShipped = () => {
|
|
1021
|
+
if (shipped) return shipped;
|
|
1022
|
+
try {
|
|
1023
|
+
const list = typeof ctx.slots?.entries === "function" ? ctx.slots.entries("conversation.chat.node") : [];
|
|
1024
|
+
shipped = (list || []).find((e) => e && e.key === "context" && e.component && e.component !== ContextNode && e.priority !== NODE_PRIORITY) || null;
|
|
1025
|
+
} catch { shipped = null; }
|
|
1026
|
+
return shipped;
|
|
1027
|
+
};
|
|
1028
|
+
function ContextNode(props) {
|
|
1029
|
+
const data = props?.node?.data;
|
|
1030
|
+
if (isMemorySource(data?.source)) return h(MemoryInjectionRow, { data });
|
|
1031
|
+
const own = findShipped();
|
|
1032
|
+
return own ? h(own.component, props) : h(PlainContextRow, props);
|
|
1033
|
+
}
|
|
1034
|
+
return ContextNode;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/* ---- the composer chip: is memory in this chat? --------------------------- */
|
|
1038
|
+
//
|
|
1039
|
+
// Memory reaches a chat through its system prompt, once, when the chat's
|
|
1040
|
+
// agent starts (a new chat, or one reopened after dsh restarts) — so there
|
|
1041
|
+
// is no "read" to watch for. What can be shown is the fact itself: this
|
|
1042
|
+
// chat's prompt holds memory, since when, how much, and whether memory
|
|
1043
|
+
// has changed since (a new chat picks that up). The host records each
|
|
1044
|
+
// briefing; the chip asks for this chat's.
|
|
1045
|
+
|
|
1046
|
+
const OPEN_EVENT = "dsh-memory-recall:open";
|
|
1047
|
+
const at12 = (ms) => new Date(ms).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
1048
|
+
const plural = (n, one, many) => n + " " + (n === 1 ? one : many);
|
|
1049
|
+
|
|
1050
|
+
/** What the chip says for one briefing record (pure, for tests). */
|
|
1051
|
+
function briefingView(b) {
|
|
1052
|
+
if (!b || !b.briefed) {
|
|
1053
|
+
return {
|
|
1054
|
+
state: "none", label: "Memory",
|
|
1055
|
+
title: "Not in this chat yet",
|
|
1056
|
+
lines: [
|
|
1057
|
+
"Memory is given to a chat as a context message before its first answer, and again after compaction or a change.",
|
|
1058
|
+
b && b.now && (b.now.user + b.now.memory) > 0
|
|
1059
|
+
? "It holds " + plural(b.now.user, "profile entry", "profile entries") + " and " + plural(b.now.memory, "note", "notes") + " — send a message and this chat gets them."
|
|
1060
|
+
: "Nothing is saved yet.",
|
|
1061
|
+
],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const count = b.user + b.memory;
|
|
1065
|
+
const lines = [
|
|
1066
|
+
({ compaction: "Given again after the chat was compacted", update: "Given again after memory changed", resume: "Loaded when this chat was reopened" }[b.source]
|
|
1067
|
+
|| "Given to this chat at its start") + " · " + at12(b.at),
|
|
1068
|
+
plural(b.user, "profile entry", "profile entries") + " · " + plural(b.memory, "note", "notes"),
|
|
1069
|
+
"It shows in the chat as \u201cContext injection · memory\u201d, and is given again after compaction or a change.",
|
|
1070
|
+
];
|
|
1071
|
+
if (b.changed) {
|
|
1072
|
+
lines.push("Memory has changed since (now " + plural(b.now.user, "profile entry", "profile entries") + " · " + plural(b.now.memory, "note", "notes") + "). A new chat uses the new version.");
|
|
1073
|
+
}
|
|
1074
|
+
return { state: b.changed ? "changed" : "loaded", label: "Memory", count, title: count ? "Memory is in this chat" : "Memory is in this chat (empty)", lines };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function MemoryChip(props) {
|
|
1078
|
+
const sessionId = props?.sessionId || null;
|
|
1079
|
+
const [b, setB] = React.useState(null);
|
|
1080
|
+
const [open, setOpen] = React.useState(false);
|
|
1081
|
+
const [bloom, setBloom] = React.useState(false);
|
|
1082
|
+
const lastAt = React.useRef(null);
|
|
1083
|
+
const rootRef = React.useRef(null);
|
|
1084
|
+
React.useEffect(() => { injectStyles(); }, []);
|
|
1085
|
+
|
|
1086
|
+
React.useEffect(() => {
|
|
1087
|
+
let alive = true;
|
|
1088
|
+
const pull = async () => {
|
|
1089
|
+
try {
|
|
1090
|
+
const next = await api("/briefing?session=" + encodeURIComponent(sessionId || ""));
|
|
1091
|
+
if (!alive) return;
|
|
1092
|
+
// A briefing this tab has not seen yet: one bloom, as memory lands.
|
|
1093
|
+
if (next && next.briefed && next.at !== lastAt.current) {
|
|
1094
|
+
if (lastAt.current !== null || Date.now() - next.at < 60_000) setBloom(true);
|
|
1095
|
+
lastAt.current = next.at;
|
|
1096
|
+
}
|
|
1097
|
+
setB(next);
|
|
1098
|
+
} catch { /* keep the last answer */ }
|
|
1099
|
+
};
|
|
1100
|
+
lastAt.current = null;
|
|
1101
|
+
pull();
|
|
1102
|
+
const t = setInterval(pull, 8000);
|
|
1103
|
+
const onFocus = () => pull();
|
|
1104
|
+
if (typeof window !== "undefined" && window.addEventListener) window.addEventListener("focus", onFocus);
|
|
1105
|
+
return () => {
|
|
1106
|
+
alive = false;
|
|
1107
|
+
clearInterval(t);
|
|
1108
|
+
if (typeof window !== "undefined" && window.removeEventListener) window.removeEventListener("focus", onFocus);
|
|
1109
|
+
};
|
|
1110
|
+
}, [sessionId]);
|
|
1111
|
+
|
|
1112
|
+
React.useEffect(() => {
|
|
1113
|
+
if (!bloom) return undefined;
|
|
1114
|
+
const t = setTimeout(() => setBloom(false), 1200);
|
|
1115
|
+
return () => clearTimeout(t);
|
|
1116
|
+
}, [bloom]);
|
|
1117
|
+
|
|
1118
|
+
React.useEffect(() => {
|
|
1119
|
+
if (!open || typeof document === "undefined" || !document.addEventListener) return undefined;
|
|
1120
|
+
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
1121
|
+
const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
|
|
1122
|
+
document.addEventListener("keydown", onKey);
|
|
1123
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
1124
|
+
return () => { document.removeEventListener("keydown", onKey); document.removeEventListener("pointerdown", onDown, true); };
|
|
1125
|
+
}, [open]);
|
|
1126
|
+
|
|
1127
|
+
const ui = useUi();
|
|
1128
|
+
const view = briefingView(b);
|
|
1129
|
+
const on = view.state !== "none";
|
|
1130
|
+
const dark = isDark();
|
|
1131
|
+
if (ui.composerButton === false) return null;
|
|
1132
|
+
|
|
1133
|
+
const pill = h("button", {
|
|
1134
|
+
key: "pill", type: "button", className: "dmr-chip dmr-composer-chip", "aria-label": view.title, "aria-expanded": open,
|
|
1135
|
+
"data-memory-state": view.state,
|
|
1136
|
+
onClick: () => setOpen(!open),
|
|
1137
|
+
style: {
|
|
1138
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", width: 28, height: 28, padding: 0, borderRadius: 999, cursor: "pointer", boxSizing: "border-box",
|
|
1139
|
+
fontFamily: "inherit", fontSize: 12, lineHeight: 1, color: "inherit", position: "relative",
|
|
1140
|
+
border: on ? "1px solid color-mix(in srgb,#9e94d5 45%,transparent)" : "1px dashed rgba(128,128,128,.45)",
|
|
1141
|
+
background: on ? "color-mix(in srgb,#9e94d5 10%,transparent)" : "transparent",
|
|
1142
|
+
opacity: on ? 1 : 0.7,
|
|
1143
|
+
},
|
|
1144
|
+
}, [
|
|
1145
|
+
h("span", {
|
|
1146
|
+
key: "i",
|
|
1147
|
+
className: on ? "dmr-legendary-glyph" + (bloom ? " dmr-land" : "") : undefined,
|
|
1148
|
+
style: { display: "inline-flex", color: on ? undefined : FAINT },
|
|
1149
|
+
}, SolidBrainIcon(15)),
|
|
1150
|
+
view.state === "changed"
|
|
1151
|
+
? h("span", { key: "d", "aria-hidden": "true", style: { position: "absolute", top: -1, right: -1, width: 7, height: 7, borderRadius: 999, background: "#f59e0b", boxShadow: "0 0 0 2px " + (dark ? "#1b1b1d" : "#fff") } })
|
|
1152
|
+
: null,
|
|
1153
|
+
]);
|
|
1154
|
+
|
|
1155
|
+
const card = open
|
|
1156
|
+
? h("div", {
|
|
1157
|
+
key: "card", role: "dialog", "aria-label": view.title, className: "dmr-pop dmr-chip", "data-plugin": "dsh-memory-recall-briefing",
|
|
1158
|
+
style: {
|
|
1159
|
+
position: "absolute", bottom: "calc(100% + 8px)", right: 0, zIndex: 40, width: 290, boxSizing: "border-box",
|
|
1160
|
+
padding: "11px 12px", borderRadius: 12, fontSize: 12, lineHeight: "17px", textAlign: "left",
|
|
1161
|
+
background: dark ? "#1c1c1e" : "#fff", color: dark ? "#eee" : "#111",
|
|
1162
|
+
border: "1px solid " + (dark ? "rgba(255,255,255,.14)" : "rgba(0,0,0,.10)"), boxShadow: "0 10px 28px rgba(0,0,0,.28)",
|
|
1163
|
+
},
|
|
1164
|
+
}, [
|
|
1165
|
+
h("div", { key: "t", style: { display: "flex", alignItems: "center", gap: 7, fontWeight: 700, fontSize: 13, marginBottom: 6 } }, [
|
|
1166
|
+
h("span", { key: "i", className: on ? "dmr-legendary-glyph" : undefined, style: { display: "inline-flex", color: on ? undefined : FAINT } }, SolidBrainIcon(15)),
|
|
1167
|
+
h("span", { key: "l", className: on ? "dmr-legendary-title" : undefined }, view.title),
|
|
1168
|
+
]),
|
|
1169
|
+
...view.lines.map((line, i) => h("div", {
|
|
1170
|
+
key: "l" + i,
|
|
1171
|
+
style: /changed since/.test(line)
|
|
1172
|
+
? { marginTop: 6, padding: "6px 8px", borderRadius: 8, background: "rgba(245,158,11,.12)", color: dark ? "#fcd34d" : "#b45309" }
|
|
1173
|
+
: { marginTop: i ? 3 : 0, color: i >= 2 ? MUTED : "inherit" },
|
|
1174
|
+
}, line)),
|
|
1175
|
+
h("button", {
|
|
1176
|
+
key: "open", type: "button",
|
|
1177
|
+
onClick: () => { setOpen(false); if (typeof window !== "undefined" && window.dispatchEvent) window.dispatchEvent(new Event(OPEN_EVENT)); },
|
|
1178
|
+
style: {
|
|
1179
|
+
marginTop: 9, cursor: "pointer", borderRadius: 999, padding: "3px 11px", fontSize: 11.5, fontWeight: 600, fontFamily: "inherit",
|
|
1180
|
+
border: "1px solid rgba(128,128,128,.4)", background: "transparent", color: "inherit",
|
|
1181
|
+
},
|
|
1182
|
+
}, "Open Memory panel"),
|
|
1183
|
+
])
|
|
1184
|
+
: null;
|
|
1185
|
+
|
|
1186
|
+
return h("span", { ref: rootRef, "data-plugin": "dsh-memory-recall-chip-seat", style: { position: "relative", display: "inline-flex", alignItems: "center" } }, [pill, card]);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/* ---- @ chats, grouped by workspace --------------------------------------- */
|
|
1190
|
+
//
|
|
1191
|
+
// dsh's own @ list shows at most 50 sessions and includes sub-agent
|
|
1192
|
+
// sessions (untitled, shown as raw ids), so real chats fall off it. This
|
|
1193
|
+
// source lists every one of the user's own chats, under the workspace
|
|
1194
|
+
// titles the sidebar uses, and every chat with a readable name. Its rows
|
|
1195
|
+
// replace dsh's session rows in the menu; dsh's file rows stay.
|
|
1196
|
+
//
|
|
1197
|
+
// Picking a chat inserts dsh's own mention, so dsh attaches that chat
|
|
1198
|
+
// exactly as it does for its native rows. Picking a workspace inserts a
|
|
1199
|
+
// chip that reaches the model as [workspace "Name" — folder], which
|
|
1200
|
+
// session_search takes as its `workspace`.
|
|
1201
|
+
|
|
1202
|
+
const SOURCE = "dsh-memory-recall";
|
|
1203
|
+
const DRILL = /^ws:([0-9a-zA-Z-]+)\/(.*)$/;
|
|
1204
|
+
const WS_REF = "dmr-workspace:";
|
|
1205
|
+
const RECENT_HERE = 6;
|
|
1206
|
+
const PER_GROUP = 8;
|
|
1207
|
+
|
|
1208
|
+
/** base64url of the UTF-8 bytes, as Node's Buffer "base64url" writes it. */
|
|
1209
|
+
function base64url(text) {
|
|
1210
|
+
const bytes = new TextEncoder().encode(text);
|
|
1211
|
+
let binary = "";
|
|
1212
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
1213
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/** dsh-session-reference's canonical mention: @[label](dsh-session:<base64url(JSON(id))>). */
|
|
1217
|
+
function sessionMention(sessionId, label) {
|
|
1218
|
+
const escaped = String(label).replace(/[\\\]]/g, (m) => "\\" + m);
|
|
1219
|
+
return "@[" + escaped + "](dsh-session:" + base64url(JSON.stringify(sessionId)) + ")";
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function workspaceRef(ws) {
|
|
1223
|
+
return WS_REF + base64url(JSON.stringify({ title: ws.title, path: ws.path }));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function workspaceText(ref) {
|
|
1227
|
+
try {
|
|
1228
|
+
const b64 = ref.slice(WS_REF.length).replace(/-/g, "+").replace(/_/g, "/");
|
|
1229
|
+
const bytes = Uint8Array.from(atob(b64 + "===".slice((b64.length + 3) % 4)), (c) => c.charCodeAt(0));
|
|
1230
|
+
const ws = JSON.parse(new TextDecoder().decode(bytes));
|
|
1231
|
+
return "[workspace \"" + ws.title + "\" — " + ws.path + "]";
|
|
1232
|
+
} catch {
|
|
1233
|
+
return ref;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function ago(ms, now) {
|
|
1238
|
+
const s = Math.max(0, (now - ms) / 1000);
|
|
1239
|
+
if (s < 60) return "now";
|
|
1240
|
+
if (s < 3600) return Math.floor(s / 60) + "m";
|
|
1241
|
+
if (s < 86400) return Math.floor(s / 3600) + "h";
|
|
1242
|
+
if (s < 86400 * 30) return Math.floor(s / 86400) + "d";
|
|
1243
|
+
if (s < 86400 * 365) return Math.floor(s / (86400 * 30)) + "mo";
|
|
1244
|
+
return Math.floor(s / (86400 * 365)) + "y";
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** A folder with the home directory shortened, for a workspace row. */
|
|
1248
|
+
function shortPath(path) {
|
|
1249
|
+
return String(path || "").replace(/^[A-Za-z]:\\Users\\[^\\]+/, "~").replace(/^\/(?:home|Users)\/[^/]+/, "~");
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function createChatSource(ctx) {
|
|
1253
|
+
const cache = new Map(); // chat id → {at, data}
|
|
1254
|
+
let last = null; // the latest data, for the synchronous breadcrumb
|
|
1255
|
+
|
|
1256
|
+
async function load(sessionId, signal) {
|
|
1257
|
+
const hit = cache.get(sessionId);
|
|
1258
|
+
if (hit && Date.now() - hit.at < 10_000) return hit.data;
|
|
1259
|
+
const res = await fetch(BASE + "/chats?session=" + encodeURIComponent(sessionId || ""), { signal, headers: { accept: "application/json" } });
|
|
1260
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
1261
|
+
const data = await res.json();
|
|
1262
|
+
cache.set(sessionId, { at: Date.now(), data });
|
|
1263
|
+
last = data;
|
|
1264
|
+
return data;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function updatedAtOf(chat) {
|
|
1268
|
+
try {
|
|
1269
|
+
return ctx.get?.("sessions")?.list?.getSnapshot?.()?.byId?.[chat.sessionId]?.updatedAt ?? chat.createdAt;
|
|
1270
|
+
} catch {
|
|
1271
|
+
return chat.createdAt;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const chatRow = (chat, section, now) => {
|
|
1276
|
+
const label = chat.title || "Untitled chat";
|
|
1277
|
+
return {
|
|
1278
|
+
name: label,
|
|
1279
|
+
description: ago(updatedAtOf(chat), now),
|
|
1280
|
+
icon: "session",
|
|
1281
|
+
section,
|
|
1282
|
+
value: JSON.stringify({ kind: "chat", id: chat.sessionId, label }),
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
const workspaceRow = (ws, section) => ({
|
|
1286
|
+
name: ws.title,
|
|
1287
|
+
description: ws.chats.length + (ws.chats.length === 1 ? " chat" : " chats") + " · " + shortPath(ws.path),
|
|
1288
|
+
icon: "folder",
|
|
1289
|
+
section,
|
|
1290
|
+
drill: true,
|
|
1291
|
+
value: JSON.stringify({ kind: "workspace", id: ws.id, title: ws.title, path: ws.path }),
|
|
1292
|
+
});
|
|
1293
|
+
const matches = (text, q) => String(text || "").toLowerCase().includes(q);
|
|
1294
|
+
|
|
1295
|
+
function listing(data, query, now) {
|
|
1296
|
+
const current = data.workspaces.find((w) => w.id === data.currentWorkspaceId) ?? null;
|
|
1297
|
+
const others = data.workspaces.filter((w) => w !== current);
|
|
1298
|
+
const drilled = DRILL.exec(query);
|
|
1299
|
+
if (drilled) {
|
|
1300
|
+
const ws = data.workspaces.find((w) => w.id.startsWith(drilled[1]));
|
|
1301
|
+
if (!ws) return [];
|
|
1302
|
+
const q = drilled[2].trim().toLowerCase();
|
|
1303
|
+
return ws.chats.filter((c) => !q || matches(c.title, q)).map((c) => chatRow(c, ws.title, now));
|
|
1304
|
+
}
|
|
1305
|
+
const q = query.trim().toLowerCase();
|
|
1306
|
+
if (!q) {
|
|
1307
|
+
const rows = [];
|
|
1308
|
+
if (current) {
|
|
1309
|
+
rows.push(workspaceRow(current, "This workspace"));
|
|
1310
|
+
rows.push(...current.chats.slice(0, RECENT_HERE).map((c) => chatRow(c, "This workspace", now)));
|
|
1311
|
+
}
|
|
1312
|
+
rows.push(...others.map((w) => workspaceRow(w, "Workspaces")));
|
|
1313
|
+
if (data.ungrouped.length) rows.push(...data.ungrouped.slice(0, PER_GROUP).map((c) => chatRow(c, "No workspace", now)));
|
|
1314
|
+
return rows;
|
|
1315
|
+
}
|
|
1316
|
+
const rows = data.workspaces.filter((w) => matches(w.title, q) || matches(w.path, q)).map((w) => workspaceRow(w, "Workspaces"));
|
|
1317
|
+
for (const ws of current ? [current, ...others] : others) {
|
|
1318
|
+
const hits = ws.chats.filter((c) => matches(c.title, q));
|
|
1319
|
+
rows.push(...hits.slice(0, PER_GROUP).map((c) => chatRow(c, ws.title, now)));
|
|
1320
|
+
}
|
|
1321
|
+
rows.push(...data.ungrouped.filter((c) => matches(c.title, q)).slice(0, PER_GROUP).map((c) => chatRow(c, "No workspace", now)));
|
|
1322
|
+
return rows;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return {
|
|
1326
|
+
trigger: "@",
|
|
1327
|
+
name: SOURCE,
|
|
1328
|
+
order: 10,
|
|
1329
|
+
showGroupTitle: false,
|
|
1330
|
+
warm(session) {
|
|
1331
|
+
load(session.sessionId).catch(() => {});
|
|
1332
|
+
},
|
|
1333
|
+
async candidates(session, req) {
|
|
1334
|
+
if (readUi().atMenu === false) return [];
|
|
1335
|
+
if (req.quoted === true) return []; // a quoted @"path" is a file
|
|
1336
|
+
let data;
|
|
1337
|
+
try {
|
|
1338
|
+
data = await load(session.sessionId, req.signal);
|
|
1339
|
+
} catch {
|
|
1340
|
+
return [];
|
|
1341
|
+
}
|
|
1342
|
+
if (req.signal?.aborted) return [];
|
|
1343
|
+
return listing(data, String(req.query || ""), Date.now());
|
|
1344
|
+
},
|
|
1345
|
+
header(_session, req) {
|
|
1346
|
+
const drilled = DRILL.exec(String(req.query || ""));
|
|
1347
|
+
if (!drilled || !last) return undefined;
|
|
1348
|
+
const ws = last.workspaces.find((w) => w.id.startsWith(drilled[1]));
|
|
1349
|
+
if (!ws) return undefined;
|
|
1350
|
+
return [
|
|
1351
|
+
{ label: "All workspaces", value: JSON.stringify({ kind: "root" }) },
|
|
1352
|
+
{ label: ws.title, value: JSON.stringify({ kind: "workspace", id: ws.id, title: ws.title, path: ws.path }), current: true },
|
|
1353
|
+
];
|
|
1354
|
+
},
|
|
1355
|
+
onPick({ candidate, action }) {
|
|
1356
|
+
let v;
|
|
1357
|
+
try {
|
|
1358
|
+
v = JSON.parse(candidate.value);
|
|
1359
|
+
} catch {
|
|
1360
|
+
return undefined;
|
|
1361
|
+
}
|
|
1362
|
+
if (v.kind === "root") return { text: "@", continue: true };
|
|
1363
|
+
if (v.kind === "workspace" && action === "drill") return { text: "@ws:" + v.id.slice(0, 8) + "/", continue: true };
|
|
1364
|
+
if (v.kind === "workspace") {
|
|
1365
|
+
const ref = workspaceRef(v);
|
|
1366
|
+
return { insert: { source: SOURCE, ref, label: v.title, appearance: "folder", clipboardText: workspaceText(ref) } };
|
|
1367
|
+
}
|
|
1368
|
+
if (v.kind === "chat") {
|
|
1369
|
+
const mention = sessionMention(v.id, v.label);
|
|
1370
|
+
return { insert: { source: SOURCE, ref: mention, label: v.label, appearance: "session", clipboardText: mention } };
|
|
1371
|
+
}
|
|
1372
|
+
return undefined;
|
|
1373
|
+
},
|
|
1374
|
+
codec: {
|
|
1375
|
+
clipboardText: (ref) => (ref.startsWith(WS_REF) ? workspaceText(ref) : ref),
|
|
1376
|
+
serialize: (ref) => Promise.resolve(ref.startsWith(WS_REF) ? workspaceText(ref) : ref),
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Drop the session rows from dsh's own @ source, keeping its file rows —
|
|
1383
|
+
* this plugin's grouped chats replace them. The source registry offers no
|
|
1384
|
+
* way to disable one group, so the native source's `candidates` is wrapped,
|
|
1385
|
+
* and restored when this plugin unloads. If the native source is not
|
|
1386
|
+
* found (renamed, or not mounted), nothing is touched and both lists show.
|
|
1387
|
+
* @returns a disposer, or null when there was nothing to wrap yet.
|
|
1388
|
+
*/
|
|
1389
|
+
function hideNativeChats(inputTriggers) {
|
|
1390
|
+
const sources = inputTriggers?.live?.sources;
|
|
1391
|
+
const native = Array.isArray(sources) ? sources.find((s) => s && s.trigger === "@" && s.name === "reference") : undefined;
|
|
1392
|
+
if (!native || typeof native.candidates !== "function") return null;
|
|
1393
|
+
if (native.__dmrOriginal) return () => {};
|
|
1394
|
+
const original = native.candidates;
|
|
1395
|
+
native.__dmrOriginal = original;
|
|
1396
|
+
native.candidates = async function (...args) {
|
|
1397
|
+
const rows = await original.apply(this, args);
|
|
1398
|
+
if (readUi().atMenu === false) return rows; // switched off: dsh's own chat rows stay
|
|
1399
|
+
return (rows || []).filter((row) => {
|
|
1400
|
+
try {
|
|
1401
|
+
return JSON.parse(row.value).kind !== "session";
|
|
1402
|
+
} catch {
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
return () => {
|
|
1408
|
+
if (native.__dmrOriginal === original) {
|
|
1409
|
+
native.candidates = original;
|
|
1410
|
+
delete native.__dmrOriginal;
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/* ---- mounting ----------------------------------------------------------- */
|
|
1416
|
+
|
|
1417
|
+
const inject = ["slots"];
|
|
1418
|
+
|
|
1419
|
+
function apply(ctx) {
|
|
1420
|
+
const openChat = (sessionId) => {
|
|
1421
|
+
try {
|
|
1422
|
+
const sessions = ctx.get?.("sessions");
|
|
1423
|
+
if (sessions && typeof sessions.open === "function" && sessionId) sessions.open(sessionId);
|
|
1424
|
+
} catch { /* an unknown chat: nothing to open */ }
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// tool.call.toolview is a CHILD slot: it must go through slots.inject, or
|
|
1428
|
+
// registering before the parent's children table commits throws and takes
|
|
1429
|
+
// the whole plugin down. Keyed by tool name, each replaces dsh's own row.
|
|
1430
|
+
ctx.slots.inject("tool.call.toolview", () =>
|
|
1431
|
+
ctx.slots.register({ name: "tool.call.toolview", key: "memory" }, MemoryCall));
|
|
1432
|
+
const RecallCall = makeRecallCall(openChat);
|
|
1433
|
+
ctx.slots.inject("tool.call.toolview", () =>
|
|
1434
|
+
ctx.slots.register({ name: "tool.call.toolview", key: "session_search" }, RecallCall));
|
|
1435
|
+
|
|
1436
|
+
// sidebar.footer.action is a LIST slot: id + order. Scheduled is 20;
|
|
1437
|
+
// Memory sits just above it. The slot passes {wide}: false when the
|
|
1438
|
+
// sidebar is collapsed to its icon rail.
|
|
1439
|
+
ctx.slots.inject("sidebar.footer.action", () =>
|
|
1440
|
+
ctx.slots.register({ name: "sidebar.footer.action", id: "dsh-memory-recall", order: 15 }, (slotProps) => h(MemoryPanel, slotProps || {})));
|
|
1441
|
+
|
|
1442
|
+
// The memory context row, drawn like the other memory cards; every other
|
|
1443
|
+
// context row is handed to dsh's own renderer (see makeContextNode).
|
|
1444
|
+
const ContextNode = makeContextNode(ctx);
|
|
1445
|
+
ctx.slots.inject("conversation.chat.node", () =>
|
|
1446
|
+
ctx.slots.register({ name: "conversation.chat.node", key: "context", priority: NODE_PRIORITY, locale: "chat" }, ContextNode));
|
|
1447
|
+
|
|
1448
|
+
// The composer chip: is memory in this chat's prompt? A list slot in the
|
|
1449
|
+
// composer's trailing row, just left of the model name (graft's chip is 20).
|
|
1450
|
+
ctx.slots.inject("conversation.input.right", () =>
|
|
1451
|
+
ctx.slots.register({ name: "conversation.input.right", id: "dsh-memory-recall", order: 30, inject: (sessionId) => ({ sessionId }) }, MemoryChip));
|
|
1452
|
+
|
|
1453
|
+
// The @ source. Injected rather than required, so a composition without
|
|
1454
|
+
// the @ menu still loads the rest of the plugin.
|
|
1455
|
+
if (typeof ctx.inject === "function") {
|
|
1456
|
+
ctx.inject(["inputTriggers"], (scope) => {
|
|
1457
|
+
const triggers = scope.inputTriggers;
|
|
1458
|
+
const source = createChatSource(ctx);
|
|
1459
|
+
const warm = source.warm;
|
|
1460
|
+
let restore = hideNativeChats(triggers);
|
|
1461
|
+
// dsh's own source may register after this one: retry on first use.
|
|
1462
|
+
source.warm = (session) => {
|
|
1463
|
+
if (!restore) restore = hideNativeChats(triggers);
|
|
1464
|
+
warm(session);
|
|
1465
|
+
};
|
|
1466
|
+
scope.effect(() => triggers.registerSource(source), "dsh-memory-recall: @ chats by workspace");
|
|
1467
|
+
scope.effect(() => () => { if (restore) restore(); }, "dsh-memory-recall: restore dsh's @ chats");
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
exports.apply = apply;
|
|
1473
|
+
exports.inject = inject;
|
|
1474
|
+
exports.memoryStatus = memoryStatus;
|
|
1475
|
+
exports.changesOf = changesOf;
|
|
1476
|
+
exports.usageIn = usageIn;
|
|
1477
|
+
exports.recallStatus = recallStatus;
|
|
1478
|
+
exports.createChatSource = createChatSource;
|
|
1479
|
+
exports.sessionMention = sessionMention;
|
|
1480
|
+
exports.hideNativeChats = hideNativeChats;
|
|
1481
|
+
exports.workspaceText = workspaceText;
|
|
1482
|
+
exports.briefingView = briefingView;
|
|
1483
|
+
exports.parseMemoryText = parseMemoryText;
|
|
1484
|
+
exports.readUi = readUi;
|
|
1485
|
+
exports.writeUi = writeUi;
|
|
1486
|
+
exports.UI_SWITCHES = UI_SWITCHES;
|
|
1487
|
+
exports.makeContextNode = makeContextNode;
|
|
1488
|
+
return module.exports;
|
|
1489
|
+
},
|
|
1490
|
+
});
|