dsh-memory-palace 1.4.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,13 +1,13 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-memory-palace",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
9
-
10
- const NS = "memory-palace";
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-memory-palace",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
9
+
10
+ const NS = "memory-palace";
11
11
 
12
12
 
13
13
  const zh = {
@@ -38,6 +38,11 @@ window.__ModuleLoader__.load({
38
38
  memoryModeLabel: "记忆模式",
39
39
  memoryModeHintPlugin: "插件模式:记忆公民指令 + 轮次轻量 + 错误捕获。切换需重启 dsh 生效。",
40
40
  memoryModeHintSmart: "智能模式:LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md)。切换需重启 dsh 生效。",
41
+ memoryModeHintHybrid: "混合模式:记忆子代理自动维护今日日志(章节化、标删去重),agent 主动维护 MEMORY.md(章节化写入、整章节替换、门禁内全量重整)。切换需重启 dsh 生效。",
42
+ reorgCooldownLabel: "重整冷却(天)",
43
+ reorgCooldownHint: "项目级 MEMORY.md 全量重整的冷却天数(距上次重整);与「超出注入预算」双条件同时满足才允许 memory_reorganize。默认 7 天。",
44
+ subagentLogBudgetLabel: "子代理日志回喂预算(字符)",
45
+ subagentLogBudgetHint: "记忆子代理回喂今日工作日志的字符上限;超出时仅回喂章节目录,子代理按需读取章节。默认 20000。",
41
46
  summaryModelLabel: "摘要模型(智能模式)",
42
47
  summaryModelHint: "留空=复用当前会话的 provider/model;也可固定廉价小模型省 token。",
43
48
  save: "保存",
@@ -104,6 +109,11 @@ window.__ModuleLoader__.load({
104
109
  memoryModeLabel: "Memory mode",
105
110
  memoryModeHintPlugin: "Plugin mode: memory-citizen instructions + turn-end light entries + error capture. Switching requires a dsh restart.",
106
111
  memoryModeHintSmart: "Smart mode: LLM conversation summarization (summary -> daily log + durable -> MEMORY.md). Switching requires a dsh restart.",
112
+ memoryModeHintHybrid: "Hybrid mode: a memory sub-agent auto-maintains today's daily log (chaptered, dedupe by strikethrough); the agent proactively maintains MEMORY.md (chaptered writes, section replace, gated full reorganize). Switching requires a dsh restart.",
113
+ reorgCooldownLabel: "Reorganize cooldown (days)",
114
+ reorgCooldownHint: "Cooldown days between full reorganizes of the project MEMORY.md; combined with 'over injection budget' both gates must pass for memory_reorganize. Default 7.",
115
+ subagentLogBudgetLabel: "Sub-agent log feed budget (chars)",
116
+ subagentLogBudgetHint: "Max chars of today's daily log fed back to the memory sub-agent; beyond this only the chapter TOC is fed and the sub-agent reads sections on demand. Default 20000.",
107
117
  summaryModelLabel: "Summary model (smart mode)",
108
118
  summaryModelHint: "Leave empty to reuse the current session's provider/model, or pin a cheap model (provider/model) to save tokens.",
109
119
  save: "Save",
@@ -204,374 +214,385 @@ window.__ModuleLoader__.load({
204
214
  }
205
215
 
206
216
 
207
- function MemoryPalaceSection(props) {
208
- const { t, getConnection } = props;
209
- const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
210
- const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
211
- const [dirty, setDirty] = react.useState(false);
212
- const [saving, setSaving] = react.useState(false);
213
- const [failed, setFailed] = react.useState(false);
214
- const [validation, setValidation] = react.useState(null);
215
- const [modelOptions, setModelOptions] = react.useState([]);
216
-
217
- // 初始加载(从自有 route 读真实值,绕过 apiproxy allowlist)。
218
- react.useEffect(() => {
219
- let alive = true;
220
- (async () => {
221
- try {
222
- const full = await apiGetFull();
223
- if (alive) {
224
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
225
- setDraft(projectDraft({ value: full.value }));
226
- }
227
- } catch (e) {
228
- if (alive) setRemote((r) => ({ ...r, loaded: true }));
229
- }
230
- })();
231
- return () => { alive = false; };
232
- }, []);
233
-
234
- // 外部变更后校准 draft(仅未编辑时)。
235
- react.useEffect(() => {
236
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
237
- }, [remote, dirty]);
238
-
239
- react.useEffect(() => {
240
- let alive = true;
241
- (async () => {
242
- try {
243
- const conn = getConnection && getConnection();
244
- if (!conn || !conn.api || !conn.api.llm || typeof conn.api.llm.models !== "function") return;
245
- const r = await conn.api.llm.models({});
246
- const groups = (r && r.result && r.result.value && r.result.value.groups) || (r && r.groups) || [];
247
- const opts = [];
248
- for (const g of groups || []) {
249
- for (const m of (g && g.models) || []) {
250
- // m.id 已经包含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
251
- opts.push({ value: m.id, label: g.name + " / " + m.name });
252
- }
253
- }
254
- if (alive) setModelOptions(opts);
255
- } catch (e) {
256
- /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
257
- }
258
- })();
259
- return () => { alive = false; };
260
- }, []);
261
-
262
- react.useEffect(() => {
263
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
264
- }, [remote, dirty]);
265
-
266
- const disabled = !remote.loaded || saving;
267
-
268
- function edit(field, text) {
269
- setDraft((d) => ({ ...d, [field]: text }));
270
- setDirty(true);
271
- setFailed(false);
272
- if (FIELD_NUMERIC.has(field)) {
273
- const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
274
- setValidation((v) => (valid ? null : field));
275
- }
276
- }
277
-
278
- function resetField(field) {
279
- setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
280
- setDirty(true);
281
- setFailed(false);
282
- if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
283
- }
284
-
285
- async function save() {
286
- setSaving(true);
287
- setFailed(false);
288
- try {
289
- // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
290
- const section = {};
291
- for (const field of Object.keys(draft)) {
292
- const text = draft[field].trim();
293
- if (FIELD_BOOL.has(field)) {
294
- section[field] = text === "true";
295
- } else if (FIELD_NUMERIC.has(field)) {
296
- if (text !== "") {
297
- const n = Number(text);
298
- section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
299
- }
300
- } else if (field === "buddyWorkspaceMemoryDirs") {
301
- section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
302
- } else {
303
- if (text !== "") section[field] = text;
304
- }
305
- }
306
- const full = await apiUpdate(section, remote.revision);
307
- // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
308
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
309
- setDirty(false);
310
- } catch (e) {
311
- setFailed(true);
312
- } finally {
313
- setSaving(false);
314
- }
315
- }
316
-
317
- function discard() {
318
- setDraft(projectDraft(remote));
319
- setDirty(false);
320
- setFailed(false);
321
- setValidation(null);
322
- }
323
-
324
- const h = react.createElement;
325
-
326
- const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
327
- const isOverridden = (field) => field in userLayer;
328
-
329
- // 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
330
- const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
331
- const toggle = (label, hint, field, disabledOverride) =>
332
- h("div", { style: toggleStyle }, [
333
- h("div", { style: toggleLabelBoxStyle }, [
334
- h("p", { style: { ...labelStyle, margin: 0 } }, label),
335
- h("p", { style: hintStyle }, hint)
336
- ]),
337
- h("input", {
338
- type: "checkbox",
339
- checked: draft[field] === "true",
340
- disabled: disabled || disabledOverride,
341
- style: checkboxStyle,
342
- onChange: (e) => edit(field, e.target.checked ? "true" : "false")
343
- })
344
- ]);
345
-
346
- const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
347
- h("div", { style: headStyle }, [
348
- h("label", { style: labelStyle }, label),
349
- dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
350
- !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
351
- ]),
352
- control,
353
- hint ? h("p", { style: hintStyle }, hint) : null
354
- ]);
355
-
356
- const inputStyle = {
357
- boxSizing: "border-box",
358
- width: "100%",
359
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
360
- background: "var(--dsw-alias-bg-layer-3, #fafafa)",
361
- height: "34px",
362
- font: "inherit",
363
- color: "var(--dsw-alias-label-primary, #1f2329)",
364
- borderRadius: "8px",
365
- padding: "0 12px",
366
- fontSize: "13px",
367
- lineHeight: "1.5"
368
- };
369
- const selectStyle = {
370
- ...inputStyle,
371
- height: "34px",
372
- appearance: "none",
373
- WebkitAppearance: "none",
374
- backgroundImage: "url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E)",
375
- backgroundPosition: "right 12px center",
376
- backgroundRepeat: "no-repeat",
377
- backgroundSize: "12px 12px",
378
- paddingRight: "32px"
379
- };
380
- const checkboxStyle = {
381
- width: "18px",
382
- height: "18px",
383
- margin: "0",
384
- accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
385
- // 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
386
- flexShrink: 0,
387
- flexGrow: 0,
388
- alignSelf: "center"
389
- };
390
- const toggleStyle = {
391
- display: "flex",
392
- alignItems: "center",
393
- justifyContent: "space-between",
394
- gap: "12px",
395
- padding: "12px 0",
396
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
397
- };
398
-
399
- const cardStyle = {
400
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
401
- borderRadius: "12px",
402
- padding: "14px 16px",
403
- marginBottom: "12px",
404
- background: "var(--dsw-alias-bg-layer-2, #ffffff)"
405
- };
406
- const groupTitleStyle = {
407
- margin: "0 0 4px",
408
- fontSize: "13px",
409
- fontWeight: "500",
410
- color: "var(--dsw-alias-label-primary, #1f2329)"
411
- };
412
- const sectionStyle = {
413
- maxWidth: "760px",
414
- color: "var(--dsw-alias-label-primary, #1f2329)",
415
- display: "flex",
416
- flexDirection: "column",
417
- gap: "12px"
418
- };
419
- const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
420
- const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
421
- const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
422
- const btnPrimary = {
423
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
424
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
425
- color: "var(--dsw-alias-label-primary, #1f2329)",
426
- borderRadius: "8px",
427
- padding: "6px 14px",
428
- fontSize: "13px",
429
- cursor: disabled ? "default" : "pointer",
430
- opacity: disabled ? 0.5 : 1
431
- };
432
- const btnAccent = {
433
- border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
434
- background: "var(--dsw-alias-brand-primary, #4b5bff)",
435
- color: "#fff",
436
- borderRadius: "8px",
437
- padding: "6px 14px",
438
- fontSize: "13px",
439
- cursor: disabled ? "default" : "pointer",
440
- opacity: disabled ? 0.5 : 1
441
- };
442
- const fieldStyle = {
443
- display: "flex",
444
- flexDirection: "column",
445
- gap: "6px",
446
- padding: "12px 0",
447
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
448
- };
449
- const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
450
- const labelStyle = {
451
- minWidth: "0",
452
- color: "var(--dsw-alias-label-primary, #1f2329)",
453
- flex: 1,
454
- fontSize: "13px",
455
- fontWeight: "500",
456
- lineHeight: "1.5"
457
- };
458
- const badgeStyle = {
459
- whiteSpace: "nowrap",
460
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
461
- color: "var(--dsw-alias-label-secondary, #6b7280)",
462
- borderRadius: "999px",
463
- padding: "1px 8px",
464
- fontSize: "11px",
465
- fontWeight: "500",
466
- lineHeight: "17px"
467
- };
468
- const mutedBadgeStyle = {
469
- whiteSpace: "nowrap",
470
- color: "var(--dsw-alias-label-tertiary, #8a919f)",
471
- borderRadius: "999px",
472
- padding: "1px 8px",
473
- fontSize: "11px",
474
- lineHeight: "17px"
475
- };
476
- const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
477
- const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
478
-
479
- const num = (field) => ({
480
- type: "text",
481
- inputMode: "numeric",
482
- value: draft[field],
483
- disabled,
484
- style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
485
- onChange: (e) => edit(field, e.target.value)
486
- });
487
-
488
- return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
489
- // 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
490
- h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
491
- h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
492
- h("div", null, [
493
- h("h3", { style: headingStyle }, t("title")),
494
- h("p", { style: introStyle }, t("intro"))
495
- ]),
496
- h("div", { style: actionsStyle }, [
497
- h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
498
- h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
499
- ])
500
- ]),
501
- failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
502
- h("div", { style: cardStyle }, [
503
- h("p", { style: groupTitleStyle }, t("core")),
504
- h("div", { style: toggleStyle }, [
505
- h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
506
- h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
507
- h("p", { style: hintStyle }, t("bridgeHint"))
508
- ]),
509
- h("input", {
510
- type: "checkbox",
511
- checked: draft.bridgeBuddyMemory === "true",
512
- disabled,
513
- style: checkboxStyle,
514
- onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
515
- })
516
- ]),
517
- row(t("dirsLabel"), t("dirsHint"),
518
- h("input", {
519
- ...num("buddyWorkspaceMemoryDirs"),
520
- type: "text",
521
- placeholder: ".workbuddy/memory, .codebuddy/memory"
522
- }), "buddyWorkspaceMemoryDirs")
523
- ]),
524
- h("div", { style: cardStyle }, [
525
- h("p", { style: groupTitleStyle }, t("smartSummary")),
526
- // 轮次结束自动记录(summarize):两种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
527
- toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
528
- row(t("memoryModeLabel"), draft.memoryMode === "smart" ? t("memoryModeHintSmart") : t("memoryModeHintPlugin"),
529
- h("select", {
530
- value: draft.memoryMode === "smart" ? "smart" : "plugin",
531
- disabled,
532
- onChange: (e) => edit("memoryMode", e.target.value),
533
- style: selectStyle
534
- }, [
535
- h("option", { value: "plugin" }, "插件模式"),
536
- h("option", { value: "smart" }, "智能模式")
537
- ]), "memoryMode"),
538
- draft.memoryMode === "smart"
539
- ? h("div", null, [
540
- row(t("summaryModelLabel"), t("summaryModelHint"),
541
- h("select", {
542
- // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
543
- // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
544
- value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
545
- disabled,
546
- style: selectStyle,
547
- onChange: (e) => edit("summaryModel", e.target.value)
548
- }, [
549
- h("option", { value: "" }, "复用当前会话模型"),
550
- ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
551
- ]), "summaryModel"),
552
- toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
553
- row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
554
- row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
555
- ])
556
- : h("div", null, [
557
- toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
558
- ])
559
- ]),
560
- h("div", { style: cardStyle }, [
561
- h("p", { style: groupTitleStyle }, t("storage")),
562
- row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
563
- row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
564
- row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
565
- row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
566
- row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
567
- ]),
568
- h("div", { style: cardStyle }, [
569
- h("p", { style: groupTitleStyle }, t("dev")),
570
- row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
571
- toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
572
- ])
573
- ]);
574
- }
217
+ function MemoryPalaceSection(props) {
218
+ const { t, getConnection } = props;
219
+ const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
220
+ const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
221
+ const [dirty, setDirty] = react.useState(false);
222
+ const [saving, setSaving] = react.useState(false);
223
+ const [failed, setFailed] = react.useState(false);
224
+ const [validation, setValidation] = react.useState(null);
225
+ const [modelOptions, setModelOptions] = react.useState([]);
226
+
227
+ // 初始加载(从自有 route 读真实值,绕过 apiproxy allowlist)。
228
+ react.useEffect(() => {
229
+ let alive = true;
230
+ (async () => {
231
+ try {
232
+ const full = await apiGetFull();
233
+ if (alive) {
234
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
235
+ setDraft(projectDraft({ value: full.value }));
236
+ }
237
+ } catch (e) {
238
+ if (alive) setRemote((r) => ({ ...r, loaded: true }));
239
+ }
240
+ })();
241
+ return () => { alive = false; };
242
+ }, []);
243
+
244
+ // 外部变更后校准 draft(仅未编辑时)。
245
+ react.useEffect(() => {
246
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
247
+ }, [remote, dirty]);
248
+
249
+ react.useEffect(() => {
250
+ let alive = true;
251
+ (async () => {
252
+ try {
253
+ const conn = getConnection && getConnection();
254
+ if (!conn || !conn.api || !conn.api.llm || typeof conn.api.llm.models !== "function") return;
255
+ const r = await conn.api.llm.models({});
256
+ const groups = (r && r.result && r.result.value && r.result.value.groups) || (r && r.groups) || [];
257
+ const opts = [];
258
+ for (const g of groups || []) {
259
+ for (const m of (g && g.models) || []) {
260
+ // m.id 已经包含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
261
+ opts.push({ value: m.id, label: g.name + " / " + m.name });
262
+ }
263
+ }
264
+ if (alive) setModelOptions(opts);
265
+ } catch (e) {
266
+ /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
267
+ }
268
+ })();
269
+ return () => { alive = false; };
270
+ }, []);
271
+
272
+ react.useEffect(() => {
273
+ if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
274
+ }, [remote, dirty]);
275
+
276
+ const disabled = !remote.loaded || saving;
277
+
278
+ function edit(field, text) {
279
+ setDraft((d) => ({ ...d, [field]: text }));
280
+ setDirty(true);
281
+ setFailed(false);
282
+ if (FIELD_NUMERIC.has(field)) {
283
+ const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
284
+ setValidation((v) => (valid ? null : field));
285
+ }
286
+ }
287
+
288
+ function resetField(field) {
289
+ setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
290
+ setDirty(true);
291
+ setFailed(false);
292
+ if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
293
+ }
294
+
295
+ async function save() {
296
+ setSaving(true);
297
+ setFailed(false);
298
+ try {
299
+ // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
300
+ const section = {};
301
+ for (const field of Object.keys(draft)) {
302
+ const text = draft[field].trim();
303
+ if (FIELD_BOOL.has(field)) {
304
+ section[field] = text === "true";
305
+ } else if (FIELD_NUMERIC.has(field)) {
306
+ if (text !== "") {
307
+ const n = Number(text);
308
+ section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
309
+ }
310
+ } else if (field === "buddyWorkspaceMemoryDirs") {
311
+ section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
312
+ } else {
313
+ if (text !== "") section[field] = text;
314
+ }
315
+ }
316
+ const full = await apiUpdate(section, remote.revision);
317
+ // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
318
+ setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
319
+ setDirty(false);
320
+ } catch (e) {
321
+ setFailed(true);
322
+ } finally {
323
+ setSaving(false);
324
+ }
325
+ }
326
+
327
+ function discard() {
328
+ setDraft(projectDraft(remote));
329
+ setDirty(false);
330
+ setFailed(false);
331
+ setValidation(null);
332
+ }
333
+
334
+ const h = react.createElement;
335
+
336
+ const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
337
+ const isOverridden = (field) => field in userLayer;
338
+
339
+ // 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
340
+ const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
341
+ const toggle = (label, hint, field, disabledOverride) =>
342
+ h("div", { style: toggleStyle }, [
343
+ h("div", { style: toggleLabelBoxStyle }, [
344
+ h("p", { style: { ...labelStyle, margin: 0 } }, label),
345
+ h("p", { style: hintStyle }, hint)
346
+ ]),
347
+ h("input", {
348
+ type: "checkbox",
349
+ checked: draft[field] === "true",
350
+ disabled: disabled || disabledOverride,
351
+ style: checkboxStyle,
352
+ onChange: (e) => edit(field, e.target.checked ? "true" : "false")
353
+ })
354
+ ]);
355
+
356
+ const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
357
+ h("div", { style: headStyle }, [
358
+ h("label", { style: labelStyle }, label),
359
+ dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
360
+ !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
361
+ ]),
362
+ control,
363
+ hint ? h("p", { style: hintStyle }, hint) : null
364
+ ]);
365
+
366
+ const inputStyle = {
367
+ boxSizing: "border-box",
368
+ width: "100%",
369
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
370
+ background: "var(--dsw-alias-bg-layer-3, #fafafa)",
371
+ height: "34px",
372
+ font: "inherit",
373
+ color: "var(--dsw-alias-label-primary, #1f2329)",
374
+ borderRadius: "8px",
375
+ padding: "0 12px",
376
+ fontSize: "13px",
377
+ lineHeight: "1.5"
378
+ };
379
+ const selectStyle = {
380
+ ...inputStyle,
381
+ height: "34px",
382
+ appearance: "none",
383
+ WebkitAppearance: "none",
384
+ backgroundImage: "url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E)",
385
+ backgroundPosition: "right 12px center",
386
+ backgroundRepeat: "no-repeat",
387
+ backgroundSize: "12px 12px",
388
+ paddingRight: "32px"
389
+ };
390
+ const checkboxStyle = {
391
+ width: "18px",
392
+ height: "18px",
393
+ margin: "0",
394
+ accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
395
+ // 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
396
+ flexShrink: 0,
397
+ flexGrow: 0,
398
+ alignSelf: "center"
399
+ };
400
+ const toggleStyle = {
401
+ display: "flex",
402
+ alignItems: "center",
403
+ justifyContent: "space-between",
404
+ gap: "12px",
405
+ padding: "12px 0",
406
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
407
+ };
408
+
409
+ const cardStyle = {
410
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
411
+ borderRadius: "12px",
412
+ padding: "14px 16px",
413
+ marginBottom: "12px",
414
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)"
415
+ };
416
+ const groupTitleStyle = {
417
+ margin: "0 0 4px",
418
+ fontSize: "13px",
419
+ fontWeight: "500",
420
+ color: "var(--dsw-alias-label-primary, #1f2329)"
421
+ };
422
+ const sectionStyle = {
423
+ maxWidth: "760px",
424
+ color: "var(--dsw-alias-label-primary, #1f2329)",
425
+ display: "flex",
426
+ flexDirection: "column",
427
+ gap: "12px"
428
+ };
429
+ const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
430
+ const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
431
+ const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
432
+ const btnPrimary = {
433
+ border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
434
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
435
+ color: "var(--dsw-alias-label-primary, #1f2329)",
436
+ borderRadius: "8px",
437
+ padding: "6px 14px",
438
+ fontSize: "13px",
439
+ cursor: disabled ? "default" : "pointer",
440
+ opacity: disabled ? 0.5 : 1
441
+ };
442
+ const btnAccent = {
443
+ border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
444
+ background: "var(--dsw-alias-brand-primary, #4b5bff)",
445
+ color: "#fff",
446
+ borderRadius: "8px",
447
+ padding: "6px 14px",
448
+ fontSize: "13px",
449
+ cursor: disabled ? "default" : "pointer",
450
+ opacity: disabled ? 0.5 : 1
451
+ };
452
+ const fieldStyle = {
453
+ display: "flex",
454
+ flexDirection: "column",
455
+ gap: "6px",
456
+ padding: "12px 0",
457
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
458
+ };
459
+ const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
460
+ const labelStyle = {
461
+ minWidth: "0",
462
+ color: "var(--dsw-alias-label-primary, #1f2329)",
463
+ flex: 1,
464
+ fontSize: "13px",
465
+ fontWeight: "500",
466
+ lineHeight: "1.5"
467
+ };
468
+ const badgeStyle = {
469
+ whiteSpace: "nowrap",
470
+ background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
471
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
472
+ borderRadius: "999px",
473
+ padding: "1px 8px",
474
+ fontSize: "11px",
475
+ fontWeight: "500",
476
+ lineHeight: "17px"
477
+ };
478
+ const mutedBadgeStyle = {
479
+ whiteSpace: "nowrap",
480
+ color: "var(--dsw-alias-label-tertiary, #8a919f)",
481
+ borderRadius: "999px",
482
+ padding: "1px 8px",
483
+ fontSize: "11px",
484
+ lineHeight: "17px"
485
+ };
486
+ const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
487
+ const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
488
+
489
+ const num = (field) => ({
490
+ type: "text",
491
+ inputMode: "numeric",
492
+ value: draft[field],
493
+ disabled,
494
+ style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
495
+ onChange: (e) => edit(field, e.target.value)
496
+ });
497
+
498
+ return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
499
+ // 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
500
+ h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
501
+ h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
502
+ h("div", null, [
503
+ h("h3", { style: headingStyle }, t("title")),
504
+ h("p", { style: introStyle }, t("intro"))
505
+ ]),
506
+ h("div", { style: actionsStyle }, [
507
+ h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
508
+ h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
509
+ ])
510
+ ]),
511
+ failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
512
+ h("div", { style: cardStyle }, [
513
+ h("p", { style: groupTitleStyle }, t("core")),
514
+ h("div", { style: toggleStyle }, [
515
+ h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
516
+ h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
517
+ h("p", { style: hintStyle }, t("bridgeHint"))
518
+ ]),
519
+ h("input", {
520
+ type: "checkbox",
521
+ checked: draft.bridgeBuddyMemory === "true",
522
+ disabled,
523
+ style: checkboxStyle,
524
+ onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
525
+ })
526
+ ]),
527
+ row(t("dirsLabel"), t("dirsHint"),
528
+ h("input", {
529
+ ...num("buddyWorkspaceMemoryDirs"),
530
+ type: "text",
531
+ placeholder: ".workbuddy/memory, .codebuddy/memory"
532
+ }), "buddyWorkspaceMemoryDirs")
533
+ ]),
534
+ h("div", { style: cardStyle }, [
535
+ h("p", { style: groupTitleStyle }, t("smartSummary")),
536
+ // 轮次结束自动记录(summarize):三种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
537
+ toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
538
+ row(t("memoryModeLabel"),
539
+ draft.memoryMode === "smart"
540
+ ? t("memoryModeHintSmart")
541
+ : draft.memoryMode === "hybrid"
542
+ ? t("memoryModeHintHybrid")
543
+ : t("memoryModeHintPlugin"),
544
+ h("select", {
545
+ value: ["plugin", "smart", "hybrid"].includes(draft.memoryMode) ? draft.memoryMode : "plugin",
546
+ disabled,
547
+ onChange: (e) => edit("memoryMode", e.target.value),
548
+ style: selectStyle
549
+ }, [
550
+ h("option", { value: "plugin" }, "插件模式"),
551
+ h("option", { value: "smart" }, "智能模式"),
552
+ h("option", { value: "hybrid" }, "混合模式")
553
+ ]), "memoryMode"),
554
+ draft.memoryMode === "smart"
555
+ ? h("div", null, [
556
+ row(t("summaryModelLabel"), t("summaryModelHint"),
557
+ h("select", {
558
+ // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
559
+ // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
560
+ value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
561
+ disabled,
562
+ style: selectStyle,
563
+ onChange: (e) => edit("summaryModel", e.target.value)
564
+ }, [
565
+ h("option", { value: "" }, "复用当前会话模型"),
566
+ ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
567
+ ]), "summaryModel"),
568
+ toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
569
+ row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
570
+ row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
571
+ ])
572
+ : draft.memoryMode === "hybrid"
573
+ ? h("div", null, [
574
+ row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
575
+ row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
576
+ ])
577
+ : h("div", null, [
578
+ toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
579
+ ])
580
+ ]),
581
+ h("div", { style: cardStyle }, [
582
+ h("p", { style: groupTitleStyle }, t("storage")),
583
+ row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
584
+ row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
585
+ row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
586
+ row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
587
+ row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
588
+ ]),
589
+ h("div", { style: cardStyle }, [
590
+ h("p", { style: groupTitleStyle }, t("dev")),
591
+ row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
592
+ toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
593
+ ])
594
+ ]);
595
+ }
575
596
 
