dsh-memory-palace 1.6.0 → 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.
Files changed (4) hide show
  1. package/README.md +181 -184
  2. package/lib/client.js +425 -433
  3. package/lib/index.js +356 -363
  4. package/package.json +56 -56
package/lib/client.js CHANGED
@@ -1,14 +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
- // v1.6.0-rc1:移除 require("@deepseek-ai/dsh-client-runtime/client")——该包在
9
- // DSH 0.1.2-alpha.2 已整体删除(原变量赋值后零使用),平台 seed 表的 react 足够。
10
-
11
- 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";
12
11
 
13
12
 
14
13
  const zh = {
@@ -215,397 +214,385 @@ window.__ModuleLoader__.load({
215
214
  }
216
215
 
217
216
 
218
- // v1.6.0-rc1:模型枚举改走 ctx.remote.session.modelCatalog()——ApiProxy 包已在
219
- // DSH 0.1.2-alpha.2 删除(旧 conn.api.llm.models 死了)。服务缺席 / 非 ok / 异常
220
- // 一律降级为空列表,下拉只剩「复用当前会话模型」,UX 不变。
221
- async function fetchModelOptions(ctx) {
222
- try {
223
- const remote = ctx && ctx.remote;
224
- if (!remote || !remote.session || typeof remote.session.modelCatalog !== "function") return [];
225
- const response = await remote.session.modelCatalog();
226
- if (!response || !response.ok || !response.value) return [];
227
- const opts = [];
228
- for (const g of response.value.groups || []) {
229
- for (const m of (g && g.models) || []) {
230
- // m.id 已含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
231
- opts.push({ value: m.id, label: (g.name || g.id) + " / " + (m.name || m.id) });
232
- }
233
- }
234
- return opts;
235
- } catch (e) {
236
- return [];
237
- }
238
- }
239
-
240
- function MemoryPalaceSection(props) {
241
- const { t, fetchModels } = props;
242
- const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
243
- const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
244
- const [dirty, setDirty] = react.useState(false);
245
- const [saving, setSaving] = react.useState(false);
246
- const [failed, setFailed] = react.useState(false);
247
- const [validation, setValidation] = react.useState(null);
248
- const [modelOptions, setModelOptions] = react.useState([]);
249
-
250
- // 初始加载(从自有 route 读真实值)。
251
- react.useEffect(() => {
252
- let alive = true;
253
- (async () => {
254
- try {
255
- const full = await apiGetFull();
256
- if (alive) {
257
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
258
- setDraft(projectDraft({ value: full.value }));
259
- }
260
- } catch (e) {
261
- if (alive) setRemote((r) => ({ ...r, loaded: true }));
262
- }
263
- })();
264
- return () => { alive = false; };
265
- }, []);
266
-
267
- // 外部变更后校准 draft(仅未编辑时)。
268
- react.useEffect(() => {
269
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
270
- }, [remote, dirty]);
271
-
272
- react.useEffect(() => {
273
- let alive = true;
274
- (async () => {
275
- try {
276
- const opts = await (fetchModels && fetchModels());
277
- if (alive) setModelOptions(Array.isArray(opts) ? opts : []);
278
- } catch (e) {
279
- /* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
280
- }
281
- })();
282
- return () => { alive = false; };
283
- }, []);
284
-
285
- react.useEffect(() => {
286
- if (remote.loaded && remote.value && !dirty) setDraft(projectDraft(remote));
287
- }, [remote, dirty]);
288
-
289
- const disabled = !remote.loaded || saving;
290
-
291
- function edit(field, text) {
292
- setDraft((d) => ({ ...d, [field]: text }));
293
- setDirty(true);
294
- setFailed(false);
295
- if (FIELD_NUMERIC.has(field)) {
296
- const valid = text === "" || (Number.isFinite(Number(text)) && String(Number(text)) === text.trim());
297
- setValidation((v) => (valid ? null : field));
298
- }
299
- }
300
-
301
- function resetField(field) {
302
- setDraft((d) => ({ ...d, [field]: projectDraft(remote)[field] }));
303
- setDirty(true);
304
- setFailed(false);
305
- if (FIELD_NUMERIC.has(field)) setValidation((v) => (v === field ? null : v));
306
- }
307
-
308
- async function save() {
309
- setSaving(true);
310
- setFailed(false);
311
- try {
312
- // 整节替换(replace 语义):空值字段省略 → 自动回退 base/schema 默认。
313
- const section = {};
314
- for (const field of Object.keys(draft)) {
315
- const text = draft[field].trim();
316
- if (FIELD_BOOL.has(field)) {
317
- section[field] = text === "true";
318
- } else if (FIELD_NUMERIC.has(field)) {
319
- if (text !== "") {
320
- const n = Number(text);
321
- section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
322
- }
323
- } else if (field === "buddyWorkspaceMemoryDirs") {
324
- section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
325
- } else {
326
- if (text !== "") section[field] = text;
327
- }
328
- }
329
- const full = await apiUpdate(section, remote.revision);
330
- // 保存成功后同步 remote(真实返回值),保留当前 draft(= 用户保存的目标值)。
331
- setRemote({ value: full.value, user: full.user, revision: full.revision, loaded: true });
332
- setDirty(false);
333
- } catch (e) {
334
- setFailed(true);
335
- } finally {
336
- setSaving(false);
337
- }
338
- }
339
-
340
- function discard() {
341
- setDraft(projectDraft(remote));
342
- setDirty(false);
343
- setFailed(false);
344
- setValidation(null);
345
- }
346
-
347
- const h = react.createElement;
348
-
349
- const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
350
- const isOverridden = (field) => field in userLayer;
351
-
352
- // 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
353
- const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
354
- const toggle = (label, hint, field, disabledOverride) =>
355
- h("div", { style: toggleStyle }, [
356
- h("div", { style: toggleLabelBoxStyle }, [
357
- h("p", { style: { ...labelStyle, margin: 0 } }, label),
358
- h("p", { style: hintStyle }, hint)
359
- ]),
360
- h("input", {
361
- type: "checkbox",
362
- checked: draft[field] === "true",
363
- disabled: disabled || disabledOverride,
364
- style: checkboxStyle,
365
- onChange: (e) => edit(field, e.target.checked ? "true" : "false")
366
- })
367
- ]);
368
-
369
- const row = (label, hint, control, field) => h("div", { style: fieldStyle }, [
370
- h("div", { style: headStyle }, [
371
- h("label", { style: labelStyle }, label),
372
- dirty && field ? h("span", { style: mutedBadgeStyle }, t("unsaved")) : null,
373
- !dirty && field && isOverridden(field) ? h("span", { style: badgeStyle }, t("overridden")) : null
374
- ]),
375
- control,
376
- hint ? h("p", { style: hintStyle }, hint) : null
377
- ]);
378
-
379
- const inputStyle = {
380
- boxSizing: "border-box",
381
- width: "100%",
382
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
383
- background: "var(--dsw-alias-bg-layer-3, #fafafa)",
384
- height: "34px",
385
- font: "inherit",
386
- color: "var(--dsw-alias-label-primary, #1f2329)",
387
- borderRadius: "8px",
388
- padding: "0 12px",
389
- fontSize: "13px",
390
- lineHeight: "1.5"
391
- };
392
- const selectStyle = {
393
- ...inputStyle,
394
- height: "34px",
395
- appearance: "none",
396
- WebkitAppearance: "none",
397
- 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)",
398
- backgroundPosition: "right 12px center",
399
- backgroundRepeat: "no-repeat",
400
- backgroundSize: "12px 12px",
401
- paddingRight: "32px"
402
- };
403
- const checkboxStyle = {
404
- width: "18px",
405
- height: "18px",
406
- margin: "0",
407
- accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
408
- // 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
409
- flexShrink: 0,
410
- flexGrow: 0,
411
- alignSelf: "center"
412
- };
413
- const toggleStyle = {
414
- display: "flex",
415
- alignItems: "center",
416
- justifyContent: "space-between",
417
- gap: "12px",
418
- padding: "12px 0",
419
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
420
- };
421
-
422
- const cardStyle = {
423
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
424
- borderRadius: "12px",
425
- padding: "14px 16px",
426
- marginBottom: "12px",
427
- background: "var(--dsw-alias-bg-layer-2, #ffffff)"
428
- };
429
- const groupTitleStyle = {
430
- margin: "0 0 4px",
431
- fontSize: "13px",
432
- fontWeight: "500",
433
- color: "var(--dsw-alias-label-primary, #1f2329)"
434
- };
435
- const sectionStyle = {
436
- maxWidth: "760px",
437
- color: "var(--dsw-alias-label-primary, #1f2329)",
438
- display: "flex",
439
- flexDirection: "column",
440
- gap: "12px"
441
- };
442
- const headingStyle = { margin: 0, fontSize: "18px", fontWeight: "600" };
443
- const introStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "13px" };
444
- const actionsStyle = { display: "flex", gap: "8px", alignItems: "center" };
445
- const btnPrimary = {
446
- border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
447
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
448
- color: "var(--dsw-alias-label-primary, #1f2329)",
449
- borderRadius: "8px",
450
- padding: "6px 14px",
451
- fontSize: "13px",
452
- cursor: disabled ? "default" : "pointer",
453
- opacity: disabled ? 0.5 : 1
454
- };
455
- const btnAccent = {
456
- border: "1px solid var(--dsw-alias-brand-primary, #4b5bff)",
457
- background: "var(--dsw-alias-brand-primary, #4b5bff)",
458
- color: "#fff",
459
- borderRadius: "8px",
460
- padding: "6px 14px",
461
- fontSize: "13px",
462
- cursor: disabled ? "default" : "pointer",
463
- opacity: disabled ? 0.5 : 1
464
- };
465
- const fieldStyle = {
466
- display: "flex",
467
- flexDirection: "column",
468
- gap: "6px",
469
- padding: "12px 0",
470
- borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
471
- };
472
- const headStyle = { display: "flex", alignItems: "center", gap: "8px" };
473
- const labelStyle = {
474
- minWidth: "0",
475
- color: "var(--dsw-alias-label-primary, #1f2329)",
476
- flex: 1,
477
- fontSize: "13px",
478
- fontWeight: "500",
479
- lineHeight: "1.5"
480
- };
481
- const badgeStyle = {
482
- whiteSpace: "nowrap",
483
- background: "var(--dsw-alias-bg-module-platform, #f2f3f5)",
484
- color: "var(--dsw-alias-label-secondary, #6b7280)",
485
- borderRadius: "999px",
486
- padding: "1px 8px",
487
- fontSize: "11px",
488
- fontWeight: "500",
489
- lineHeight: "17px"
490
- };
491
- const mutedBadgeStyle = {
492
- whiteSpace: "nowrap",
493
- color: "var(--dsw-alias-label-tertiary, #8a919f)",
494
- borderRadius: "999px",
495
- padding: "1px 8px",
496
- fontSize: "11px",
497
- lineHeight: "17px"
498
- };
499
- const hintStyle = { color: "var(--dsw-alias-label-tertiary, #8a919f)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
500
- const errStyle = { color: "var(--dsw-alias-label-error, #d54941)", margin: 0, fontSize: "12px", lineHeight: "1.5" };
501
-
502
- const num = (field) => ({
503
- type: "text",
504
- inputMode: "numeric",
505
- value: draft[field],
506
- disabled,
507
- style: { ...inputStyle, borderColor: validation === field ? "var(--dsw-alias-label-error, #d54941)" : undefined },
508
- onChange: (e) => edit(field, e.target.value)
509
- });
510
-
511
- return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
512
- // 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
513
- h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
514
- h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
515
- h("div", null, [
516
- h("h3", { style: headingStyle }, t("title")),
517
- h("p", { style: introStyle }, t("intro"))
518
- ]),
519
- h("div", { style: actionsStyle }, [
520
- h("button", { type: "button", style: btnPrimary, disabled, onClick: discard }, t("discard")),
521
- h("button", { type: "button", style: btnAccent, disabled, onClick: save }, saving ? t("saving") : t("save"))
522
- ])
523
- ]),
524
- failed ? h("p", { style: errStyle }, t("saveFailed")) : null,
525
- h("div", { style: cardStyle }, [
526
- h("p", { style: groupTitleStyle }, t("core")),
527
- h("div", { style: toggleStyle }, [
528
- h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
529
- h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
530
- h("p", { style: hintStyle }, t("bridgeHint"))
531
- ]),
532
- h("input", {
533
- type: "checkbox",
534
- checked: draft.bridgeBuddyMemory === "true",
535
- disabled,
536
- style: checkboxStyle,
537
- onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
538
- })
539
- ]),
540
- row(t("dirsLabel"), t("dirsHint"),
541
- h("input", {
542
- ...num("buddyWorkspaceMemoryDirs"),
543
- type: "text",
544
- placeholder: ".workbuddy/memory, .codebuddy/memory"
545
- }), "buddyWorkspaceMemoryDirs")
546
- ]),
547
- h("div", { style: cardStyle }, [
548
- h("p", { style: groupTitleStyle }, t("smartSummary")),
549
- // 轮次结束自动记录(summarize):三种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
550
- toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
551
- row(t("memoryModeLabel"),
552
- draft.memoryMode === "smart"
553
- ? t("memoryModeHintSmart")
554
- : draft.memoryMode === "hybrid"
555
- ? t("memoryModeHintHybrid")
556
- : t("memoryModeHintPlugin"),
557
- h("select", {
558
- value: ["plugin", "smart", "hybrid"].includes(draft.memoryMode) ? draft.memoryMode : "plugin",
559
- disabled,
560
- onChange: (e) => edit("memoryMode", e.target.value),
561
- style: selectStyle
562
- }, [
563
- h("option", { value: "plugin" }, "插件模式"),
564
- h("option", { value: "smart" }, "智能模式"),
565
- h("option", { value: "hybrid" }, "混合模式")
566
- ]), "memoryMode"),
567
- draft.memoryMode === "smart"
568
- ? h("div", null, [
569
- row(t("summaryModelLabel"), t("summaryModelHint"),
570
- h("select", {
571
- // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
572
- // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
573
- value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
574
- disabled,
575
- style: selectStyle,
576
- onChange: (e) => edit("summaryModel", e.target.value)
577
- }, [
578
- h("option", { value: "" }, "复用当前会话模型"),
579
- ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
580
- ]), "summaryModel"),
581
- toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
582
- row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
583
- row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
584
- ])
585
- : draft.memoryMode === "hybrid"
586
- ? h("div", null, [
587
- row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
588
- row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
589
- ])
590
- : h("div", null, [
591
- toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
592
- ])
593
- ]),
594
- h("div", { style: cardStyle }, [
595
- h("p", { style: groupTitleStyle }, t("storage")),
596
- row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
597
- row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
598
- row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
599
- row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
600
- row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
601
- ]),
602
- h("div", { style: cardStyle }, [
603
- h("p", { style: groupTitleStyle }, t("dev")),
604
- row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
605
- toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
606
- ])
607
- ]);
608
- }
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
+ }
609
596
 
610
597
 
611
598
  // ---- v1.2.0 会话标题栏「记忆」按钮(蒸馏入口)----
@@ -867,34 +854,39 @@ window.__ModuleLoader__.load({
867
854
  }
868
855
 
869
856
 
870
- function apply(ctx) {
871
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), "memory-palace: dictionaries");
872
- const t = ctx.locale.bind(NS);
873
- // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
874
- // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
875
- const controller = new MemoryPalaceController(ctx);
876
- // v1.6.0-rc1:枚举已配置模型改走 ctx.remote.session.modelCatalog()(ApiProxy 已删),
877
- // 枚举失败则降级为空列表。
878
- ctx.slots.inject("settings.section", () => ctx.slots.register({
879
- name: "settings.section",
880
- id: "memory-palace",
881
- order: 20,
882
- label: () => t("nav"),
883
- locale: NS,
884
- inject: () => controller.inject()
885
- }, (props) => MemoryPalaceSection({ ...props, fetchModels: () => fetchModelOptions(ctx) })));
886
- // v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
887
- ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
888
- name: "conversation.session.header.utilities",
889
- id: "memory-palace-distill",
890
- order: -1,
891
- locale: NS
892
- }, DistillButton));
893
- }
894
-
895
- exports.apply = apply;
896
- // v1.6.0-rc1:connection → remote + remote.session(模型枚举走 ctx.remote.session.modelCatalog)。
897
- exports.inject = ["slots", "locale", "remote", "remote.session"];
898
- return module.exports;
899
- }
900
- });
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
+ });