576
597
 
577
598
  // ---- v1.2.0 会话标题栏「记忆」按钮(蒸馏入口)----
@@ -833,39 +854,39 @@ window.__ModuleLoader__.load({
833
854
  }
834
855
 
835
856
 
836
- function apply(ctx) {
837
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
838
- const t = ctx.locale.bind(NS);
839
- // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
840
- // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
841
- const controller = new MemoryPalaceController(ctx);
842
- // v1.1.0:枚举已配置模型供「摘要模型」下拉(connection 服务,枚举失败则降级为空列表)。
843
- const getConnection = () => {
844
- try {
845
- return ctx.get("connection");
846
- } catch {
847
- return null;
848
- }
849
- };
850
- ctx.slots.inject("settings.section", () => ctx.slots.register({
851
- name: "settings.section",
852
- id: "memory-palace",
853
- order: 20,
854
- label: () => t("nav"),
855
- locale: NS,
856
- inject: () => controller.inject()
857
- }, (props) => MemoryPalaceSection({ ...props, getConnection })));
858
- // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
859
- ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
860
- name: "conversation.session.header.utilities",
861
- id: "memory-palace-distill",
862
- order: -1,
863
- locale: NS
864
- }, DistillButton));
865
- }
866
-
867
- exports.apply = apply;
868
- exports.inject = ["slots", "locale", "connection"];
869
- return module.exports;
870
- }
871
- });
857
+ function apply(ctx) {
858
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
859
+ const t = ctx.locale.bind(NS);
860
+ // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
861
+ // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
862
+ const controller = new MemoryPalaceController(ctx);
863
+ // v1.1.0:枚举已配置模型供「摘要模型」下拉(connection 服务,枚举失败则降级为空列表)。
864
+ const getConnection = () => {
865
+ try {
866
+ return ctx.get("connection");
867
+ } catch {
868
+ return null;
869
+ }
870
+ };
871
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
872
+ name: "settings.section",
873
+ id: "memory-palace",
874
+ order: 20,
875
+ label: () => t("nav"),
876
+ locale: NS,
877
+ inject: () => controller.inject()
878
+ }, (props) => MemoryPalaceSection({ ...props, getConnection })));
879
+ // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
880
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
881
+ name: "conversation.session.header.utilities",
882
+ id: "memory-palace-distill",
883
+ order: -1,
884
+ locale: NS
885
+ }, DistillButton));
886
+ }
887
+
888
+ exports.apply = apply;
889
+ exports.inject = ["slots", "locale", "connection"];
890
+ return module.exports;
891
+ }
892
+ });