@modusensus/dsh-mneme 0.8.15 → 0.9.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 +21 -3
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +4 -2
- package/docs/CONFIGURATION.md +16 -3
- package/docs/CONTINUITY.md +18 -3
- package/docs/DISTILL.md +162 -0
- package/docs/STORAGE.md +11 -2
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +22 -39
- package/lib/api.js +294 -10
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2767 -337
- package/lib/config.js +99 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +37 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +7 -1
- package/lib/entities/extractor.js +4 -0
- package/lib/hot-memory.js +62 -0
- package/lib/index.js +72 -18
- package/lib/inject.js +33 -64
- package/lib/maintenance.js +25 -5
- package/lib/pipeline.js +534 -0
- package/lib/recall-stats.js +22 -7
- package/lib/reclaim-http.js +150 -0
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +16 -4
- package/lib/serve.js +1 -1
- package/lib/service.js +83 -17
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +19 -2
- package/lib/store.js +251 -10
- package/lib/summarize.js +239 -60
- package/lib/tools.js +11 -2
- package/package.json +1 -1
- package/scripts/publish-gate.mjs +197 -0
- package/src/api-standalone.js +22 -39
- package/src/api.js +294 -10
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +99 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +37 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +7 -1
- package/src/entities/extractor.js +4 -0
- package/src/hot-memory.js +62 -0
- package/src/index.js +72 -18
- package/src/inject.js +33 -64
- package/src/maintenance.js +25 -5
- package/src/pipeline.js +534 -0
- package/src/recall-stats.js +22 -7
- package/src/reclaim-http.js +150 -0
- package/src/safe-slice.js +18 -0
- package/src/scope.js +16 -4
- package/src/serve.js +1 -1
- package/src/service.js +83 -17
- package/src/session-usage.js +350 -0
- package/src/settings.js +19 -2
- package/src/store.js +251 -10
- package/src/summarize.js +239 -60
- package/src/tools.js +11 -2
- package/test/api-routes-gaps.test.js +681 -0
- package/test/api.test.js +171 -8
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/client.test.js +715 -4
- package/test/continuity.test.js +84 -0
- package/test/contrast.test.js +314 -0
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/hot-memory.test.js +67 -1
- package/test/inject-recall.test.js +68 -1
- package/test/inject.test.js +23 -0
- package/test/llm-audit.test.js +190 -1
- package/test/maintenance.test.js +112 -2
- package/test/mint-source.test.js +202 -0
- package/test/pipeline.test.js +505 -0
- package/test/placement.test.js +1 -1
- package/test/pre-inject-gate.test.js +10 -0
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/recall-stats.test.js +89 -4
- package/test/reclaim-http.test.js +209 -0
- package/test/release-workflow.test.js +77 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/serve.test.js +1 -1
- package/test/service.test.js +7 -2
- package/test/session-usage.test.js +308 -0
- package/test/settings.test.js +8 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/store.test.js +108 -0
- package/test/summarize.test.js +350 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
package/lib/client.js
CHANGED
|
@@ -74,15 +74,12 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
// 这是记忆插件(issue 走维护者自开通道);
|
|
75
75
|
// · 宿主耦合——宿主图标名会换代(#287:Outline20 → OutlineRegular/Medium),
|
|
76
76
|
// 只认某一代就取到 undefined,h(undefined) 即 React #130 把整个 slot entry
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const MNEME_MARK_D = "M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97";
|
|
84
|
-
|
|
85
|
-
/** mneme mark 的内联 SVG;currentColor + 描边 1,随主题与字号走。 */
|
|
77
|
+
// 崩掉。改用本地 Lucide 路径数据后,不再需要「探测多代名字 + 缺名降级」。
|
|
78
|
+
// Lucide Brain(项目现有 Lucide 1.42 图标数据,ISC);静态内嵌路径,避免插件运行时加载 ESM 包。
|
|
79
|
+
// 宿主图标名会随版本换代,因此插件标识仍由本地 helper 渲染;插件列表 SVG 与这里共用同一路径。
|
|
80
|
+
const MNEME_MARK_D = "M12 18V5M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5M17.997 5.125a4 4 0 0 1 2.526 5.77M18 18a4 4 0 0 0 2-7.464M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517M6 18a4 4 0 0 1-2-7.464M6.003 5.125a4 4 0 0 0-2.526 5.77";
|
|
81
|
+
|
|
82
|
+
/** 24 栅格 Lucide Brain 映射到宿主 16 栅格,颜色仍随主题走。 */
|
|
86
83
|
const MnemeMark = ({ size = 16, className }) => h("svg", {
|
|
87
84
|
width: size,
|
|
88
85
|
height: size,
|
|
@@ -92,10 +89,10 @@ window.__ModuleLoader__.load({
|
|
|
92
89
|
xmlns: "http://www.w3.org/2000/svg",
|
|
93
90
|
"aria-hidden": "true",
|
|
94
91
|
stroke: "currentColor",
|
|
95
|
-
strokeWidth:
|
|
92
|
+
strokeWidth: 2,
|
|
96
93
|
strokeLinecap: "round",
|
|
97
94
|
strokeLinejoin: "round"
|
|
98
|
-
}, h("path", { d: MNEME_MARK_D }));
|
|
95
|
+
}, h("path", { d: MNEME_MARK_D, transform: "scale(0.666667)" }));
|
|
99
96
|
|
|
100
97
|
/** 渲染 mneme mark:四个入口(浮层标题栏 / footer 回退 / 侧边栏 portal / better-sidebar tab)共用同一颗。 */
|
|
101
98
|
const renderMnemeMark = (props) => h(MnemeMark, props);
|
|
@@ -425,30 +422,48 @@ window.__ModuleLoader__.load({
|
|
|
425
422
|
"memory.tab.constraint": "工程约束",
|
|
426
423
|
"memory.tab.document": "文档",
|
|
427
424
|
"memory.features.title": "功能开关",
|
|
428
|
-
"memory.features.desc": "
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
425
|
+
"memory.features.desc": "控制插件在后台做哪些事。先看「常用」里的总开关就够了,其余项收在「全部设置」里。改动即时保存,重启 DSH 后生效。",
|
|
426
|
+
// Issue #427:分组原先按「实现」分(基础/检索与增强/巩固与睡眠/多会话隔离
|
|
427
|
+
// 是架构图的分区),而人真正会问的是四个问题:它会记住什么、它什么时候
|
|
428
|
+
// 写进去、它怎么用回来、不同会话会不会串。组名改成人话,术语留在 hint。
|
|
429
|
+
// group.basic 是首屏层:新用户不读文档就该答得出「会不会自动记」;
|
|
430
|
+
// 「别的会话能不能看到」由同在首屏的 scope 组回答。
|
|
431
|
+
"memory.features.group.basic": "常用(先动这几个)",
|
|
432
|
+
"memory.features.group.core": "它会记住什么",
|
|
433
|
+
"memory.features.group.enhance": "它什么时候写进去",
|
|
434
|
+
"memory.features.group.dream": "它怎么用回来",
|
|
435
|
+
"memory.features.group.scope": "不同会话之间",
|
|
433
436
|
"memory.features.group.advanced": "高级",
|
|
434
|
-
|
|
437
|
+
// 「全部设置」= 第一层折叠(issue #427 建议 1),常显只留 basic + scope
|
|
438
|
+
// 两组;「高级」是它内部的第二层(进阶 7 项)。
|
|
439
|
+
"memory.features.allToggle": "全部设置",
|
|
440
|
+
"memory.features.advancedToggle": "更少用的 7 个开关",
|
|
435
441
|
"memory.features.restartHint": "重启 DSH 后生效",
|
|
436
442
|
"memory.features.loadFailed": "加载失败",
|
|
437
|
-
"memory.features.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
"memory.features.
|
|
441
|
-
"memory.features.
|
|
442
|
-
"memory.features.
|
|
443
|
-
"memory.features.
|
|
444
|
-
"memory.features.
|
|
445
|
-
"memory.features.
|
|
446
|
-
"memory.features.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
"memory.features.
|
|
450
|
-
"memory.features.
|
|
451
|
-
"memory.features.
|
|
443
|
+
"memory.features.saveFailed": "保存失败",
|
|
444
|
+
// Issue #427 建议 2:条目名给「打开会怎样、关掉会怎样」,术语放末尾。
|
|
445
|
+
// 括号里的英文是设置项键名,末尾的「(BM25)」才是术语。
|
|
446
|
+
"memory.features.autoInject": "自动带上旧记忆(每轮都用)",
|
|
447
|
+
"memory.features.autoInject.hint": "开:每轮自动挑相关记忆塞给 Agent,它不用问就能想起;关:Agent 得自己调用查记忆的工具",
|
|
448
|
+
"memory.features.injectGuidanceEnabled": "顺带教 Agent 怎么用记忆",
|
|
449
|
+
"memory.features.injectGuidanceEnabled.hint": "开:在记忆工具的说明里写清什么时候该查、什么时候该写;关:只给工具不给用法",
|
|
450
|
+
"memory.features.continuityRescueEnabled": "长对话被压缩前先存一份快照",
|
|
451
|
+
"memory.features.continuityRescueEnabled.hint": "开:压缩前先存「正在做什么 / 下一步 / 未决问题」,关键上下文活过压缩;关:压缩可能把它压掉",
|
|
452
|
+
"memory.features.parentOff": "上一项关闭时不生效",
|
|
453
|
+
// 「默认 / 已改」徽标:overrides 里没有这个键 = 值来自 config schema 的
|
|
454
|
+
// 默认值,一条命令就能退回。徽标让这件事不必去读配置文件才成立。
|
|
455
|
+
"memory.features.tagDefault": "默认",
|
|
456
|
+
"memory.features.tagDefault.hint": "这一项用的是出厂默认值,没有被你改过",
|
|
457
|
+
"memory.features.tagChanged": "已改",
|
|
458
|
+
"memory.features.tagChanged.hint": "你改过这一项,它不再用默认值",
|
|
459
|
+
"memory.features.autoSummarize": "自动把对话记下来(自动注入之外)",
|
|
460
|
+
"memory.features.autoSummarize.hint": "开:对话结束自动提炼成长期记忆条目(autoSummarize);关:只有你手动存或 Agent 主动存才有记忆",
|
|
461
|
+
"memory.features.hotMemoryEnabled": "顺带带上最近几轮原话",
|
|
462
|
+
"memory.features.hotMemoryEnabled.hint": "开:注入时把最近几轮的对话原文一并带上,Agent 眼前就是刚才聊的内容;关:只带提炼后的长期记忆。这几轮原文不会写进长期记忆(热记忆,默认开)",
|
|
463
|
+
"memory.features.injectTimePrefix": "会话开头告诉它今天几号",
|
|
464
|
+
"memory.features.injectTimePrefix.hint": "开:每个新会话的第一轮注入一次日期与星期,「今天」「上周」这类说法更稳;关:注入里不带时间(时间前缀,默认关)",
|
|
465
|
+
"memory.features.entityExtractionEnabled": "顺手抽出人名、项目和概念",
|
|
466
|
+
"memory.features.entityExtractionEnabled.hint": "开:每条新记忆里抽出人物/项目/概念,关系图谱自己长起来,也能按「谁」找记忆;关:只存整条记忆,不建关系(实体抽取,默认关)",
|
|
452
467
|
"memory.features.entityExtractionProvider": "实体抽取 Provider",
|
|
453
468
|
"memory.features.entityExtractionModel": "实体抽取模型",
|
|
454
469
|
"memory.features.entityExtractionReasoning": "实体抽取思考强度",
|
|
@@ -464,68 +479,140 @@ window.__ModuleLoader__.load({
|
|
|
464
479
|
"memory.features.summarizeReasoningEffort.high": "高",
|
|
465
480
|
"memory.features.summarizeReasoningEffort.hint": "思考型模型建议选「低」或「关闭思考」,避免推理占满输出导致总结失败",
|
|
466
481
|
"memory.features.entityExtractionModelHint": "Provider / 模型留空 = 跟随主对话模型;思考强度选「跟随默认」= 服务商默认",
|
|
467
|
-
"memory.features.codingRetrospect": "
|
|
468
|
-
"memory.features.codingRetrospect.hint": "
|
|
469
|
-
"memory.features.rerankEnabled": "
|
|
470
|
-
"memory.features.rerankEnabled.hint": "
|
|
471
|
-
"memory.features.heatEnabled": "
|
|
472
|
-
"memory.features.heatEnabled.hint": "
|
|
473
|
-
"memory.features.documentMemoryEnabled": "
|
|
474
|
-
"memory.features.documentMemoryEnabled.hint": "Agent
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
"memory.
|
|
479
|
-
"memory.
|
|
480
|
-
"memory.
|
|
481
|
-
"memory.
|
|
482
|
-
"memory.
|
|
483
|
-
"memory.
|
|
484
|
-
"memory.
|
|
485
|
-
"memory.
|
|
486
|
-
"memory.
|
|
487
|
-
"memory.
|
|
488
|
-
"memory.
|
|
489
|
-
"memory.
|
|
490
|
-
"memory.
|
|
482
|
+
"memory.features.codingRetrospect": "写代码的过程也记下来",
|
|
483
|
+
"memory.features.codingRetrospect.hint": "开:整段编码过程(含报错与工具调用)一并回顾,沉淀踩坑、约束和被否决的方案;关:只提炼对话内容(编码记忆蒸馏,默认关)",
|
|
484
|
+
"memory.features.rerankEnabled": "搜出来的结果再精排一遍",
|
|
485
|
+
"memory.features.rerankEnabled.hint": "开:本地重排模型给召回结果重新打分,更准但更慢;关:直接用检索时的排序(结果重排,默认关)",
|
|
486
|
+
"memory.features.heatEnabled": "越久没用越靠后(像会忘)",
|
|
487
|
+
"memory.features.heatEnabled.hint": "开:像遗忘曲线一样给记忆降温,长期没用过的往后排、不占注入名额;关:一直按相关度排(热度衰减,默认关)",
|
|
488
|
+
"memory.features.documentMemoryEnabled": "长文档只存一张摘要卡",
|
|
489
|
+
"memory.features.documentMemoryEnabled.hint": "开:Agent 写的长文档在记忆里只放「摘要卡」,正文仍由 Agent 保管、按需读取,注入只带摘要;关:整篇都往上下文里带(document 型记忆,默认关)",
|
|
490
|
+
// --- v0.9.0 M.O.(Modus Operandi · 手法,beta)---
|
|
491
|
+
// M.O. = Modus Operandi,拉丁语「一个人一贯的做事方式」;也是 org 词根
|
|
492
|
+
// modus(modusensus)。界面主显示 M.O.,设置项 hint 第一句给全名。
|
|
493
|
+
"memory.actions.beta": "beta",
|
|
494
|
+
"memory.actions.notice": "提炼结果是候选,可能有错、过时、会被推翻。",
|
|
495
|
+
"memory.actions.title": "M.O. · 手法",
|
|
496
|
+
"memory.actions.open": "打开 M.O.",
|
|
497
|
+
"memory.actions.viewFull": "完整视图",
|
|
498
|
+
"memory.actions.openPane": "在右栏打开完整视图",
|
|
499
|
+
"memory.actions.announce.open": "M.O. 面板已打开",
|
|
500
|
+
"memory.actions.announce.close": "M.O. 面板已关闭",
|
|
501
|
+
"memory.actions.close": "关闭",
|
|
502
|
+
"memory.actions.candidates": "候选 {n} 条",
|
|
503
|
+
"memory.actions.runs": "最近运行",
|
|
504
|
+
"memory.actions.runOf": "运行 {n}",
|
|
505
|
+
"memory.actions.run.ok": "成功",
|
|
506
|
+
"memory.actions.run.failed": "失败",
|
|
507
|
+
"memory.actions.run.refuted": "被推翻",
|
|
508
|
+
"memory.actions.state.candidate": "候选",
|
|
509
|
+
"memory.actions.state.trusted": "可信",
|
|
510
|
+
"memory.actions.state.stale": "过时",
|
|
511
|
+
"memory.actions.state.refuted": "被推翻",
|
|
512
|
+
"memory.actions.gate.has": "有闸门证据",
|
|
513
|
+
"memory.actions.gate.none": "还没闸门",
|
|
514
|
+
"memory.actions.gate.tag": "闸",
|
|
515
|
+
"memory.actions.who.main": "主",
|
|
516
|
+
"memory.actions.who.human": "人",
|
|
517
|
+
"memory.actions.who.ci": "CI",
|
|
518
|
+
"memory.actions.ov.done": "完成判据",
|
|
519
|
+
"memory.actions.ov.trigger": "何时触发",
|
|
520
|
+
"memory.actions.band.count": "{n} 步",
|
|
521
|
+
"memory.actions.legend.states": "状态",
|
|
522
|
+
"memory.actions.detail.situation": "情形",
|
|
523
|
+
"memory.actions.detail.action": "动作",
|
|
524
|
+
"memory.actions.detail.why": "为什么",
|
|
525
|
+
"memory.actions.detail.cf": "反事实",
|
|
526
|
+
"memory.actions.detail.noCf": "该节点还没有反事实记录",
|
|
527
|
+
"memory.actions.detail.source": "来源记忆",
|
|
528
|
+
"memory.actions.credit": "复用",
|
|
529
|
+
"memory.actions.creditValue": "{n} 次 · 上次 {d} 天前",
|
|
530
|
+
"memory.actions.dep": "依赖",
|
|
531
|
+
"memory.actions.zoom.in": "放大",
|
|
532
|
+
"memory.actions.zoom.out": "缩小",
|
|
533
|
+
"memory.actions.zoom.fit": "适应宽度",
|
|
534
|
+
"memory.actions.zoom.hint": "Ctrl/⌘ + 滚轮也能缩放",
|
|
535
|
+
"memory.actions.list.heading": "这个项目里的工作流",
|
|
536
|
+
"memory.actions.list.count": "工作流 {n} 条",
|
|
537
|
+
"memory.actions.list.meta": "{steps} 步 · {gates} 道闸门 · 反事实 {cf}/{steps}",
|
|
538
|
+
"memory.actions.scope.project": "项目 {name}",
|
|
539
|
+
"memory.actions.scope.global": "全局",
|
|
540
|
+
"memory.actions.scope.none": "项目未知",
|
|
541
|
+
"memory.actions.broken": "有文件读不出来:",
|
|
542
|
+
"memory.actions.empty.title": "这个项目里还没有工作流",
|
|
543
|
+
"memory.actions.empty.body": "工作流的本体住在项目里的 .mneme/pipelines/ 下——跟着仓库走,能被提交、能被 review、能被 CI 读。做法被复用第二次才算节点类型,第一次只是实例;同一路径被走通两次之后,它才会从会话里被提炼出来、落到那个目录里。",
|
|
544
|
+
"memory.actions.noRuns": "这一条还没有运行记录,所以每个节点都还是候选。",
|
|
545
|
+
"memory.actions.settle.title": "本次运行",
|
|
546
|
+
"memory.actions.settle.line": "「{title}」",
|
|
547
|
+
"memory.actions.settle.used": "本次复用 {n} 条做法(勾选为被用上)",
|
|
548
|
+
"memory.actions.settle.refutedWarn": "本次路径里有已被推翻的节点,结果仅供参考",
|
|
549
|
+
"memory.features.actionPipelineEnabled": "M.O.(beta)",
|
|
550
|
+
"memory.features.actionPipelineEnabled.hint": "⚠️ 开发中:目前只有「看」这一层能用,自己沉淀和触发都还没做,建议暂时保持关闭。Modus Operandi(手法):把真实会话里反复走通的「做法」连成一张只读图谱——会话头图标、右栏完整视图、每次运行的结算卡。重启 DSH 后生效",
|
|
551
|
+
"memory.features.resilientModelDownload": "模型下载断了能续上",
|
|
552
|
+
"memory.features.resilientModelDownload.hint": "开:下载中断后从断点续传并自动重试;关:中断就整份重来(断点续传,默认开)",
|
|
553
|
+
"memory.features.searchSemanticDedup": "搜到意思相同的只留一条",
|
|
554
|
+
"memory.features.searchSemanticDedup.hint": "开:检索结果里意思相近的条目合并成一条,少占名额;关:同义条目一起出现(语义去重,默认关)",
|
|
555
|
+
"memory.features.autoDream": "后台自动整理记忆(合并 · 去重)",
|
|
556
|
+
"memory.features.autoDream.hint": "开:后台自动合并、沉淀碎片记忆(autoDream);关:记忆只增不减,越用越散",
|
|
557
|
+
"memory.features.sleepModeEnabled": "你不在的时候它整理记忆",
|
|
558
|
+
"memory.features.sleepModeEnabled.hint": "开:空闲时段做分层压缩与整理,白天少打扰;关:只有你手动触发的整理(睡眠模式,默认关)",
|
|
559
|
+
// #427 建议 5:hybridInject / selectiveInjectEnabled / adaptiveThresholdEnabled
|
|
560
|
+
// 是同一件事「带什么回来」的三档,相邻摆放并在 hint 里点明,不再摊成
|
|
561
|
+
// 三个不相干的并列开关。
|
|
562
|
+
"memory.features.hybridInject": "关键词 + 意思 两路一起找",
|
|
563
|
+
"memory.features.hybridInject.hint": "开:两路检索各取一批再合并,召回更全;关:只用一路。「带什么回来」共三档(混合注入 / 选择性注入 / 自适应阈值),可只开其中一档(混合注入,默认开)",
|
|
564
|
+
"memory.features.selectiveInjectEnabled": "只带真正相关的记忆",
|
|
565
|
+
"memory.features.selectiveInjectEnabled.hint": "开:按相关性筛一遍再注入,上下文更干净;关:带出去的更多也更杂。三档之一(选择性注入,默认开)",
|
|
566
|
+
"memory.features.adaptiveThresholdEnabled": "按检索质量自动调松紧",
|
|
567
|
+
"memory.features.adaptiveThresholdEnabled.hint": "开:召回质量差时放宽门槛、好时收紧;关:门槛固定。三档之一(自适应阈值,默认开)",
|
|
568
|
+
"memory.features.reflectionUpdateEnabled": "整理时允许改已有记忆",
|
|
569
|
+
"memory.features.reflectionUpdateEnabled.hint": "开:巩固可以改写、合并已有条目,不是只增不改;关:旧记忆永不被自动改动(反思更新,默认开)",
|
|
491
570
|
"memory.features.distill": "蒸馏与限流",
|
|
492
571
|
"memory.features.distillRateLimitIntervalMs": "蒸馏放行间隔 (ms)",
|
|
493
572
|
"memory.features.distillRateLimitRetries": "429 重试次数",
|
|
494
573
|
"memory.features.distillRateLimitBaseDelayMs": "退避起始延迟 (ms)",
|
|
495
574
|
"memory.features.distillMaxChars": "转录截断上限(字符)",
|
|
575
|
+
// #399 的两个留存键。此前只有后端白名单、面板够不着,而
|
|
576
|
+
// docs/CONFIGURATION.md 写的是「面板可启停」——补 UI 让文档成真。
|
|
577
|
+
// hint 必须写清两件事:这是**原始对话**落盘(隐私面),以及天数只管
|
|
578
|
+
// 启动期裁剪(reclaim 走另一条 --older-than,不读这个键)。
|
|
579
|
+
"memory.features.distillRecords.enabled": "留存蒸馏原文",
|
|
580
|
+
"memory.features.distillRecords.enabled.hint": "蒸馏时把窗口内的公开对话原文一并落盘,并记下每条记忆的来源区间;私密推理与工具结果不落。关 = 一行不写、连收集都不做",
|
|
581
|
+
"memory.features.distillRecords.retentionDays": "原文保留天数",
|
|
582
|
+
"memory.features.distillRecords.retentionDays.hint": "1–3650 天;只管启动期裁剪。想让 reclaim 也按这个天数回收,需显式传 --older-than 同一数值",
|
|
496
583
|
"memory.features.codingBoostFactor": "编码记忆加权",
|
|
497
|
-
"memory.features.memoryQualityFilter.enabled": "
|
|
498
|
-
"memory.features.memoryQualityFilter.enabled.hint": "
|
|
499
|
-
"memory.features.llmAudit.enabled": "
|
|
500
|
-
"memory.features.llmAudit.enabled.hint": "
|
|
584
|
+
"memory.features.memoryQualityFilter.enabled": "没价值的记忆自动收起来",
|
|
585
|
+
"memory.features.memoryQualityFilter.enabled.hint": "开:低价值条目自动归档、注入时按质量降权;关:什么都留着,噪声也一起带(记忆质量过滤,默认开)",
|
|
586
|
+
"memory.features.llmAudit.enabled": "记下后台用了多少模型调用",
|
|
587
|
+
"memory.features.llmAudit.enabled.hint": "开:巩固/总结的后台调用与 token 消耗都有账可查;关:后台开销无从核对(调用审计,默认开)",
|
|
501
588
|
// #254:写入准入两层。检测关时整条路径不碰;检测开、拦截关 = 仅告警 +
|
|
502
589
|
// 留审计;两个都开才真拦。文案要写清「只留审计」与「真拦下」的区别,
|
|
503
590
|
// 否则操作者会以为打开检测就已经在拦。
|
|
504
|
-
"memory.features.writeAdmission.enabled": "
|
|
505
|
-
"memory.features.writeAdmission.enabled.hint": "
|
|
506
|
-
"memory.features.writeAdmission.enforce": "
|
|
507
|
-
"memory.features.writeAdmission.enforce.hint": "
|
|
508
|
-
"memory.features.preInjectGate.enabled": "
|
|
509
|
-
"memory.features.preInjectGate.enabled.hint": "
|
|
510
|
-
"memory.features.preInjectGate.enforce": "
|
|
511
|
-
"memory.features.preInjectGate.enforce.hint": "
|
|
591
|
+
"memory.features.writeAdmission.enabled": "写入前先判一下是不是废话(只记不拦)",
|
|
592
|
+
"memory.features.writeAdmission.enabled.hint": "开:空白/噪声写入判为命中并留一条审计记录,但仍存进去;关:完全不判。要真拦,见下一项(写入准入判定,默认关)",
|
|
593
|
+
"memory.features.writeAdmission.enforce": "真把没用的写入挡下来",
|
|
594
|
+
"memory.features.writeAdmission.enforce.hint": "开:命中直接拒绝写入;关:只留审计、照存不误。需先打开上面那项判定(写入准入拦截,默认关)",
|
|
595
|
+
"memory.features.preInjectGate.enabled": "注入前先筛掉「只是我的看法」的记忆(只记不拦)",
|
|
596
|
+
"memory.features.preInjectGate.enabled.hint": "开:注入前判出意见/立场类记忆并留审计,先观察分布;关:照常注入(注入前判定,默认关)",
|
|
597
|
+
"memory.features.preInjectGate.enforce": "注入时真滤掉判定不合适的",
|
|
598
|
+
"memory.features.preInjectGate.enforce.hint": "开:命中的候选在注入时丢掉;关:只留审计、照常注入。需先打开上面那项判定(注入前判定拦截,默认关)",
|
|
512
599
|
// #164 A2:密钥/PII 判据自己的闸。文案要写清它管的是哪一批判据(与上面的
|
|
513
600
|
// 空白/噪声分开)、它跑在「写入准入判定」里面(那一项关着就不跑),以及
|
|
514
601
|
// 「开着也不拦」——拦是上面那项的事。
|
|
515
|
-
"memory.features.sensitiveScanEnabled": "
|
|
516
|
-
"memory.features.sensitiveScanEnabled.hint": "
|
|
517
|
-
"memory.features.bm25SearchEnabled": "
|
|
518
|
-
"memory.features.bm25SearchEnabled.hint": "
|
|
519
|
-
"memory.features.conflictFreezeEnabled": "
|
|
520
|
-
"memory.features.conflictFreezeEnabled.hint": "
|
|
521
|
-
"memory.features.scopeEnabled": "
|
|
522
|
-
"memory.features.scopeEnabled.hint": "
|
|
523
|
-
"memory.features.strictScope": "
|
|
524
|
-
"memory.features.strictScope.hint": "
|
|
525
|
-
"memory.features.trustEpistemicWeighting": "
|
|
526
|
-
"memory.features.trustEpistemicWeighting.hint": "
|
|
527
|
-
"memory.features.reflectionFailureTracking": "
|
|
528
|
-
"memory.features.reflectionFailureTracking.hint": "
|
|
602
|
+
"memory.features.sensitiveScanEnabled": "写入前扫一遍密钥和个人信息",
|
|
603
|
+
"memory.features.sensitiveScanEnabled.hint": "开:在写入准入判定里面扫密钥/个人信息,命中只留审计;要真拦,再打开「写入准入拦截」。判定关着时它不跑(密钥/PII 扫描,默认关)",
|
|
604
|
+
"memory.features.bm25SearchEnabled": "也用关键词方式找记忆",
|
|
605
|
+
"memory.features.bm25SearchEnabled.hint": "开:多一个字少一个字也搜得到,专名、报错码、路径更易命中,与「按意思找」互补;关:只按语义相近度(BM25 关键词检索,默认开)",
|
|
606
|
+
"memory.features.conflictFreezeEnabled": "互相矛盾的记忆先冻结",
|
|
607
|
+
"memory.features.conflictFreezeEnabled.hint": "开:整理发现矛盾时先冻结待你确认,不让两条同时生效;关:两边都照用(冲突冻结,默认关)",
|
|
608
|
+
"memory.features.scopeEnabled": "记住每条记忆来自哪个会话",
|
|
609
|
+
"memory.features.scopeEnabled.hint": "开:每条新记忆标上来自哪个 Agent / 工作区,别的会话能不能看到由它说了算;关:新记忆不带来源,所有会话共用一批记忆(作用域标注,默认关)",
|
|
610
|
+
"memory.features.strictScope": "别人设为私有的记忆我看不见",
|
|
611
|
+
"memory.features.strictScope.hint": "开:其他会话明确标私有的记忆对你彻底不可见;关:仍可见、只是排到后面。要彻底隔离请用条目的「敏感度」字段。需先打开上面那项标注(严格隔离,默认关)",
|
|
612
|
+
"memory.features.trustEpistemicWeighting": "按来源可信度调排序",
|
|
613
|
+
"memory.features.trustEpistemicWeighting.hint": "开:可信度高的来源排前面;关:一视同仁(可信度加权,实验性,默认关)",
|
|
614
|
+
"memory.features.reflectionFailureTracking": "整理失败时留下案例",
|
|
615
|
+
"memory.features.reflectionFailureTracking.hint": "开:把失败原因与案例记下来供诊断改进;关:失败不留记录(失败追踪,默认开)",
|
|
529
616
|
"memory.features.embedRoute": "语义检索路线",
|
|
530
617
|
"memory.features.embedProvider": "Embedding 提供方",
|
|
531
618
|
"memory.features.embedProvider.openai": "OpenAI 兼容接口",
|
|
@@ -553,6 +640,7 @@ window.__ModuleLoader__.load({
|
|
|
553
640
|
"memory.features.modelTestOk": "连通正常",
|
|
554
641
|
"memory.features.modelTestFail": "测试失败",
|
|
555
642
|
"memory.features.modelTestHint": "真实发起一次最小巩固调用,验证 Provider/模型连通与 effort 支持",
|
|
643
|
+
"memory.features.modelTestDowngraded": "档位不被该模型接受,已去掉档位连通(与真实巩固一致)",
|
|
556
644
|
"memory.features.routeStaleMark": "(不在可用列表)",
|
|
557
645
|
"memory.features.routeStaleHint": "该值不在当前可用列表(常见于切换过 Provider):请重新选择,或点「测试连通性」当场验证",
|
|
558
646
|
"memory.explorer.viewCards": "卡片",
|
|
@@ -577,6 +665,20 @@ window.__ModuleLoader__.load({
|
|
|
577
665
|
"memory.explorer.imported": "已合并 {n} 条编辑",
|
|
578
666
|
"memory.explorer.importFailed": "导入失败",
|
|
579
667
|
"memory.explorer.importNoFile": "请先选择文件",
|
|
668
|
+
"memory.explorer.newMemory": "新增记忆…",
|
|
669
|
+
"memory.explorer.newTitle": "新增记忆",
|
|
670
|
+
"memory.explorer.newHint": "手动写一条记忆。同类型同标题会并入既有条目,而不是长出第二行。",
|
|
671
|
+
"memory.explorer.newType": "类型",
|
|
672
|
+
"memory.explorer.newTitlePlaceholder": "短标题,也是去重键",
|
|
673
|
+
"memory.explorer.newContentPlaceholder": "正文",
|
|
674
|
+
"memory.explorer.newTagsPlaceholder": "标签,逗号分隔(可留空)",
|
|
675
|
+
"memory.explorer.newConfirm": "新建",
|
|
676
|
+
"memory.explorer.newSaving": "保存中…",
|
|
677
|
+
"memory.explorer.newCreated": "已新建",
|
|
678
|
+
"memory.explorer.newMerged": "已并入同标题的既有条目",
|
|
679
|
+
"memory.explorer.newFailed": "新建失败",
|
|
680
|
+
"memory.explorer.newNoTitle": "请先填写标题",
|
|
681
|
+
"memory.explorer.newNoContent": "请先填写内容",
|
|
580
682
|
"memory.explorer.exportFailed": "导出失败",
|
|
581
683
|
"memory.explorer.conflictBadge": "冲突",
|
|
582
684
|
"memory.explorer.archivedBadge": "已归档",
|
|
@@ -643,6 +745,14 @@ window.__ModuleLoader__.load({
|
|
|
643
745
|
"memory.status.conflictQueue.similarity": "相似度",
|
|
644
746
|
"memory.status.conflictQueue.diffTitle": "差异已高亮:红=删除、绿=新增",
|
|
645
747
|
"memory.status.conflictQueue.diffFallback": "两段文本差异较大,请对照原文阅读",
|
|
748
|
+
"memory.status.conflictQueue.expand": "展开全文",
|
|
749
|
+
"memory.status.conflictQueue.collapse": "收起全文",
|
|
750
|
+
"memory.status.conflictQueue.copySide": "复制本侧",
|
|
751
|
+
"memory.status.conflictQueue.copiedSide": "本侧全文已复制",
|
|
752
|
+
"memory.status.conflictQueue.importance": "重要度 {n}",
|
|
753
|
+
"memory.status.conflictQueue.chars": "{n} 字符",
|
|
754
|
+
"memory.status.conflictQueue.updated": "更新于 {time}",
|
|
755
|
+
"memory.status.conflictQueue.created": "创建于 {time}",
|
|
646
756
|
"memory.status.conflictQueue.emptyTitle": "暂无待确认冲突",
|
|
647
757
|
"memory.status.conflictQueue.emptyBody": "当巩固发现两条互相矛盾或高度相似的记忆时,会先冻结在这里等你裁决,不会自动修改。裁决前双方都不参与注入。",
|
|
648
758
|
"memory.status.conflictQueue.applyHintTitle": "确认后:保留方正文追加已否决注记,另一方归档",
|
|
@@ -724,8 +834,62 @@ window.__ModuleLoader__.load({
|
|
|
724
834
|
"memory.status.mirror.foot": "记忆条目在磁盘上的 Markdown 副本同步状态",
|
|
725
835
|
"memory.status.sec.overview": "库内一览",
|
|
726
836
|
"memory.status.sec.engine": "后台运转",
|
|
727
|
-
"memory.status.
|
|
728
|
-
"memory.status.
|
|
837
|
+
"memory.status.sec.maintenance": "维护",
|
|
838
|
+
"memory.status.maint": "存储回收",
|
|
839
|
+
"memory.status.maint.freelist": "空闲页可立刻回收",
|
|
840
|
+
"memory.status.maint.plannedInputs": "待清理输入快照",
|
|
841
|
+
"memory.status.maint.plannedEmbeddings": "待清理归档向量",
|
|
842
|
+
"memory.status.maint.window": "保留窗口",
|
|
843
|
+
"memory.status.maint.windowUnit": "天",
|
|
844
|
+
"memory.status.maint.windowLabel": "保留窗口(天)",
|
|
845
|
+
"memory.status.maint.reestimate": "重新估算",
|
|
846
|
+
"memory.status.maint.reestimating": "估算中…",
|
|
847
|
+
"memory.status.maint.stale": "窗口已改动:先点「重新估算」再执行,否则执行的是上一次估算的数字。",
|
|
848
|
+
"memory.status.maint.vacuum": "同时 VACUUM",
|
|
849
|
+
"memory.status.maint.vacuumHint": "VACUUM 会短暂独占数据库,期间其它读写要等待;不做也不影响清理结果。",
|
|
850
|
+
"memory.status.maint.execute": "执行回收",
|
|
851
|
+
"memory.status.maint.confirm": "确认执行(不可逆)",
|
|
852
|
+
"memory.status.maint.running": "执行中…",
|
|
853
|
+
"memory.status.maint.cancel": "取消",
|
|
854
|
+
"memory.status.maint.confirmHint": "将清理 {inputs} 行输入快照与 {embeddings} 行归档向量,且不可逆。",
|
|
855
|
+
"memory.status.maint.badWindow": "保留窗口要填不小于 0 的整数",
|
|
856
|
+
"memory.status.maint.failed": "回收失败",
|
|
857
|
+
"memory.status.maint.done": "回收已完成",
|
|
858
|
+
"memory.status.maint.size": "体积(前 → 后)",
|
|
859
|
+
"memory.status.maint.reclaimed": "实际释放",
|
|
860
|
+
"memory.status.maint.clearedInputs": "已清理输入快照",
|
|
861
|
+
"memory.status.maint.clearedEmbeddings": "已清理归档向量",
|
|
862
|
+
"memory.status.maint.receipt": "审计回执",
|
|
863
|
+
"memory.status.maint.receiptNone": "未记录(审计未启用或回执写入失败)",
|
|
864
|
+
"memory.status.maint.duration": "耗时",
|
|
865
|
+
"memory.status.maint.vacuumRan": "VACUUM 已执行",
|
|
866
|
+
"memory.status.maint.vacuumOff": "未 VACUUM",
|
|
867
|
+
"memory.status.maint.vacuumFailed": "VACUUM 失败",
|
|
868
|
+
"memory.status.maint.vacuumFailedHint": "清理已完成且不可逆,但文件暂时不会变小;稍后重跑即可(清理是幂等的)。",
|
|
869
|
+
"memory.status.sec.token": "Token 去向 · 近 7 天",
|
|
870
|
+
"memory.status.token.in": "输入",
|
|
871
|
+
"memory.status.token.out": "输出",
|
|
872
|
+
"memory.status.token.total": "7 天 token 总量",
|
|
873
|
+
"memory.status.token.calls": "调用次数",
|
|
874
|
+
"memory.status.token.callsCap": "近 7 天 · 全部后台组件",
|
|
875
|
+
"memory.status.token.share": "巩固占比",
|
|
876
|
+
"memory.status.token.shareFoot": "dream_consolidate · {n} token",
|
|
877
|
+
"memory.status.token.conflicts": "待确认冲突",
|
|
878
|
+
"memory.status.token.conflictsFoot": "来自 /dream-status · 实时",
|
|
879
|
+
"memory.status.token.donut": "按组件 · 谁花了这些 token",
|
|
880
|
+
"memory.status.token.days": "按天 · 7 天趋势",
|
|
881
|
+
"memory.status.token.heat": "8 周热力 · 每天花了多少",
|
|
882
|
+
"memory.status.token.heatLess": "少",
|
|
883
|
+
"memory.status.token.heatMore": "多",
|
|
884
|
+
"memory.status.token.last7": "近 7 天",
|
|
885
|
+
"memory.status.token.meterOnly": "(只计量)",
|
|
886
|
+
"memory.status.token.detail": "详细状态卡(原「库内一览 / 后台运转」9 张,点击展开)",
|
|
887
|
+
"memory.status.token.op.dream_consolidate": "记忆巩固",
|
|
888
|
+
"memory.status.token.op.summarize_compress": "会话蒸馏",
|
|
889
|
+
"memory.status.token.op.entity_extract": "实体抽取",
|
|
890
|
+
"memory.status.token.op.dream_summarize": "总览摘要",
|
|
891
|
+
"memory.status.token.op.sleep_pattern": "睡眠模式",
|
|
892
|
+
"memory.status.token.op.write_admission": "写入准入",
|
|
729
893
|
"memory.status.error": "加载失败"
|
|
730
894
|
},
|
|
731
895
|
en: {
|
|
@@ -892,30 +1056,43 @@ window.__ModuleLoader__.load({
|
|
|
892
1056
|
"memory.tab.constraint": "Constraints",
|
|
893
1057
|
"memory.tab.document": "Documents",
|
|
894
1058
|
"memory.features.title": "Features",
|
|
895
|
-
"memory.features.desc": "Choose what the plugin does in the background. Changes are saved immediately and take effect after restarting DSH.",
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1059
|
+
"memory.features.desc": "Choose what the plugin does in the background. The Common switches at the top are all you need to start; everything else lives under All settings. Changes are saved immediately and take effect after restarting DSH.",
|
|
1060
|
+
// Issue #427: groups used to be implementation slices (the architecture
|
|
1061
|
+
// diagram's partitions). What a person actually asks is four questions —
|
|
1062
|
+
// what gets remembered, when it gets written, how it comes back, and
|
|
1063
|
+
// whether sessions bleed into each other. Terminology stays in the hints.
|
|
1064
|
+
// group.basic is the first-screen tier; the scope group sits there too so
|
|
1065
|
+
// "can other sessions see this?" is answerable without opening anything.
|
|
1066
|
+
"memory.features.group.basic": "Common (start here)",
|
|
1067
|
+
"memory.features.group.core": "What gets remembered",
|
|
1068
|
+
"memory.features.group.enhance": "When it gets written",
|
|
1069
|
+
"memory.features.group.dream": "How it comes back",
|
|
1070
|
+
"memory.features.group.scope": "Between sessions",
|
|
900
1071
|
"memory.features.group.advanced": "Advanced",
|
|
901
|
-
"memory.features.
|
|
1072
|
+
"memory.features.allToggle": "All settings",
|
|
1073
|
+
"memory.features.advancedToggle": "7 rarer switches",
|
|
902
1074
|
"memory.features.restartHint": "Takes effect after restarting DSH",
|
|
903
1075
|
"memory.features.loadFailed": "Failed to load",
|
|
904
|
-
"memory.features.
|
|
905
|
-
"memory.features.autoInject
|
|
906
|
-
"memory.features.
|
|
907
|
-
"memory.features.injectGuidanceEnabled
|
|
908
|
-
"memory.features.
|
|
909
|
-
"memory.features.continuityRescueEnabled
|
|
910
|
-
"memory.features.
|
|
911
|
-
"memory.features.
|
|
912
|
-
"memory.features.
|
|
913
|
-
"memory.features.
|
|
914
|
-
"memory.features.
|
|
915
|
-
"memory.features.
|
|
916
|
-
"memory.features.
|
|
917
|
-
"memory.features.
|
|
918
|
-
"memory.features.
|
|
1076
|
+
"memory.features.saveFailed": "Failed to save",
|
|
1077
|
+
"memory.features.autoInject": "Carry old memories into every turn",
|
|
1078
|
+
"memory.features.autoInject.hint": "On: relevant memories are handed to the agent every turn, so it remembers without being asked. Off: the agent has to call the search tool itself",
|
|
1079
|
+
"memory.features.injectGuidanceEnabled": "Also teach the agent how to use memory",
|
|
1080
|
+
"memory.features.injectGuidanceEnabled.hint": "On: the memory tools' description says when to search and when to write. Off: the tools ship without guidance",
|
|
1081
|
+
"memory.features.continuityRescueEnabled": "Snapshot before a long chat gets compacted",
|
|
1082
|
+
"memory.features.continuityRescueEnabled.hint": "On: a snapshot of what's in progress / next steps / open questions is stored before compaction, so key context survives it. Off: compaction can crush it",
|
|
1083
|
+
"memory.features.parentOff": "Inactive while the switch above is off",
|
|
1084
|
+
"memory.features.tagDefault": "default",
|
|
1085
|
+
"memory.features.tagDefault.hint": "This one is at its factory default — you have not changed it",
|
|
1086
|
+
"memory.features.tagChanged": "changed",
|
|
1087
|
+
"memory.features.tagChanged.hint": "You changed this one; it no longer uses the default",
|
|
1088
|
+
"memory.features.autoSummarize": "Turn conversations into memory automatically",
|
|
1089
|
+
"memory.features.autoSummarize.hint": "On: memory entries are distilled when a conversation ends (autoSummarize). Off: memory only exists if you save it, or the agent decides to",
|
|
1090
|
+
"memory.features.hotMemoryEnabled": "Carry the last few turns verbatim",
|
|
1091
|
+
"memory.features.hotMemoryEnabled.hint": "On: the recent turns ride along with each injection, so the agent sees what was just said. Off: only distilled memories travel. Those turns are never written to long-term memory (hot memory, on by default)",
|
|
1092
|
+
"memory.features.injectTimePrefix": "Tell it today's date at session start",
|
|
1093
|
+
"memory.features.injectTimePrefix.hint": "On: the first turn of each new session gets the date and weekday once, so \"today\" and \"last week\" land reliably. Off: no time in the injection (time prefix, off by default)",
|
|
1094
|
+
"memory.features.entityExtractionEnabled": "Also pull out people, projects and concepts",
|
|
1095
|
+
"memory.features.entityExtractionEnabled.hint": "On: each new memory yields people / projects / concepts, so the relation graph grows by itself and recall can key on who. Off: memories stay whole, no relations (entity extraction, off by default)",
|
|
919
1096
|
"memory.features.entityExtractionProvider": "Entity extraction provider",
|
|
920
1097
|
"memory.features.entityExtractionModel": "Entity extraction model",
|
|
921
1098
|
"memory.features.entityExtractionReasoning": "Entity extraction reasoning",
|
|
@@ -931,68 +1108,134 @@ window.__ModuleLoader__.load({
|
|
|
931
1108
|
"memory.features.summarizeReasoningEffort.high": "High",
|
|
932
1109
|
"memory.features.summarizeReasoningEffort.hint": "For thinking models, prefer Low or No reasoning so thinking cannot fill the output budget and break summaries",
|
|
933
1110
|
"memory.features.entityExtractionModelHint": "Provider / model empty = follow the main conversation model; reasoning \"Follow default\" = provider default",
|
|
934
|
-
"memory.features.codingRetrospect": "
|
|
935
|
-
"memory.features.codingRetrospect.hint": "
|
|
936
|
-
"memory.features.rerankEnabled": "
|
|
937
|
-
"memory.features.rerankEnabled.hint": "
|
|
938
|
-
"memory.features.heatEnabled": "
|
|
939
|
-
"memory.features.heatEnabled.hint": "
|
|
940
|
-
"memory.features.documentMemoryEnabled": "
|
|
941
|
-
"memory.features.documentMemoryEnabled.hint": "
|
|
942
|
-
|
|
943
|
-
"memory.
|
|
944
|
-
"memory.
|
|
945
|
-
"memory.
|
|
946
|
-
"memory.
|
|
947
|
-
"memory.
|
|
948
|
-
"memory.
|
|
949
|
-
"memory.
|
|
950
|
-
"memory.
|
|
951
|
-
"memory.
|
|
952
|
-
"memory.
|
|
953
|
-
"memory.
|
|
954
|
-
"memory.
|
|
955
|
-
"memory.
|
|
956
|
-
"memory.
|
|
957
|
-
"memory.
|
|
1111
|
+
"memory.features.codingRetrospect": "Remember how the code was written too",
|
|
1112
|
+
"memory.features.codingRetrospect.hint": "On: the whole coding session (errors and tool calls included) is reviewed, yielding pitfalls, constraints and rejected solutions. Off: only conversation content is distilled (coding retrospection, off by default)",
|
|
1113
|
+
"memory.features.rerankEnabled": "Rerank search results once more",
|
|
1114
|
+
"memory.features.rerankEnabled.hint": "On: a local rerank model re-scores recalled candidates — more precise, slower. Off: the search-time order stands (reranking, off by default)",
|
|
1115
|
+
"memory.features.heatEnabled": "The longer unused, the lower it ranks",
|
|
1116
|
+
"memory.features.heatEnabled.hint": "On: memories cool down like a forgetting curve — long-unused ones rank later and stop taking injection slots. Off: relevance order always (heat decay, off by default)",
|
|
1117
|
+
"memory.features.documentMemoryEnabled": "Store long documents as a summary card",
|
|
1118
|
+
"memory.features.documentMemoryEnabled.hint": "On: long agent-authored documents live in memory as a \"summary card\"; the full text stays agent-owned and is read on demand. Off: the whole document travels into context (document memory, off by default)",
|
|
1119
|
+
// --- v0.9.0 M.O. / Modus Operandi (beta) ---
|
|
1120
|
+
"memory.actions.beta": "Beta",
|
|
1121
|
+
"memory.actions.notice": "Extracted steps are candidates: they may be wrong, outdated, or refuted.",
|
|
1122
|
+
"memory.actions.title": "Modus Operandi",
|
|
1123
|
+
"memory.actions.open": "Open Modus Operandi",
|
|
1124
|
+
"memory.actions.viewFull": "Full view",
|
|
1125
|
+
"memory.actions.openPane": "Open full view in sidebar",
|
|
1126
|
+
"memory.actions.announce.open": "Modus Operandi panel opened",
|
|
1127
|
+
"memory.actions.announce.close": "Modus Operandi panel closed",
|
|
1128
|
+
"memory.actions.close": "Close",
|
|
1129
|
+
"memory.actions.candidates": "{n} candidates",
|
|
1130
|
+
"memory.actions.runs": "Recent runs",
|
|
1131
|
+
"memory.actions.runOf": "Run {n}",
|
|
1132
|
+
"memory.actions.run.ok": "ok",
|
|
1133
|
+
"memory.actions.run.failed": "failed",
|
|
1134
|
+
"memory.actions.run.refuted": "refuted",
|
|
1135
|
+
"memory.actions.state.candidate": "candidate",
|
|
1136
|
+
"memory.actions.state.trusted": "trusted",
|
|
1137
|
+
"memory.actions.state.stale": "stale",
|
|
1138
|
+
"memory.actions.state.refuted": "refuted",
|
|
1139
|
+
"memory.actions.gate.has": "has counterfactual",
|
|
1140
|
+
"memory.actions.gate.none": "no gate yet",
|
|
1141
|
+
"memory.actions.gate.tag": "GATE",
|
|
1142
|
+
"memory.actions.who.main": "main",
|
|
1143
|
+
"memory.actions.who.human": "human",
|
|
1144
|
+
"memory.actions.who.ci": "CI",
|
|
1145
|
+
"memory.actions.ov.done": "Done when",
|
|
1146
|
+
"memory.actions.ov.trigger": "Triggered by",
|
|
1147
|
+
"memory.actions.band.count": "{n} steps",
|
|
1148
|
+
"memory.actions.legend.states": "States",
|
|
1149
|
+
"memory.actions.detail.situation": "Situation",
|
|
1150
|
+
"memory.actions.detail.action": "Action",
|
|
1151
|
+
"memory.actions.detail.why": "Why",
|
|
1152
|
+
"memory.actions.detail.cf": "Counterfactual",
|
|
1153
|
+
"memory.actions.detail.noCf": "No counterfactual recorded yet",
|
|
1154
|
+
"memory.actions.detail.source": "Source memories",
|
|
1155
|
+
"memory.actions.credit": "Reuse",
|
|
1156
|
+
"memory.actions.creditValue": "{n} uses · last {d}d ago",
|
|
1157
|
+
"memory.actions.dep": "depends on",
|
|
1158
|
+
"memory.actions.zoom.in": "Zoom in",
|
|
1159
|
+
"memory.actions.zoom.out": "Zoom out",
|
|
1160
|
+
"memory.actions.zoom.fit": "Fit width",
|
|
1161
|
+
"memory.actions.zoom.hint": "Ctrl/⌘ + wheel also zooms",
|
|
1162
|
+
"memory.actions.list.heading": "Workflows in this project",
|
|
1163
|
+
"memory.actions.list.count": "{n} workflow(s)",
|
|
1164
|
+
"memory.actions.list.meta": "{steps} steps · {gates} gates · counterfactual {cf}/{steps}",
|
|
1165
|
+
"memory.actions.scope.project": "project {name}",
|
|
1166
|
+
"memory.actions.scope.global": "global",
|
|
1167
|
+
"memory.actions.scope.none": "project unknown",
|
|
1168
|
+
"memory.actions.broken": "Some files could not be read:",
|
|
1169
|
+
"memory.actions.empty.title": "No workflows in this project yet",
|
|
1170
|
+
"memory.actions.empty.body": "A workflow lives in the project, under .mneme/pipelines/ — it travels with the repository: committable, reviewable, readable by CI. A practice becomes a node type on its second reuse; the first run is just an instance. Once the same path has been walked twice it gets distilled out of the session and lands in that directory.",
|
|
1171
|
+
"memory.actions.noRuns": "No runs recorded for this workflow yet, so every step is still a candidate.",
|
|
1172
|
+
"memory.actions.settle.title": "This run",
|
|
1173
|
+
"memory.actions.settle.line": "\"{title}\"",
|
|
1174
|
+
"memory.actions.settle.used": "Reused {n} steps this run (checked = actually used)",
|
|
1175
|
+
"memory.actions.settle.refutedWarn": "This path contains refuted steps; take the result with caution",
|
|
1176
|
+
"memory.features.actionPipelineEnabled": "M.O. (beta)",
|
|
1177
|
+
"memory.features.actionPipelineEnabled.hint": "⚠️ Under development: only the viewing layer works today — self-distillation and triggering are not built yet, so leave this off for now. Modus Operandi: the ways of working that actually got walked through in real sessions, drawn as a read-only graph — session-header icon, full sidebar view, and a per-run settlement card. Takes effect after restarting DSH",
|
|
1178
|
+
"memory.features.resilientModelDownload": "Model downloads resume after interruption",
|
|
1179
|
+
"memory.features.resilientModelDownload.hint": "On: downloads continue from where they stopped and retry automatically. Off: an interrupted download starts over (resumable downloads, on by default)",
|
|
1180
|
+
"memory.features.searchSemanticDedup": "Keep one entry when search hits near-duplicates",
|
|
1181
|
+
"memory.features.searchSemanticDedup.hint": "On: near-duplicate entries merge into one, saving injection slots. Off: duplicates all show up (semantic dedup, off by default)",
|
|
1182
|
+
"memory.features.autoDream": "Tidy memories in the background (merge · dedupe)",
|
|
1183
|
+
"memory.features.autoDream.hint": "On: fragmented memories are merged and settled in the background (autoDream). Off: memory only accumulates, so it gets messier the longer you use it",
|
|
1184
|
+
"memory.features.sleepModeEnabled": "It tidies memories while you are away",
|
|
1185
|
+
"memory.features.sleepModeEnabled.hint": "On: layered compaction and tidying happen during idle periods. Off: only consolidation you trigger manually (sleep mode, off by default)",
|
|
1186
|
+
// #427 suggestion 5: hybridInject / selectiveInjectEnabled /
|
|
1187
|
+
// adaptiveThresholdEnabled are three dials of one question — what comes
|
|
1188
|
+
// back. Adjacent placement plus a hint that names the other two.
|
|
1189
|
+
"memory.features.hybridInject": "Search by keywords and meaning together",
|
|
1190
|
+
"memory.features.hybridInject.hint": "On: both channels each fetch a batch and merge — broader recall. Off: one channel only. Three dials exist for \"what comes back\" (hybrid / selective / adaptive threshold); one is enough (hybrid injection, on by default)",
|
|
1191
|
+
"memory.features.selectiveInjectEnabled": "Only carry memories that truly matter",
|
|
1192
|
+
"memory.features.selectiveInjectEnabled.hint": "On: relevance is filtered before injecting — cleaner context. Off: more comes along, and noisier. One of the three dials (selective injection, on by default)",
|
|
1193
|
+
"memory.features.adaptiveThresholdEnabled": "Tighten or loosen by retrieval quality",
|
|
1194
|
+
"memory.features.adaptiveThresholdEnabled.hint": "On: a poor-recall day loosens the floor, a good one tightens it. Off: the floor is fixed. One of the three dials (adaptive threshold, on by default)",
|
|
1195
|
+
"memory.features.reflectionUpdateEnabled": "Let tidying revise existing memories",
|
|
1196
|
+
"memory.features.reflectionUpdateEnabled.hint": "On: consolidation may rewrite and merge existing entries, not only add. Off: old memories are never auto-edited (reflective update, on by default)",
|
|
958
1197
|
"memory.features.distill": "Distillation & rate limiting",
|
|
959
1198
|
"memory.features.distillRateLimitIntervalMs": "Distill spacing (ms)",
|
|
960
1199
|
"memory.features.distillRateLimitRetries": "429 retries",
|
|
961
1200
|
"memory.features.distillRateLimitBaseDelayMs": "Backoff base delay (ms)",
|
|
962
1201
|
"memory.features.distillMaxChars": "Transcript cap (chars)",
|
|
1202
|
+
"memory.features.distillRecords.enabled": "Keep distilled transcripts",
|
|
1203
|
+
"memory.features.distillRecords.enabled.hint": "Also store the public parts of the distilled window, with a provenance link per memory; private reasoning and tool output are never stored. Off writes nothing and skips collection entirely",
|
|
1204
|
+
"memory.features.distillRecords.retentionDays": "Transcript retention (days)",
|
|
1205
|
+
"memory.features.distillRecords.retentionDays.hint": "1–3650 days; startup pruning only. To make reclaim use the same window, pass the same value via --older-than",
|
|
963
1206
|
"memory.features.codingBoostFactor": "Coding boost factor",
|
|
964
|
-
"memory.features.memoryQualityFilter.enabled": "
|
|
965
|
-
"memory.features.memoryQualityFilter.enabled.hint": "
|
|
966
|
-
"memory.features.llmAudit.enabled": "
|
|
967
|
-
"memory.features.llmAudit.enabled.hint": "
|
|
1207
|
+
"memory.features.memoryQualityFilter.enabled": "Tuck low-value memories away automatically",
|
|
1208
|
+
"memory.features.memoryQualityFilter.enabled.hint": "On: low-value entries are archived automatically and injection downranks by quality. Off: everything stays, noise included (memory quality filter, on by default)",
|
|
1209
|
+
"memory.features.llmAudit.enabled": "Keep tabs on background model usage",
|
|
1210
|
+
"memory.features.llmAudit.enabled.hint": "On: background consolidation / summary calls and token usage are all on record. Off: there is no way to check what the background spent (call audit, on by default)",
|
|
968
1211
|
// #254 — two layers: enabled runs the check (audit only), enforce actually
|
|
969
1212
|
// blocks. The hints must keep that difference explicit.
|
|
970
|
-
"memory.features.writeAdmission.enabled": "
|
|
971
|
-
"memory.features.writeAdmission.enabled.hint": "
|
|
972
|
-
"memory.features.writeAdmission.enforce": "
|
|
973
|
-
"memory.features.writeAdmission.enforce.hint": "
|
|
974
|
-
"memory.features.preInjectGate.enabled": "
|
|
975
|
-
"memory.features.preInjectGate.enabled.hint": "
|
|
976
|
-
"memory.features.preInjectGate.enforce": "
|
|
977
|
-
"memory.features.preInjectGate.enforce.hint": "
|
|
1213
|
+
"memory.features.writeAdmission.enabled": "Judge blank / noise writes before storing (audit only)",
|
|
1214
|
+
"memory.features.writeAdmission.enabled.hint": "On: blank / noise writes are flagged with an audit row but still stored. Off: nothing is judged. To actually block, see the next switch (write admission checks, off by default)",
|
|
1215
|
+
"memory.features.writeAdmission.enforce": "Really reject useless writes",
|
|
1216
|
+
"memory.features.writeAdmission.enforce.hint": "On: flagged writes are rejected outright. Off: audit row only, stored anyway. Needs the checks switch above (write admission enforcement, off by default)",
|
|
1217
|
+
"memory.features.preInjectGate.enabled": "Screen out \"just my opinion\" memories before injection (audit only)",
|
|
1218
|
+
"memory.features.preInjectGate.enabled.hint": "On: opinion / stance memories are judged before injection and audited, so you can watch the distribution first. Off: injected as usual (pre-inject gate, off by default)",
|
|
1219
|
+
"memory.features.preInjectGate.enforce": "Really drop flagged candidates at injection",
|
|
1220
|
+
"memory.features.preInjectGate.enforce.hint": "On: flagged candidates are dropped at injection time. Off: audit row only, injected anyway. Needs the gate switch above (pre-inject gate enforcement, off by default)",
|
|
978
1221
|
// #164 A2: its own gate for the secret / PII rules (separate from the
|
|
979
1222
|
// blank/noise pair above), and it runs inside the write admission checks
|
|
980
1223
|
// above, so with those off it never runs. Hits are audited only;
|
|
981
1224
|
// enforcement stays with the key above.
|
|
982
|
-
"memory.features.sensitiveScanEnabled": "
|
|
983
|
-
"memory.features.sensitiveScanEnabled.hint": "
|
|
984
|
-
"memory.features.bm25SearchEnabled": "
|
|
985
|
-
"memory.features.bm25SearchEnabled.hint": "
|
|
986
|
-
"memory.features.conflictFreezeEnabled": "
|
|
987
|
-
"memory.features.conflictFreezeEnabled.hint": "
|
|
988
|
-
"memory.features.scopeEnabled": "
|
|
989
|
-
"memory.features.scopeEnabled.hint": "
|
|
990
|
-
"memory.features.strictScope": "
|
|
991
|
-
"memory.features.strictScope.hint": "
|
|
992
|
-
"memory.features.trustEpistemicWeighting": "
|
|
993
|
-
"memory.features.trustEpistemicWeighting.hint": "
|
|
994
|
-
"memory.features.reflectionFailureTracking": "
|
|
995
|
-
"memory.features.reflectionFailureTracking.hint": "
|
|
1225
|
+
"memory.features.sensitiveScanEnabled": "Scan writes for credentials and personal data",
|
|
1226
|
+
"memory.features.sensitiveScanEnabled.hint": "On: secret / PII rules run inside the write admission checks; hits are audited only — to block, also enable enforcement. Nothing runs while the checks switch is off (secret / PII scan, off by default)",
|
|
1227
|
+
"memory.features.bm25SearchEnabled": "Also search by exact keywords",
|
|
1228
|
+
"memory.features.bm25SearchEnabled.hint": "On: exact-term scoring joins in, so proper nouns, error codes and paths hit reliably — a complement to meaning-based search. Off: semantic similarity only (BM25 keyword search, on by default)",
|
|
1229
|
+
"memory.features.conflictFreezeEnabled": "Freeze contradictory memories first",
|
|
1230
|
+
"memory.features.conflictFreezeEnabled.hint": "On: contradictions found while tidying are frozen for your confirmation instead of both staying live. Off: both stay in use (conflict freezing, off by default)",
|
|
1231
|
+
"memory.features.scopeEnabled": "Record which session each memory came from",
|
|
1232
|
+
"memory.features.scopeEnabled.hint": "On: every new memory is tagged with the agent / workspace it came from — that tag decides whether other sessions see it. Off: new memories carry no origin and all sessions share one pool (scope tagging, off by default)",
|
|
1233
|
+
"memory.features.strictScope": "Memories others marked private stay invisible to me",
|
|
1234
|
+
"memory.features.strictScope.hint": "On: memories explicitly made private by other sessions are fully invisible. Off: they stay visible but rank lower. For true isolation use a memory's sensitivity field. Needs the tagging switch above (strict isolation, off by default)",
|
|
1235
|
+
"memory.features.trustEpistemicWeighting": "Rank by source credibility",
|
|
1236
|
+
"memory.features.trustEpistemicWeighting.hint": "On: more credible sources rank higher. Off: everything counts the same (credibility weighting, experimental, off by default)",
|
|
1237
|
+
"memory.features.reflectionFailureTracking": "Keep failed tidying cases on record",
|
|
1238
|
+
"memory.features.reflectionFailureTracking.hint": "On: failure causes and cases are stored for diagnosis and improvement. Off: failures leave no record (failure tracking, on by default)",
|
|
996
1239
|
"memory.features.embedRoute": "Semantic search route",
|
|
997
1240
|
"memory.features.embedProvider": "Embedding provider",
|
|
998
1241
|
"memory.features.embedProvider.openai": "OpenAI-compatible API",
|
|
@@ -1020,6 +1263,7 @@ window.__ModuleLoader__.load({
|
|
|
1020
1263
|
"memory.features.modelTestOk": "Connected",
|
|
1021
1264
|
"memory.features.modelTestFail": "Test failed",
|
|
1022
1265
|
"memory.features.modelTestHint": "Fires one minimal consolidation call to verify provider/model connectivity and effort support",
|
|
1266
|
+
"memory.features.modelTestDowngraded": "Effort not accepted by this model; connected without it (same as a real consolidation)",
|
|
1023
1267
|
"memory.features.routeStaleMark": " (not in list)",
|
|
1024
1268
|
"memory.features.routeStaleHint": "This value is not in the currently available list (often a leftover from switching providers): reselect, or run a connectivity test to verify now",
|
|
1025
1269
|
"memory.explorer.viewCards": "Cards",
|
|
@@ -1044,6 +1288,20 @@ window.__ModuleLoader__.load({
|
|
|
1044
1288
|
"memory.explorer.imported": "Merged {n} edits",
|
|
1045
1289
|
"memory.explorer.importFailed": "Import failed",
|
|
1046
1290
|
"memory.explorer.importNoFile": "Choose a file first",
|
|
1291
|
+
"memory.explorer.newMemory": "New memory…",
|
|
1292
|
+
"memory.explorer.newTitle": "New memory",
|
|
1293
|
+
"memory.explorer.newHint": "Write one memory by hand. Same type + same title merges into the existing entry instead of minting a second row.",
|
|
1294
|
+
"memory.explorer.newType": "Type",
|
|
1295
|
+
"memory.explorer.newTitlePlaceholder": "Short title; also the dedupe key",
|
|
1296
|
+
"memory.explorer.newContentPlaceholder": "Body",
|
|
1297
|
+
"memory.explorer.newTagsPlaceholder": "Tags, comma separated (optional)",
|
|
1298
|
+
"memory.explorer.newConfirm": "Create",
|
|
1299
|
+
"memory.explorer.newSaving": "Saving…",
|
|
1300
|
+
"memory.explorer.newCreated": "Created",
|
|
1301
|
+
"memory.explorer.newMerged": "Merged into the existing entry with that title",
|
|
1302
|
+
"memory.explorer.newFailed": "Create failed",
|
|
1303
|
+
"memory.explorer.newNoTitle": "Title is required",
|
|
1304
|
+
"memory.explorer.newNoContent": "Body is required",
|
|
1047
1305
|
"memory.explorer.exportFailed": "Export failed",
|
|
1048
1306
|
"memory.explorer.conflictBadge": "Conflict",
|
|
1049
1307
|
"memory.explorer.archivedBadge": "Archived",
|
|
@@ -1110,6 +1368,14 @@ window.__ModuleLoader__.load({
|
|
|
1110
1368
|
"memory.status.conflictQueue.similarity": "similarity",
|
|
1111
1369
|
"memory.status.conflictQueue.diffTitle": "Differences highlighted: red = removed, green = added",
|
|
1112
1370
|
"memory.status.conflictQueue.diffFallback": "The two texts differ substantially — compare the originals",
|
|
1371
|
+
"memory.status.conflictQueue.expand": "Expand full text",
|
|
1372
|
+
"memory.status.conflictQueue.collapse": "Collapse",
|
|
1373
|
+
"memory.status.conflictQueue.copySide": "Copy this side",
|
|
1374
|
+
"memory.status.conflictQueue.copiedSide": "Full text copied",
|
|
1375
|
+
"memory.status.conflictQueue.importance": "importance {n}",
|
|
1376
|
+
"memory.status.conflictQueue.chars": "{n} chars",
|
|
1377
|
+
"memory.status.conflictQueue.updated": "updated {time}",
|
|
1378
|
+
"memory.status.conflictQueue.created": "created {time}",
|
|
1113
1379
|
"memory.status.conflictQueue.emptyTitle": "No pending conflicts",
|
|
1114
1380
|
"memory.status.conflictQueue.emptyBody": "When consolidation finds two contradictory or highly similar memories, it freezes them here for your review instead of changing anything automatically. Neither side participates in injection until resolved.",
|
|
1115
1381
|
"memory.status.conflictQueue.applyHintTitle": "On confirm: a veto note is appended to the kept side and the other side is archived",
|
|
@@ -1191,8 +1457,62 @@ window.__ModuleLoader__.load({
|
|
|
1191
1457
|
"memory.status.mirror.foot": "Sync state of the on-disk Markdown mirror of memory entries",
|
|
1192
1458
|
"memory.status.sec.overview": "Library",
|
|
1193
1459
|
"memory.status.sec.engine": "Background activity",
|
|
1194
|
-
"memory.status.
|
|
1195
|
-
"memory.status.
|
|
1460
|
+
"memory.status.sec.maintenance": "Maintenance",
|
|
1461
|
+
"memory.status.maint": "Storage reclaim",
|
|
1462
|
+
"memory.status.maint.freelist": "Free pages reclaimable now",
|
|
1463
|
+
"memory.status.maint.plannedInputs": "Input snapshots to clear",
|
|
1464
|
+
"memory.status.maint.plannedEmbeddings": "Archived embeddings to clear",
|
|
1465
|
+
"memory.status.maint.window": "Retention window",
|
|
1466
|
+
"memory.status.maint.windowUnit": "days",
|
|
1467
|
+
"memory.status.maint.windowLabel": "Retention window (days)",
|
|
1468
|
+
"memory.status.maint.reestimate": "Re-estimate",
|
|
1469
|
+
"memory.status.maint.reestimating": "Estimating…",
|
|
1470
|
+
"memory.status.maint.stale": "Window changed - re-estimate before running, or the run would use the previous estimate's numbers.",
|
|
1471
|
+
"memory.status.maint.vacuum": "VACUUM as well",
|
|
1472
|
+
"memory.status.maint.vacuumHint": "VACUUM briefly takes an exclusive lock on the database; other reads and writes wait. Skipping it does not change what gets cleared.",
|
|
1473
|
+
"memory.status.maint.execute": "Run reclaim",
|
|
1474
|
+
"memory.status.maint.confirm": "Confirm (irreversible)",
|
|
1475
|
+
"memory.status.maint.running": "Running…",
|
|
1476
|
+
"memory.status.maint.cancel": "Cancel",
|
|
1477
|
+
"memory.status.maint.confirmHint": "This clears {inputs} input-snapshot rows and {embeddings} archived-embedding rows. It cannot be undone.",
|
|
1478
|
+
"memory.status.maint.badWindow": "The retention window must be an integer of at least 0",
|
|
1479
|
+
"memory.status.maint.failed": "Reclaim failed",
|
|
1480
|
+
"memory.status.maint.done": "Reclaim finished",
|
|
1481
|
+
"memory.status.maint.size": "Size (before → after)",
|
|
1482
|
+
"memory.status.maint.reclaimed": "Actually freed",
|
|
1483
|
+
"memory.status.maint.clearedInputs": "Input snapshots cleared",
|
|
1484
|
+
"memory.status.maint.clearedEmbeddings": "Archived embeddings cleared",
|
|
1485
|
+
"memory.status.maint.receipt": "Audit receipt",
|
|
1486
|
+
"memory.status.maint.receiptNone": "Not recorded (audit disabled or receipt write failed)",
|
|
1487
|
+
"memory.status.maint.duration": "Duration",
|
|
1488
|
+
"memory.status.maint.vacuumRan": "VACUUM ran",
|
|
1489
|
+
"memory.status.maint.vacuumOff": "Not VACUUMed",
|
|
1490
|
+
"memory.status.maint.vacuumFailed": "VACUUM failed",
|
|
1491
|
+
"memory.status.maint.vacuumFailedHint": "The reclaim already completed and cannot be undone, but the file will not shrink yet; run it again later (reclaim is idempotent).",
|
|
1492
|
+
"memory.status.sec.token": "Token flow · last 7 days",
|
|
1493
|
+
"memory.status.token.in": "input",
|
|
1494
|
+
"memory.status.token.out": "output",
|
|
1495
|
+
"memory.status.token.total": "7-day token total",
|
|
1496
|
+
"memory.status.token.calls": "Calls",
|
|
1497
|
+
"memory.status.token.callsCap": "Last 7 days · all background components",
|
|
1498
|
+
"memory.status.token.share": "Consolidation share",
|
|
1499
|
+
"memory.status.token.shareFoot": "dream_consolidate · {n} token",
|
|
1500
|
+
"memory.status.token.conflicts": "Pending conflicts",
|
|
1501
|
+
"memory.status.token.conflictsFoot": "From /dream-status · live",
|
|
1502
|
+
"memory.status.token.donut": "By component · who spent these tokens",
|
|
1503
|
+
"memory.status.token.days": "By day · 7-day trend",
|
|
1504
|
+
"memory.status.token.heat": "8-week heat · daily spend",
|
|
1505
|
+
"memory.status.token.heatLess": "less",
|
|
1506
|
+
"memory.status.token.heatMore": "more",
|
|
1507
|
+
"memory.status.token.last7": "last 7 days",
|
|
1508
|
+
"memory.status.token.meterOnly": "(metered only)",
|
|
1509
|
+
"memory.status.token.detail": "Detailed status cards (the original 9 under Library / Background activity, click to expand)",
|
|
1510
|
+
"memory.status.token.op.dream_consolidate": "Memory consolidation",
|
|
1511
|
+
"memory.status.token.op.summarize_compress": "Session distillation",
|
|
1512
|
+
"memory.status.token.op.entity_extract": "Entity extraction",
|
|
1513
|
+
"memory.status.token.op.dream_summarize": "Overview summary",
|
|
1514
|
+
"memory.status.token.op.sleep_pattern": "Sleep patterns",
|
|
1515
|
+
"memory.status.token.op.write_admission": "Write admission",
|
|
1196
1516
|
"memory.status.error": "Failed to load"
|
|
1197
1517
|
}
|
|
1198
1518
|
};
|
|
@@ -1253,18 +1573,50 @@ window.__ModuleLoader__.load({
|
|
|
1253
1573
|
// --- shared controls ---
|
|
1254
1574
|
".mneme-search{box-sizing:border-box;height:30px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;transition:border-color .12s,box-shadow .12s}",
|
|
1255
1575
|
".mneme-search:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
1256
|
-
".mneme-search::placeholder{color:var(--
|
|
1576
|
+
".mneme-search::placeholder{color:var(--mneme-fg-dim)}",
|
|
1257
1577
|
".mneme-chip{height:26px;padding:0 10px;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
|
|
1258
1578
|
".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1259
|
-
".mneme-chip.mneme-active{color:var(--
|
|
1579
|
+
".mneme-chip.mneme-active{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
1260
1580
|
".mneme-select{box-sizing:border-box;height:30px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none}",
|
|
1261
1581
|
".mneme-routeselect{width:240px;max-width:60%}",
|
|
1262
1582
|
".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}",
|
|
1263
1583
|
".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1264
|
-
".mneme-hint{color:var(--
|
|
1265
|
-
".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--
|
|
1584
|
+
".mneme-hint{color:var(--mneme-fg-dim);padding:24px 0;text-align:center;font-size:13px}",
|
|
1585
|
+
".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--mneme-accent);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
|
|
1266
1586
|
".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
1267
1587
|
// --- main-area memory library page ---
|
|
1588
|
+
// 面板语义色层(#178):宿主 token 在浅色下给正文/次要文字只有 1.26–4.49
|
|
1589
|
+
// 的对比度,且面板还一直在引用 5 个根本不存在的宿主名(--dsw-alias-state-error
|
|
1590
|
+
// / -warning / -success / -warn / --dsw-alias-border),
|
|
1591
|
+
// 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组
|
|
1592
|
+
// 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。
|
|
1593
|
+
// 必须挂四个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body
|
|
1594
|
+
// (.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口
|
|
1595
|
+
// portal 到宿主侧边栏(.mneme-topentry)。
|
|
1596
|
+
".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{" +
|
|
1597
|
+
"--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
|
|
1598
|
+
"--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
1599
|
+
"--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
1600
|
+
"--mneme-danger:color-mix(in srgb,var(--dsw-alias-state-error-primary) 80%,var(--dsw-alias-label-primary));" +
|
|
1601
|
+
"--mneme-warn:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 65%,var(--dsw-alias-label-primary));" +
|
|
1602
|
+
"--mneme-ok:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 65%,var(--dsw-alias-label-primary));" +
|
|
1603
|
+
// Token 去向三屏(2026-10 可观测性改造):图表六色全部单层 color-mix、
|
|
1604
|
+
// 基准只用宿主 token,test/contrast.test.js GRAPHICS 逐 selector 锁浅/深
|
|
1605
|
+
// ≥3:1。配比按 2026-10 审计数据校准(巩固 37% 是最大弧,占最深主色)。
|
|
1606
|
+
"--mneme-chart-1:color-mix(in srgb,var(--dsw-alias-state-business-primary) 70%,var(--dsw-alias-label-primary));" +
|
|
1607
|
+
"--mneme-chart-2:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 55%,var(--dsw-alias-label-primary));" +
|
|
1608
|
+
"--mneme-chart-3:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 60%,var(--dsw-alias-label-primary));" +
|
|
1609
|
+
"--mneme-chart-4:color-mix(in srgb,var(--dsw-alias-state-error-primary) 65%,var(--dsw-alias-label-primary));" +
|
|
1610
|
+
"--mneme-chart-5:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 45%,var(--dsw-alias-state-business-primary));" +
|
|
1611
|
+
// chart-6 与 graphic-dim 同款但内联写:对比度解析器不递归 --mneme-* 变量。
|
|
1612
|
+
"--mneme-chart-6:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
1613
|
+
// 热力档位用 label 补余而非 transparent:transparent 混色在浅色主题下
|
|
1614
|
+
// 低档位对白底不足 3:1;label 补余两套主题各档都过线且单调。
|
|
1615
|
+
"--mneme-heat-1:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
1616
|
+
"--mneme-heat-2:color-mix(in srgb,var(--dsw-alias-state-business-primary) 70%,var(--dsw-alias-label-primary));" +
|
|
1617
|
+
"--mneme-heat-3:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,var(--dsw-alias-label-primary));" +
|
|
1618
|
+
"--mneme-heat-4:color-mix(in srgb,var(--dsw-alias-state-business-primary) 40%,var(--dsw-alias-label-primary))" +
|
|
1619
|
+
"}",
|
|
1268
1620
|
".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);container-type:inline-size}",
|
|
1269
1621
|
// 窄容器(better-sidebar 工作台 ~500px)下中央 vtabs 会与右缘悬浮的
|
|
1270
1622
|
// 卡片/时间线切换器重叠:容器查询收窄时隐藏切换器,视图模式沿用
|
|
@@ -1273,14 +1625,14 @@ window.__ModuleLoader__.load({
|
|
|
1273
1625
|
".mneme-xbar{position:relative;flex:none;display:flex;align-items:center;justify-content:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px;min-height:52px}",
|
|
1274
1626
|
".mneme-filterbar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
1275
1627
|
".mneme-vtabs{display:flex;align-items:stretch;height:46px}",
|
|
1276
|
-
".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--
|
|
1628
|
+
".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--mneme-fg-dim);font-family:inherit;font-size:14px;font-weight:500;line-height:20px;display:inline-flex;align-items:center;gap:7px}",
|
|
1277
1629
|
".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}",
|
|
1278
|
-
".mneme-vtab.mneme-active{color:var(--
|
|
1630
|
+
".mneme-vtab.mneme-active{color:var(--mneme-accent)}",
|
|
1279
1631
|
".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:10px;right:10px;bottom:-1px;height:2.5px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}",
|
|
1280
1632
|
// transform 会自建层叠上下文:层级必须给在容器上——3 高于吸顶月份头(2)、
|
|
1281
1633
|
// 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。
|
|
1282
1634
|
".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}",
|
|
1283
|
-
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--
|
|
1635
|
+
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
1284
1636
|
// --- three-column browse layout: hairline separators, no outer box ---
|
|
1285
1637
|
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
1286
1638
|
".mneme-xside{flex:none;width:236px;min-width:0;min-height:0;overflow-y:auto;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
|
|
@@ -1289,23 +1641,23 @@ window.__ModuleLoader__.load({
|
|
|
1289
1641
|
".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
1290
1642
|
".mneme-xsearch{width:100%}",
|
|
1291
1643
|
".mneme-xselect{flex:1;min-width:0}",
|
|
1292
|
-
".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--
|
|
1644
|
+
".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--mneme-fg-dim);padding:2px 8px 8px}",
|
|
1293
1645
|
".mneme-xtype{display:flex;justify-content:flex-start;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
|
|
1294
1646
|
".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1295
1647
|
".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
1296
|
-
".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--
|
|
1648
|
+
".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
1297
1649
|
".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
|
|
1298
1650
|
".mneme-xmonth:first-child{margin-top:0}",
|
|
1299
1651
|
".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1300
|
-
".mneme-xcaret{flex:none;font-size:10px;color:var(--
|
|
1301
|
-
".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--
|
|
1652
|
+
".mneme-xcaret{flex:none;font-size:10px;color:var(--mneme-fg-dim);width:10px}",
|
|
1653
|
+
".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);cursor:pointer;text-align:left}",
|
|
1302
1654
|
".mneme-xday:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1303
1655
|
".mneme-xitem{display:flex;gap:8px;align-items:baseline;width:calc(100% - 22px);margin-left:22px;padding:4px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
|
|
1304
1656
|
".mneme-xitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1305
1657
|
".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
1306
|
-
".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--
|
|
1658
|
+
".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
1307
1659
|
".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1308
|
-
".mneme-xempty{padding:18px 16px;text-align:center;color:var(--
|
|
1660
|
+
".mneme-xempty{padding:18px 16px;text-align:center;color:var(--mneme-fg-dim);font-size:13px}",
|
|
1309
1661
|
// --- memory-type color dots (mirrored across filter / timeline / detail) ---
|
|
1310
1662
|
".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
|
|
1311
1663
|
".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
|
|
@@ -1317,7 +1669,7 @@ window.__ModuleLoader__.load({
|
|
|
1317
1669
|
".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
|
|
1318
1670
|
".mneme-xdinner{max-width:720px}",
|
|
1319
1671
|
".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
|
|
1320
|
-
".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--
|
|
1672
|
+
".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
|
|
1321
1673
|
".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
|
|
1322
1674
|
".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
|
|
1323
1675
|
".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
|
|
@@ -1330,42 +1682,44 @@ window.__ModuleLoader__.load({
|
|
|
1330
1682
|
".mneme-gnode:hover circle{stroke-width:4}",
|
|
1331
1683
|
".mneme-gnode.mneme-groot circle{stroke:var(--dsw-alias-state-business-primary);stroke-width:3}",
|
|
1332
1684
|
".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}",
|
|
1333
|
-
".mneme-gedge{stroke:var(--
|
|
1685
|
+
".mneme-gedge{stroke:var(--mneme-graphic-dim);stroke-width:1.5;cursor:pointer}",
|
|
1334
1686
|
".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}",
|
|
1335
|
-
|
|
1336
|
-
|
|
1687
|
+
// 虚线态靠 dash 图案区分,不再叠 opacity:50% 的描边再乘 .7 后浅色画布上
|
|
1688
|
+
// 只剩 2.28,低于图形要求的 3:1(图案已经承担了「没有 memory_id」这个语义)。
|
|
1689
|
+
".mneme-gedge-dashed{stroke-dasharray:5 4}",
|
|
1690
|
+
".mneme-graphhint{flex:none;color:var(--mneme-fg-dim);font-size:13px;text-align:center;padding:6px 0 2px}",
|
|
1337
1691
|
".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}",
|
|
1338
1692
|
".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
|
|
1339
|
-
".mneme-gs-meta{font-size:13px;color:var(--
|
|
1693
|
+
".mneme-gs-meta{font-size:13px;color:var(--mneme-fg-dim);margin-bottom:6px}",
|
|
1340
1694
|
".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
|
|
1341
|
-
".mneme-gs-attrkey{flex:none;color:var(--
|
|
1695
|
+
".mneme-gs-attrkey{flex:none;color:var(--mneme-fg-dim);font-size:13px}",
|
|
1342
1696
|
".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
|
|
1343
1697
|
".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}",
|
|
1344
1698
|
".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1345
1699
|
// --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
|
|
1346
1700
|
".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
1347
1701
|
".mneme-entrail{flex:none;width:224px;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:4px}",
|
|
1348
|
-
".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--
|
|
1702
|
+
".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
|
|
1349
1703
|
".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
|
|
1350
1704
|
".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1351
1705
|
".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
1352
1706
|
".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1353
|
-
".mneme-entmentions{flex:none;font-size:11px;color:var(--
|
|
1707
|
+
".mneme-entmentions{flex:none;font-size:11px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
1354
1708
|
".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}",
|
|
1355
1709
|
".mneme-entinner{max-width:720px}",
|
|
1356
1710
|
".mneme-enttitle{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
1357
1711
|
".mneme-enttitle .mneme-xdot{width:10px;height:10px}",
|
|
1358
|
-
".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--
|
|
1712
|
+
".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
|
|
1359
1713
|
".mneme-layer{margin-top:24px}",
|
|
1360
1714
|
".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
|
|
1361
|
-
".mneme-layercount{font-size:12px;font-weight:400;color:var(--
|
|
1715
|
+
".mneme-layercount{font-size:12px;font-weight:400;color:var(--mneme-fg-dim)}",
|
|
1362
1716
|
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
|
|
1363
1717
|
".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
|
|
1364
|
-
".mneme-attrkey{font-size:11px;color:var(--
|
|
1718
|
+
".mneme-attrkey{font-size:11px;color:var(--mneme-fg-dim);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1365
1719
|
".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
1366
1720
|
".mneme-relrow{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:none;border-radius:10px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:20px;text-align:left}",
|
|
1367
1721
|
".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1368
|
-
".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--
|
|
1722
|
+
".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--mneme-accent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}",
|
|
1369
1723
|
".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1370
1724
|
// --- settings: quiet stacked sections, one concern per section ---
|
|
1371
1725
|
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
@@ -1373,25 +1727,25 @@ window.__ModuleLoader__.load({
|
|
|
1373
1727
|
// 设置页各节一律用 boxed 卡(.mneme-set-card)+ 分组标题(.mneme-set-grouphead):
|
|
1374
1728
|
// 旧的「无框 set-sec + 有框 set-card」混排是版面杂乱的一半来源
|
|
1375
1729
|
".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
1376
|
-
".mneme-set-desc{font-size:13px;line-height:19px;color:var(--
|
|
1730
|
+
".mneme-set-desc{font-size:13px;line-height:19px;color:var(--mneme-fg-dim);margin-bottom:14px}",
|
|
1377
1731
|
".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}",
|
|
1378
1732
|
".mneme-set-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
1379
|
-
".mneme-set-input::placeholder{color:var(--
|
|
1733
|
+
".mneme-set-input::placeholder{color:var(--mneme-fg-dim)}",
|
|
1380
1734
|
".mneme-set-input--area{height:auto;min-height:76px;padding:8px 12px;resize:vertical;line-height:1.6}",
|
|
1381
1735
|
".mneme-btn{flex:none;height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center;gap:6px}",
|
|
1382
1736
|
".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1383
1737
|
".mneme-btn:disabled{opacity:.5;cursor:default}",
|
|
1384
|
-
".mneme-saved{font-size:12px;color:var(--
|
|
1738
|
+
".mneme-saved{font-size:12px;color:var(--mneme-ok)}",
|
|
1385
1739
|
// rule rows: numbered chips + hover-revealed delete
|
|
1386
1740
|
".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}",
|
|
1387
|
-
".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--
|
|
1741
|
+
".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--mneme-fg-dim);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
|
|
1388
1742
|
".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
1389
|
-
".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--
|
|
1743
|
+
".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
|
|
1390
1744
|
".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}",
|
|
1391
|
-
".mneme-set-del:hover{color:var(--
|
|
1745
|
+
".mneme-set-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1392
1746
|
".mneme-set-cmd{flex:1;min-width:0}",
|
|
1393
|
-
".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--
|
|
1394
|
-
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--
|
|
1747
|
+
".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--mneme-accent)}",
|
|
1748
|
+
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
|
|
1395
1749
|
// --- settings sub-view ---
|
|
1396
1750
|
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
1397
1751
|
// --- 记忆库页面:居中 sheet,非全屏 ---
|
|
@@ -1411,9 +1765,9 @@ window.__ModuleLoader__.load({
|
|
|
1411
1765
|
".mneme-stars{display:inline-flex;align-items:center;gap:2px}",
|
|
1412
1766
|
// 热度徽章三档:热(橙)/温(次级文字)/冷(弱化文字),冷档藏百分比只留图标。
|
|
1413
1767
|
".mneme-heat{display:inline-flex;align-items:center;gap:2px;font-size:11px;line-height:14px;font-variant-numeric:tabular-nums}",
|
|
1414
|
-
".mneme-heat--hot{color:var(--
|
|
1768
|
+
".mneme-heat--hot{color:var(--mneme-warn)}",
|
|
1415
1769
|
".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
|
|
1416
|
-
".mneme-heat--cold{color:var(--
|
|
1770
|
+
".mneme-heat--cold{color:var(--mneme-fg-dim)}",
|
|
1417
1771
|
".mneme-heat--cold .mneme-heatpct{display:none}",
|
|
1418
1772
|
// 侧边栏入口两种形态的样式(挂载与搬移逻辑见 SidebarTopEntry 的头注释)。
|
|
1419
1773
|
// 展开态兜底:逐条镜像宿主 panelRow(36px 行高 / 透明底 / 2px 外边距 /
|
|
@@ -1436,12 +1790,12 @@ window.__ModuleLoader__.load({
|
|
|
1436
1790
|
// 菜单让位只在「收起 + 入口确实挂在标题栏里」时生效;展开态完全不碰宿主变量。
|
|
1437
1791
|
"html[data-windows-titlebar]:has([data-sidebar-collapsed=true]):has([data-plugin-entry=\"@modusensus/dsh-mneme\"]){--dsh-windows-menu-start:120px}",
|
|
1438
1792
|
// --- 功能开关:一行一开关,Claude 式安静排版 ---
|
|
1439
|
-
".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--
|
|
1793
|
+
".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:16px 0 2px}",
|
|
1440
1794
|
".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
1441
1795
|
".mneme-featrow:last-child{border-bottom:none}",
|
|
1442
1796
|
".mneme-featmain{flex:1;min-width:0}",
|
|
1443
1797
|
".mneme-featname{font-size:13.5px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-primary)}",
|
|
1444
|
-
".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--
|
|
1798
|
+
".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--mneme-fg-dim);margin-top:2px}",
|
|
1445
1799
|
".mneme-switch{position:relative;flex:none;width:36px;height:22px;border-radius:11px;border:none;cursor:pointer;background:var(--dsw-alias-interactive-bg-active);transition:background .15s}",
|
|
1446
1800
|
".mneme-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 3px color-mix(in srgb,var(--dsw-alias-label-primary) 25%,transparent);transition:transform .15s}",
|
|
1447
1801
|
".mneme-switch.mneme-on{background:var(--dsw-alias-state-business-primary)}",
|
|
@@ -1457,14 +1811,14 @@ window.__ModuleLoader__.load({
|
|
|
1457
1811
|
".mneme-strinput:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
1458
1812
|
".mneme-featsub{margin:2px 0 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;display:flex;flex-direction:column;gap:6px}",
|
|
1459
1813
|
".mneme-featsub .mneme-featnum{padding:6px 0;border-bottom:none}",
|
|
1460
|
-
".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--
|
|
1814
|
+
".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
|
|
1461
1815
|
// 运行时那一块的说明文字:比常规 hint 再小一档,它是背景信息而不是要读的正文。
|
|
1462
|
-
".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--
|
|
1816
|
+
".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
1463
1817
|
// --- 交互式记忆库:工具栏 / 卡片网格 / 详情抽屉 / 更多菜单 ---
|
|
1464
1818
|
// sheet 边距放宽:与窗口边缘保持呼吸距离,居中不顶满。
|
|
1465
1819
|
// 视图切换(卡片/时间线)分段控件挂在子页栏右侧。
|
|
1466
1820
|
".mneme-seg{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}",
|
|
1467
|
-
".mneme-seg button{border:none;background:none;color:var(--
|
|
1821
|
+
".mneme-seg button{border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}",
|
|
1468
1822
|
".mneme-seg button:hover{color:var(--dsw-alias-label-primary)}",
|
|
1469
1823
|
".mneme-seg button.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
1470
1824
|
// 卡片网格:响应式 auto-fill,卡片悬停轻浮起
|
|
@@ -1472,53 +1826,68 @@ window.__ModuleLoader__.load({
|
|
|
1472
1826
|
".mneme-card{display:flex;flex-direction:column;gap:7px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 13px;background:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .12s,box-shadow .12s,transform .12s}",
|
|
1473
1827
|
".mneme-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,var(--dsw-alias-border-l2));box-shadow:0 6px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);transform:translateY(-1px)}",
|
|
1474
1828
|
".mneme-card.mneme-active{border-color:var(--dsw-alias-state-business-primary)}",
|
|
1475
|
-
".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--
|
|
1829
|
+
".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
1476
1830
|
".mneme-cardtitle{font-size:13.5px;font-weight:600;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}",
|
|
1477
1831
|
".mneme-cardexcerpt{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}",
|
|
1478
|
-
".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--
|
|
1832
|
+
".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
1479
1833
|
".mneme-cardsrc{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1480
1834
|
// 徽章:冲突待确认 / 已归档
|
|
1481
1835
|
".mneme-badge{flex:none;display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 7px;border-radius:6px;font-size:11px;line-height:14px}",
|
|
1482
|
-
".mneme-badge--conflict{color:var(--
|
|
1483
|
-
".mneme-badge--archived{color:var(--
|
|
1484
|
-
".mneme-badge--scope{color:var(--
|
|
1836
|
+
".mneme-badge--conflict{color:var(--mneme-danger);background:color-mix(in srgb,var(--mneme-danger) 10%,transparent);border:1px solid color-mix(in srgb,var(--mneme-danger) 30%,transparent)}",
|
|
1837
|
+
".mneme-badge--archived{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1838
|
+
".mneme-badge--scope{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}",
|
|
1839
|
+
// 「默认 / 已改」徽标(#427):default 复用 --archived 的语义色组合
|
|
1840
|
+
// (fg-dim on interactive-bg-hover),changed 复用 --scope 的 accent 组
|
|
1841
|
+
// 合——两组配对都已过 contrast.test.js 的重算,不再发明新颜色。
|
|
1842
|
+
".mneme-badge--default{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1843
|
+
".mneme-badge--changed{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}",
|
|
1485
1844
|
// 冲突集中处理队列(v0.8.0):状态页的待确认冲突列表
|
|
1486
1845
|
".mneme-conflictq{display:flex;flex-direction:column;gap:10px}",
|
|
1487
1846
|
".mneme-conflictq-head{display:flex;align-items:center;gap:8px}",
|
|
1488
|
-
".mneme-conflict-item{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}",
|
|
1847
|
+
".mneme-conflict-item{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}",
|
|
1489
1848
|
".mneme-conflict-reason{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}",
|
|
1490
1849
|
".mneme-conflict-pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}",
|
|
1491
|
-
".mneme-conflict-side{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}",
|
|
1492
|
-
".mneme-conflict-sidelabel{font-size:11px;color:var(--
|
|
1850
|
+
".mneme-conflict-side{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}",
|
|
1851
|
+
".mneme-conflict-sidelabel{font-size:11px;color:var(--mneme-fg-dim)}",
|
|
1493
1852
|
".mneme-conflict-sidetitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1494
1853
|
".mneme-conflict-snippet{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}",
|
|
1495
|
-
".mneme-conflict-missing{font-size:12px;color:var(--
|
|
1854
|
+
".mneme-conflict-missing{font-size:12px;color:var(--mneme-fg-dim)}",
|
|
1496
1855
|
".mneme-conflict-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
1497
|
-
".mneme-conflict-hint{font-size:11px;color:var(--
|
|
1856
|
+
".mneme-conflict-hint{font-size:11px;color:var(--mneme-fg-dim);margin-right:auto}",
|
|
1498
1857
|
// #177 冲突队列视觉批次:A/B 侧色、相似度条、词级 diff、主色按钮、空态
|
|
1499
1858
|
".mneme-conflict-side--a{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 4%,transparent)}",
|
|
1500
|
-
".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--
|
|
1501
|
-
".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--
|
|
1502
|
-
".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--
|
|
1859
|
+
".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--mneme-accent);font-weight:600}",
|
|
1860
|
+
".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--mneme-warn) 45%,transparent);background:color-mix(in srgb,var(--mneme-warn) 5%,transparent)}",
|
|
1861
|
+
".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--mneme-warn);font-weight:600}",
|
|
1503
1862
|
".mneme-conflict-simrow{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary)}",
|
|
1504
1863
|
".mneme-conflict-simbar{position:relative;flex:1;height:5px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}",
|
|
1505
1864
|
".mneme-conflict-simfill{position:absolute;top:0;bottom:0;left:0;border-radius:3px;background:var(--dsw-alias-state-business-primary)}",
|
|
1506
1865
|
".mneme-conflict-diff{font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}",
|
|
1866
|
+
// 展开全文:取消行数钳制并给一个内部滚动区——报障点是「窗口太小又不能
|
|
1867
|
+
// 放大,完全对比不了内容」,所以展开态必须能容纳长文本且不撑破面板。
|
|
1868
|
+
// 上限用 em 而非 vh:宿主可能把面板放在零高视口里(隐藏 webview / 后台
|
|
1869
|
+
// 页),那时 46vh 会算成 0px,展开态直接看不见字——实测踩过。
|
|
1870
|
+
".mneme-conflict-diff--full{display:block;-webkit-line-clamp:unset;max-height:28em;overflow-y:auto}",
|
|
1871
|
+
".mneme-conflict-text{font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-height:28em;overflow-y:auto}",
|
|
1872
|
+
".mneme-conflict-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
1873
|
+
".mneme-conflict-metaval{color:var(--dsw-alias-label-secondary)}",
|
|
1874
|
+
// 窄面板下并排会把每侧挤到无法阅读,退成单列(容器查询按面板实际宽度判)
|
|
1875
|
+
"@container (max-width:560px){.mneme-conflict-pair{grid-template-columns:1fr}}",
|
|
1507
1876
|
".mneme-conflict-mark{text-decoration:none;border-radius:3px;padding:0 1px}",
|
|
1508
|
-
".mneme-conflict-mark--del{background:color-mix(in srgb,var(--
|
|
1509
|
-
".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--
|
|
1510
|
-
".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--
|
|
1877
|
+
".mneme-conflict-mark--del{background:color-mix(in srgb,var(--mneme-danger) 16%,transparent);text-decoration:line-through}",
|
|
1878
|
+
".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--mneme-ok) 18%,transparent)}",
|
|
1879
|
+
".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--mneme-accent);color:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
|
|
1511
1880
|
".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
|
|
1512
|
-
".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-
|
|
1513
|
-
".mneme-badge--frozen{color:var(--
|
|
1881
|
+
".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--mneme-accent) 85%,var(--dsw-alias-label-primary))}",
|
|
1882
|
+
".mneme-badge--frozen{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent)}",
|
|
1514
1883
|
".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
|
|
1515
1884
|
".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
1516
|
-
".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--
|
|
1517
|
-
".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--
|
|
1518
|
-
".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--
|
|
1885
|
+
".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
|
|
1886
|
+
".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}",
|
|
1887
|
+
".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1,#fff);font-size:10.5px;line-height:16px;font-weight:600;text-align:center;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1)}",
|
|
1519
1888
|
".mneme-triggerwrap{position:relative;pointer-events:none}",
|
|
1520
1889
|
".mneme-triggerwrap .mneme-trigger{pointer-events:auto}",
|
|
1521
|
-
".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--
|
|
1890
|
+
".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}",
|
|
1522
1891
|
".mneme-conflict-jump:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:12px}",
|
|
1523
1892
|
// 详情抽屉:sheet 内右侧滑出,覆盖在浏览区之上
|
|
1524
1893
|
".mneme-xmain{position:relative}",
|
|
@@ -1530,7 +1899,7 @@ window.__ModuleLoader__.load({
|
|
|
1530
1899
|
".mneme-drawertitle{width:100%;font-size:16px;font-weight:600;line-height:23px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
1531
1900
|
".mneme-drawertitle-input{width:100%;box-sizing:border-box;font-size:15px;font-weight:600;line-height:22px;padding:7px 10px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
1532
1901
|
".mneme-dmeta{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin-top:12px;font-size:12.5px;line-height:19px}",
|
|
1533
|
-
".mneme-dmetakey{color:var(--
|
|
1902
|
+
".mneme-dmetakey{color:var(--mneme-fg-dim)}",
|
|
1534
1903
|
".mneme-dmetaval{color:var(--dsw-alias-label-secondary);min-width:0;word-break:break-word}",
|
|
1535
1904
|
".mneme-dcontent{margin-top:14px;font-size:13.5px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
|
|
1536
1905
|
".mneme-dcontent-input{width:100%;box-sizing:border-box;min-height:180px;font-size:13.5px;line-height:1.7;padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;resize:vertical;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
@@ -1550,12 +1919,12 @@ window.__ModuleLoader__.load({
|
|
|
1550
1919
|
// --- explorer refresh: paged month tree, sticky headers, inline icons ---
|
|
1551
1920
|
".mneme-vtabico{flex:none;opacity:.85}",
|
|
1552
1921
|
".mneme-xsearchwrap{position:relative;flex:none}",
|
|
1553
|
-
".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--
|
|
1922
|
+
".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--mneme-fg-dim);pointer-events:none}",
|
|
1554
1923
|
".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}",
|
|
1555
1924
|
".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}",
|
|
1556
1925
|
".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}",
|
|
1557
|
-
".mneme-xmonthcount{margin-left:auto;color:var(--
|
|
1558
|
-
".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--
|
|
1926
|
+
".mneme-xmonthcount{margin-left:auto;color:var(--mneme-fg-dim);font-weight:400;font-variant-numeric:tabular-nums}",
|
|
1927
|
+
".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--mneme-fg-dim)}",
|
|
1559
1928
|
".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}",
|
|
1560
1929
|
".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}",
|
|
1561
1930
|
// --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) ---
|
|
@@ -1569,45 +1938,103 @@ window.__ModuleLoader__.load({
|
|
|
1569
1938
|
"@container (min-width:452px) and (max-width:683px){.mneme-statuscard--vectorwide{grid-column:span 2}}",
|
|
1570
1939
|
"@container (min-width:452px){.mneme-statuscard--wide{grid-column:span 2}}",
|
|
1571
1940
|
// 状态页分组标题(库内一览 / 后台运转):只做扫读锚点,压得比卡片标题低
|
|
1572
|
-
".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--
|
|
1941
|
+
".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim)}",
|
|
1573
1942
|
".mneme-statusgrid+.mneme-statushead{margin-top:26px}",
|
|
1574
1943
|
".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 16px 14px}",
|
|
1575
1944
|
// 指标行:一行一指标(label 左 / 数值右),取代「·」串联的说明长句——
|
|
1576
1945
|
// 拥挤感的根源是把多指标压进一段会换行的散文
|
|
1577
1946
|
".mneme-statrows{margin-top:10px;display:flex;flex-direction:column;gap:4px}",
|
|
1578
|
-
".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--
|
|
1947
|
+
".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
|
|
1579
1948
|
".mneme-statrow>span:first-child{flex:none;white-space:nowrap}",
|
|
1580
1949
|
".mneme-statrow .mneme-heatdot{align-self:center}",
|
|
1581
1950
|
".mneme-statrowval{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}",
|
|
1582
|
-
".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--
|
|
1951
|
+
".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
1583
1952
|
// 热度分布条:三档占比一图看清,行标只补数字(类名带双横线,与 heatdot 同款)
|
|
1584
1953
|
".mneme-heatbar{display:flex;height:6px;border-radius:3px;overflow:hidden;margin-top:12px;background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1585
1954
|
".mneme-heatbar-seg{min-width:2px}",
|
|
1586
|
-
".mneme-heatbar--hot{background:var(--
|
|
1587
|
-
".mneme-heatbar--warm{background:var(--
|
|
1588
|
-
".mneme-heatbar--cold{background:var(--
|
|
1955
|
+
".mneme-heatbar--hot{background:var(--mneme-warn)}",
|
|
1956
|
+
".mneme-heatbar--warm{background:var(--mneme-fg-dim)}",
|
|
1957
|
+
".mneme-heatbar--cold{background:var(--mneme-graphic-dim)}",
|
|
1589
1958
|
".mneme-heatdot{flex:none;width:8px;height:8px;border-radius:50%}",
|
|
1590
|
-
".mneme-heatdot--hot{background:var(--
|
|
1591
|
-
".mneme-heatdot--warm{background:var(--
|
|
1592
|
-
".mneme-heatdot--cold{background:var(--
|
|
1959
|
+
".mneme-heatdot--hot{background:var(--mneme-warn)}",
|
|
1960
|
+
".mneme-heatdot--warm{background:var(--mneme-fg-dim)}",
|
|
1961
|
+
".mneme-heatdot--cold{background:var(--mneme-graphic-dim)}",
|
|
1962
|
+
// --- Token 去向三屏(2026-10 可观测性改造):KPI 带 / donut / 7 天柱状 / 8 周热力 ---
|
|
1963
|
+
// KPI 带固定 4 列、容器收窄降 2 列:用 @container 不用 @media——面板同时长在
|
|
1964
|
+
// 宿主侧栏(~500px)与 sheet(1240px)里,决定布局的是容器宽度而非视口。
|
|
1965
|
+
".mneme-kpiband{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;max-width:1000px;margin:0 auto 12px}",
|
|
1966
|
+
"@container (max-width:560px){.mneme-kpiband{grid-template-columns:repeat(2,minmax(0,1fr))}}",
|
|
1967
|
+
".mneme-tokenflow{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 16px 14px}",
|
|
1968
|
+
".mneme-donutwrap{display:flex;gap:20px;align-items:center;margin-top:12px}",
|
|
1969
|
+
"@container (max-width:480px){.mneme-donutwrap{flex-direction:column;align-items:stretch}.mneme-donutbox{align-self:center}}",
|
|
1970
|
+
".mneme-donutbox{position:relative;flex:none;width:148px;height:148px}",
|
|
1971
|
+
".mneme-donutbox svg{width:100%;height:100%;display:block}",
|
|
1972
|
+
".mneme-donut-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}",
|
|
1973
|
+
".mneme-donut-center b{font-size:19px;font-weight:650;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}",
|
|
1974
|
+
".mneme-donut-center span{font-size:11px;color:var(--mneme-fg-dim);margin-top:1px}",
|
|
1975
|
+
// r=15.9155 让周长≈100,dasharray 直接填百分比;段缝靠 JS 侧留,这里只管观感。
|
|
1976
|
+
".mneme-donut circle.mneme-seg{fill:none;stroke-width:5.6;transition:opacity .15s}",
|
|
1977
|
+
".mneme-donut.has-focus circle.mneme-seg:not(.focused){opacity:.35}",
|
|
1978
|
+
".mneme-seg--1{stroke:var(--mneme-chart-1)}",
|
|
1979
|
+
".mneme-seg--2{stroke:var(--mneme-chart-2)}",
|
|
1980
|
+
".mneme-seg--3{stroke:var(--mneme-chart-3)}",
|
|
1981
|
+
".mneme-seg--4{stroke:var(--mneme-chart-4)}",
|
|
1982
|
+
".mneme-seg--5{stroke:var(--mneme-chart-5)}",
|
|
1983
|
+
".mneme-seg--6{stroke:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent)}",
|
|
1984
|
+
".mneme-legend{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}",
|
|
1985
|
+
".mneme-legendrow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;cursor:default;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-primary)}",
|
|
1986
|
+
".mneme-legendrow:hover,.mneme-legendrow.focused{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1987
|
+
".mneme-legendrow.dim{opacity:.45}",
|
|
1988
|
+
".mneme-legenddot{flex:none;width:10px;height:10px;border-radius:3px}",
|
|
1989
|
+
".mneme-dot--1{background:var(--mneme-chart-1)}",
|
|
1990
|
+
".mneme-dot--2{background:var(--mneme-chart-2)}",
|
|
1991
|
+
".mneme-dot--3{background:var(--mneme-chart-3)}",
|
|
1992
|
+
".mneme-dot--4{background:var(--mneme-chart-4)}",
|
|
1993
|
+
".mneme-dot--5{background:var(--mneme-chart-5)}",
|
|
1994
|
+
".mneme-dot--6{background:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent)}",
|
|
1995
|
+
".mneme-legendname{color:var(--dsw-alias-label-primary);white-space:nowrap}",
|
|
1996
|
+
".mneme-legendsub{color:var(--mneme-fg-dim);font-size:11px}",
|
|
1997
|
+
".mneme-legendval{margin-left:auto;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
1998
|
+
".mneme-legendpct{flex:none;width:46px;text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);font-weight:600}",
|
|
1999
|
+
".mneme-bars{position:relative;height:150px;margin-top:14px}",
|
|
2000
|
+
".mneme-gridline{position:absolute;left:0;right:0;border-top:1px dashed var(--dsw-alias-border-l2)}",
|
|
2001
|
+
".mneme-gridlabel{position:absolute;right:0;transform:translateY(-58%);font-size:10px;color:var(--mneme-fg-dim)}",
|
|
2002
|
+
".mneme-barcols{position:absolute;inset:0;display:flex;align-items:flex-end;gap:8px;padding:0 2px}",
|
|
2003
|
+
".mneme-barcol{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;height:100%}",
|
|
2004
|
+
".mneme-bar{border-radius:5px 5px 2px 2px;background:var(--mneme-accent);min-height:2px;transition:opacity .15s}",
|
|
2005
|
+
".mneme-barcol:hover .mneme-bar{opacity:.8}",
|
|
2006
|
+
".mneme-barlabels{display:flex;gap:8px;padding:6px 2px 0}",
|
|
2007
|
+
".mneme-barlabels span{flex:1;min-width:0;text-align:center;font-size:10.5px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
2008
|
+
".mneme-heatgrid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,12px);gap:3px;margin-top:12px}",
|
|
2009
|
+
".mneme-heatcell{width:12px;height:12px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2010
|
+
// 类名编号=视觉档位(l1 最浅…l4 最深),变量编号=business 占比降序,故交叉引用。
|
|
2011
|
+
".mneme-heatcell.l1{background:var(--mneme-heat-4)}",
|
|
2012
|
+
".mneme-heatcell.l2{background:var(--mneme-heat-3)}",
|
|
2013
|
+
".mneme-heatcell.l3{background:var(--mneme-heat-2)}",
|
|
2014
|
+
".mneme-heatcell.l4{background:var(--mneme-heat-1)}",
|
|
2015
|
+
".mneme-heatlegend{display:flex;align-items:center;gap:3px;margin-top:8px;font-size:10.5px;color:var(--mneme-fg-dim)}",
|
|
2016
|
+
".mneme-heatlegend .mneme-heatcell{width:10px;height:10px}",
|
|
2017
|
+
// 原 9 卡降级折叠区:默认收起,展开才挂载子卡(见 TokenFlowSection 注释)。
|
|
2018
|
+
".mneme-legacy{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 16px}",
|
|
2019
|
+
".mneme-legacy summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
1593
2020
|
// --- 状态页工作台:让用户看见插件在干活(动态/沉淀/归档) ---
|
|
1594
2021
|
".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px;display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
|
|
1595
2022
|
".mneme-wblist{max-width:1000px;display:flex;flex-direction:column;gap:6px}",
|
|
1596
2023
|
".mneme-wbrow{display:flex;gap:10px;align-items:flex-start;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
|
|
1597
|
-
".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--
|
|
1598
|
-
".mneme-wbdot--err{background:var(--
|
|
2024
|
+
".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--mneme-ok)}",
|
|
2025
|
+
".mneme-wbdot--err{background:var(--mneme-danger)}",
|
|
1599
2026
|
".mneme-wbmain{min-width:0;flex:1}",
|
|
1600
2027
|
".mneme-wbtitle{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
|
|
1601
|
-
".mneme-wbsub{font-size:12px;line-height:17px;color:var(--
|
|
2028
|
+
".mneme-wbsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px}",
|
|
1602
2029
|
".mneme-wbmemo{display:flex;gap:10px;align-items:center;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
|
|
1603
2030
|
".mneme-wbmemo .mneme-xdot{flex:none}",
|
|
1604
2031
|
".mneme-wbmemo-main{min-width:0;flex:1}",
|
|
1605
2032
|
".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
|
|
1606
|
-
".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--
|
|
2033
|
+
".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--mneme-fg-dim);word-break:break-word}",
|
|
1607
2034
|
// --- destructive actions: red outline = delete, solid red = confirm ---
|
|
1608
|
-
".mneme-btndanger{color:var(--
|
|
1609
|
-
".mneme-btndanger:hover{background:color-mix(in srgb,var(--
|
|
1610
|
-
".mneme-btndangerconfirm{background:var(--
|
|
2035
|
+
".mneme-btndanger{color:var(--mneme-danger);border-color:var(--mneme-danger)}",
|
|
2036
|
+
".mneme-btndanger:hover{background:color-mix(in srgb,var(--mneme-danger) 12%,transparent)}",
|
|
2037
|
+
".mneme-btndangerconfirm{background:var(--mneme-danger);border-color:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1)}",
|
|
1611
2038
|
".mneme-btndangerconfirm:hover{filter:brightness(.9)}",
|
|
1612
2039
|
// --- settings cards: boxed cards for the runtime-mode and external-API
|
|
1613
2040
|
// sections — each card owns its fetch/PUT state, so it renders as a
|
|
@@ -1615,14 +2042,14 @@ window.__ModuleLoader__.load({
|
|
|
1615
2042
|
".mneme-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;margin-bottom:10px}",
|
|
1616
2043
|
// 设置页分组标题(个性化 / 记忆引擎 / 搜索 / 连接与安全 / 帮助):
|
|
1617
2044
|
// 先给「这一段在管什么」,卡片才是具体选项
|
|
1618
|
-
".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--
|
|
2045
|
+
".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:24px 0 10px}",
|
|
1619
2046
|
".mneme-set-grouphead:first-child{margin-top:8px}",
|
|
1620
2047
|
".mneme-set-token{font-family:monospace;font-size:12px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);word-break:break-all;user-select:all}",
|
|
1621
|
-
".mneme-set-hint{font-size:12px;color:var(--
|
|
2048
|
+
".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}",
|
|
1622
2049
|
// --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) ---
|
|
1623
2050
|
".mneme-staredit{display:inline-flex;align-items:center;gap:1px;justify-self:start}",
|
|
1624
2051
|
".mneme-starbtn{border:none;background:none;cursor:pointer;padding:2px 1px;display:inline-flex;color:var(--dsw-alias-label-secondary);transition:transform .12s}",
|
|
1625
|
-
".mneme-starbtn--on{color:var(--
|
|
2052
|
+
".mneme-starbtn--on{color:var(--mneme-warn)}",
|
|
1626
2053
|
".mneme-starbtn:hover{transform:scale(1.12)}",
|
|
1627
2054
|
".mneme-starbtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-radius:4px}",
|
|
1628
2055
|
// --- 意见与反馈:图标行 + 版本 footer,收在设置页末尾当「关于」用 ---
|
|
@@ -1633,8 +2060,170 @@ window.__ModuleLoader__.load({
|
|
|
1633
2060
|
".mneme-feedrow:hover .mneme-feedico{background:var(--dsw-alias-interactive-bg-active)}",
|
|
1634
2061
|
".mneme-feedtext{display:flex;flex-direction:column;gap:1px;min-width:0}",
|
|
1635
2062
|
".mneme-feedname{font-size:13px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
1636
|
-
".mneme-feedsub{font-size:12px;line-height:17px;color:var(--
|
|
1637
|
-
".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--
|
|
2063
|
+
".mneme-feedsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
|
|
2064
|
+
".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2065
|
+
// --- 存储回收卡(#386 第 4 条):控制行 + 两步确认的警告块 ---
|
|
2066
|
+
".mneme-maintrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}",
|
|
2067
|
+
".mneme-maintlbl{flex:none;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
|
|
2068
|
+
".mneme-maintrow .mneme-set-input{flex:none;width:96px;margin-bottom:0}",
|
|
2069
|
+
".mneme-maintcheck{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
|
|
2070
|
+
".mneme-maintwarn{margin-top:12px;padding:10px 12px;border-radius:10px;border:1px solid color-mix(in srgb,var(--mneme-danger) 45%,transparent);background:color-mix(in srgb,var(--mneme-danger) 8%,transparent)}",
|
|
2071
|
+
".mneme-maintwarnhead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-danger)}",
|
|
2072
|
+
".mneme-maintresult{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
2073
|
+
".mneme-maintresulthead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-ok)}",
|
|
2074
|
+
".mneme-mainterr{margin-top:10px;font-size:12px;line-height:17px;color:var(--mneme-danger)}",
|
|
2075
|
+
// --- v0.9.0 M.O.(beta)---------------------------------------------
|
|
2076
|
+
// 语义色层第二份:浮层面板(portal 到 body)、右栏 pane、退化 sheet、
|
|
2077
|
+
// 对话行结算卡都不在原四个渲染根之下。contrast.test.js 对原四根选择器
|
|
2078
|
+
// 做精确匹配锁,所以这里另起一条规则、取值逐字相同,且必须排在其后
|
|
2079
|
+
// (测试按首次出现抓取 --mneme-* 定义)。引用的宿主 token 全部在
|
|
2080
|
+
// test/contrast.test.js 的 HOST_TOKENS 快照里,不许新增名字。
|
|
2081
|
+
".mneme-pipe,.mneme-pipeview,.mneme-pipe-settle,.mneme-pipe-root{" +
|
|
2082
|
+
"--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
|
|
2083
|
+
"--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
2084
|
+
"--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
2085
|
+
"--mneme-danger:color-mix(in srgb,var(--dsw-alias-state-error-primary) 80%,var(--dsw-alias-label-primary));" +
|
|
2086
|
+
"--mneme-warn:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 65%,var(--dsw-alias-label-primary));" +
|
|
2087
|
+
"--mneme-ok:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 65%,var(--dsw-alias-label-primary))" +
|
|
2088
|
+
"}",
|
|
2089
|
+
// 会话头 trigger:与 Agent Teams 同规格(28px 行高、12px 字号),
|
|
2090
|
+
// 页头窄容器下文字标签隐藏、降级纯图标。
|
|
2091
|
+
".mneme-pipe-root{position:relative;display:inline-flex}",
|
|
2092
|
+
".mneme-pipe-trigger{min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:5px;padding:3px 7px;font-family:inherit;font-size:12px;display:inline-flex}",
|
|
2093
|
+
".mneme-pipe-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2094
|
+
".mneme-pipe-trigger:focus-visible{color:var(--dsw-alias-label-primary);outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2095
|
+
".mneme-pipe-triggerLabel{white-space:nowrap}",
|
|
2096
|
+
"@container (width<=480px){.mneme-pipe-triggerLabel{display:none}}",
|
|
2097
|
+
".mneme-pipe-dot{flex:none;min-width:14px;height:14px;border-radius:7px;padding:0 3px;background:color-mix(in srgb,var(--mneme-warn) 14%,transparent);color:var(--dsw-alias-label-primary);font-size:10px;line-height:14px;font-weight:600;font-variant-numeric:tabular-nums;text-align:center}",
|
|
2098
|
+
// 浮层面板:几何抄 Agent Teams(min(500px,100vw-32px) / 12px 圆角 /
|
|
2099
|
+
// fixed 定位由 useAnchoredPosition 给出)。阴影用中性 rgba:宿主的
|
|
2100
|
+
// elevation token 不在面板 token 快照白名单里,不能引用。
|
|
2101
|
+
".mneme-pipe-panel{z-index:100;box-sizing:border-box;width:min(500px,100vw - 32px);max-height:min(680px,100vh - 32px);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;padding:0;display:flex;position:fixed;overflow:hidden;background:var(--dsw-alias-bg-layer-1);box-shadow:0 8px 28px rgba(0,0,0,0.28);color:var(--dsw-alias-label-primary)}",
|
|
2102
|
+
".mneme-pipe-head{flex:none;display:flex;align-items:center;gap:8px;padding:10px 12px 8px 14px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
2103
|
+
".mneme-pipe-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;gap:6px;min-width:0}",
|
|
2104
|
+
".mneme-pipe-beta{flex:none;font-size:11px;line-height:16px;font-weight:600;color:var(--mneme-warn);border:1px solid color-mix(in srgb,var(--mneme-warn) 55%,transparent);border-radius:6px;padding:0 6px}",
|
|
2105
|
+
// 内测免责:界面常驻一行,「候选」是可见状态词不是 tooltip。
|
|
2106
|
+
".mneme-pipe-notice{flex:none;display:flex;gap:6px;align-items:flex-start;padding:7px 14px 0;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}",
|
|
2107
|
+
".mneme-pipe-scope{flex:none;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
2108
|
+
// 运行档位:一次运行一个芯片;失败/被推翻的运行照常列出(留痕)。
|
|
2109
|
+
".mneme-pipe-runs{flex:none;display:flex;gap:6px;flex-wrap:wrap;padding:6px 14px 2px}",
|
|
2110
|
+
".mneme-pipe-run{border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:8px;cursor:pointer;color:var(--mneme-fg-dim);font-family:inherit;font-size:12px;line-height:16px;padding:2px 8px;display:inline-flex;gap:6px;align-items:center}",
|
|
2111
|
+
".mneme-pipe-run:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2112
|
+
".mneme-pipe-run:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2113
|
+
".mneme-pipe-run.mneme-on{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
2114
|
+
".mneme-pipe-runDot{flex:none;width:8px;height:8px;border-radius:4px}",
|
|
2115
|
+
".mneme-pipe-runDot--ok{background:var(--mneme-ok)}",
|
|
2116
|
+
".mneme-pipe-runDot--failed{background:var(--mneme-warn)}",
|
|
2117
|
+
".mneme-pipe-runDot--refuted{background:var(--mneme-danger)}",
|
|
2118
|
+
// 图:宽态 SVG 分层 DAG(trace viewer 读法),窄态整块换成竖排列表。
|
|
2119
|
+
// 高度下限:flex:1 的基准是 0,只要某一层祖先没有确定高度(浮层与居中 sheet 都是 max-height
|
|
2120
|
+
// 封顶),分到的就是 0 像素——画布「在 DOM 里、但看不见」。下限不修复成因,只保证最坏情况下
|
|
2121
|
+
// 还能看见图;这跟上面那条撞车是两件事,别把任何一条当成另一条的修复。
|
|
2122
|
+
".mneme-pipe-graph{flex:1;min-height:220px;overflow:auto;padding:4px 8px 2px}",
|
|
2123
|
+
// 缩放条与缩放容器。画布本身是 width:100% 的 SVG,所以「缩放」= 改外层容器的百分比宽度:
|
|
2124
|
+
// 100% 就是适应栏宽,放大则容器比栏宽,滚动条自己出来——平移不必另写一套。
|
|
2125
|
+
".mneme-pipe-zoombar{flex:none;display:flex;align-items:center;gap:6px;padding:5px 12px 0;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2126
|
+
".mneme-pipe-zoombtn{border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:6px;cursor:pointer;color:var(--mneme-fg-dim);font-family:inherit;font-size:12px;line-height:16px;padding:1px 7px}",
|
|
2127
|
+
".mneme-pipe-zoombtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2128
|
+
".mneme-pipe-zoombtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2129
|
+
".mneme-pipe-zoomval{min-width:40px;text-align:right;font-variant-numeric:tabular-nums}",
|
|
2130
|
+
".mneme-pipe-zoomwrap{min-width:0}",
|
|
2131
|
+
".mneme-pipe-svg{display:block;width:100%;height:auto}",
|
|
2132
|
+
".mneme-pipe-edge{stroke:var(--mneme-graphic-dim);fill:none;stroke-width:1.5}",
|
|
2133
|
+
".mneme-pipe-edgeOn{stroke:var(--mneme-accent);fill:none;stroke-width:2.5}",
|
|
2134
|
+
".mneme-pipe-edgeFail{stroke:var(--mneme-danger);fill:none;stroke-width:2;stroke-dasharray:5 3}",
|
|
2135
|
+
// 节点形状按 kind 区分(step 矩形 / gate 菱形 / branch 圆角 / artifact 胶囊),
|
|
2136
|
+
// 状态走描边+填充:refuted 是全篇核心信号,双份危险色最扎眼。
|
|
2137
|
+
".mneme-pipe-node{fill:var(--dsw-alias-bg-layer-2);stroke:var(--mneme-graphic-dim);stroke-width:1.2}",
|
|
2138
|
+
".mneme-pipe-node--trusted{stroke:var(--mneme-ok);stroke-width:1.6}",
|
|
2139
|
+
".mneme-pipe-node--candidate{stroke:var(--mneme-graphic-dim)}",
|
|
2140
|
+
".mneme-pipe-node--stale{stroke:var(--dsw-alias-border-l2);stroke-dasharray:3 2;fill:var(--dsw-alias-bg-base)}",
|
|
2141
|
+
".mneme-pipe-node--refuted{stroke:var(--mneme-danger);stroke-width:2;fill:color-mix(in srgb,var(--mneme-danger) 10%,transparent)}",
|
|
2142
|
+
".mneme-pipe-node--onpath{stroke:var(--dsw-alias-state-business-primary);stroke-width:2}",
|
|
2143
|
+
".mneme-pipe-nodeT{fill:var(--dsw-alias-label-primary);font-size:11px;font-weight:600}",
|
|
2144
|
+
".mneme-pipe-nodeS{fill:var(--mneme-fg-dim);font-size:9.5px}",
|
|
2145
|
+
".mneme-pipe-nodeK{fill:var(--mneme-fg-dim);font-size:9px}",
|
|
2146
|
+
// 图例:四个状态 + 闸门有无(counterfactual null = 空心圈「还没闸门」)。
|
|
2147
|
+
".mneme-pipe-legend{flex:none;display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:2px 14px 8px;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
2148
|
+
".mneme-pipe-pill{flex:none;border-radius:6px;padding:0 6px;font-size:11px;line-height:18px;font-weight:600;border:1px solid transparent}",
|
|
2149
|
+
".mneme-pipe-pill--candidate{color:var(--mneme-fg-dim);border-color:var(--dsw-alias-border-l2)}",
|
|
2150
|
+
".mneme-pipe-pill--trusted{color:var(--mneme-ok);border-color:color-mix(in srgb,var(--mneme-ok) 45%,transparent)}",
|
|
2151
|
+
".mneme-pipe-pill--stale{color:var(--mneme-fg-dim);border-color:var(--dsw-alias-border-l2);border-style:dashed}",
|
|
2152
|
+
".mneme-pipe-pill--refuted{color:var(--mneme-danger);border-color:color-mix(in srgb,var(--mneme-danger) 55%,transparent);background:color-mix(in srgb,var(--mneme-danger) 10%,transparent)}",
|
|
2153
|
+
".mneme-pipe-gate{flex:none;width:10px;height:10px;border-radius:5px;border:1.5px solid var(--mneme-graphic-dim)}",
|
|
2154
|
+
".mneme-pipe-gate--has{border-color:var(--mneme-ok);background:var(--mneme-ok)}",
|
|
2155
|
+
// 阶段带(2026-10-16):段底用宿主 hover 层而不是自造色——对比度锁不认识
|
|
2156
|
+
// 「SVG 文字底下还有一层 rect」,所以段底必须淡到不参与文字对比度。
|
|
2157
|
+
".mneme-pipe-bandbg{fill:var(--dsw-alias-interactive-bg-hover)}",
|
|
2158
|
+
".mneme-pipe-bandline{stroke:var(--dsw-alias-border-l2);stroke-width:1}",
|
|
2159
|
+
".mneme-pipe-bandT{fill:var(--dsw-alias-label-primary);font-size:12px;font-weight:600}",
|
|
2160
|
+
// 闸门节点与风险条:颜色只做冗余,信息本体在文本(闸 / R0-R3)里——
|
|
2161
|
+
// 不许靠单一色相区分(AGENTS.md 自查清单第 9 条)。
|
|
2162
|
+
".mneme-pipe-node--gate{stroke:var(--mneme-warn);stroke-width:2}",
|
|
2163
|
+
".mneme-pipe-gatetag{fill:var(--mneme-warn);font-size:9px;font-weight:600}",
|
|
2164
|
+
".mneme-pipe-risk--r0{stroke:var(--mneme-graphic-dim)}",
|
|
2165
|
+
".mneme-pipe-risk--r1{stroke:var(--mneme-accent)}",
|
|
2166
|
+
".mneme-pipe-risk--r2{stroke:var(--mneme-warn)}",
|
|
2167
|
+
".mneme-pipe-risk--r3{stroke:var(--mneme-danger)}",
|
|
2168
|
+
// 流程概览带:读投影里 pipeline.done 与 pipeline.trigger,回答「这条流程
|
|
2169
|
+
// 在干什么、何时起跑」。正文用 label-secondary(与节点详情屏同档)。
|
|
2170
|
+
".mneme-pipe-overview{flex:none;padding:8px 14px 6px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:3px}",
|
|
2171
|
+
// 节点详情(宽态点选展开 / 窄态 details 手风琴共用)。
|
|
2172
|
+
".mneme-pipe-detail{flex:none;border-top:1px solid var(--dsw-alias-border-l1);padding:8px 14px 10px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);max-height:180px;overflow:auto}",
|
|
2173
|
+
".mneme-pipe-detailTitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:2px}",
|
|
2174
|
+
".mneme-pipe-dt{font-weight:600;color:var(--mneme-fg-dim)}",
|
|
2175
|
+
".mneme-pipe-cf{margin-top:4px;padding:4px 8px;border-radius:8px;background:color-mix(in srgb,var(--mneme-ok) 10%,transparent);color:var(--dsw-alias-label-secondary)}",
|
|
2176
|
+
".mneme-pipe-cf--none{background:0 0;border:1px dashed var(--dsw-alias-border-l2);color:var(--mneme-fg-dim)}",
|
|
2177
|
+
// 窄态(better-sidebar ~500px / 浮层内):节点竖排列表化,边收成
|
|
2178
|
+
// 左侧轨道线 + 「依赖」芯片——换布局,不是缩放。
|
|
2179
|
+
".mneme-pipe-list{flex:1;min-height:180px;overflow:auto;padding:4px 12px 8px}",
|
|
2180
|
+
".mneme-pipe-item{position:relative;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:8px 10px 8px 30px;margin:0 0 10px}",
|
|
2181
|
+
".mneme-pipe-item::before{content:\"\";position:absolute;left:13px;top:-10px;height:10px;border-left:2px solid var(--mneme-graphic-dim)}",
|
|
2182
|
+
".mneme-pipe-item:first-child::before{display:none}",
|
|
2183
|
+
".mneme-pipe-itemDot{position:absolute;left:8px;top:12px;width:10px;height:10px;border-radius:5px;border:2px solid var(--mneme-graphic-dim);background:var(--dsw-alias-bg-layer-2)}",
|
|
2184
|
+
".mneme-pipe-itemDot--trusted{border-color:var(--mneme-ok)}",
|
|
2185
|
+
".mneme-pipe-itemDot--stale{border-style:dashed;border-color:var(--dsw-alias-border-l2)}",
|
|
2186
|
+
".mneme-pipe-itemDot--refuted{border-color:var(--mneme-danger);background:var(--mneme-danger)}",
|
|
2187
|
+
".mneme-pipe-itemDot--onpath{border-color:var(--dsw-alias-state-business-primary)}",
|
|
2188
|
+
".mneme-pipe-itemName{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary)}",
|
|
2189
|
+
".mneme-pipe-itemName--refuted{color:var(--mneme-danger)}",
|
|
2190
|
+
".mneme-pipe-itemMeta{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-top:2px;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
2191
|
+
".mneme-pipe-dep{font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2192
|
+
".mneme-pipe-item summary{cursor:pointer;list-style:none}",
|
|
2193
|
+
".mneme-pipe-item summary::-webkit-details-marker{display:none}",
|
|
2194
|
+
".mneme-pipe-itemBody{margin-top:6px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}",
|
|
2195
|
+
// 对话行结算卡(turnTail):死数据,跟这次运行绑死。
|
|
2196
|
+
".mneme-pipe-settle{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:8px 10px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);margin:4px 0;max-width:620px}",
|
|
2197
|
+
".mneme-pipe-settleHead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px}",
|
|
2198
|
+
".mneme-pipe-settleTitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;gap:6px}",
|
|
2199
|
+
".mneme-pipe-steps{display:flex;gap:4px;flex-wrap:wrap;align-items:center;margin:2px 0}",
|
|
2200
|
+
".mneme-pipe-stepChip{border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:0 7px;font-size:11px;line-height:18px;color:var(--mneme-fg-dim);display:inline-flex;gap:4px;align-items:center}",
|
|
2201
|
+
".mneme-pipe-stepChip--used{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent);color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
2202
|
+
".mneme-pipe-stepArrow{color:var(--mneme-fg-dim);font-size:11px}",
|
|
2203
|
+
// 老宿主退化:居中 sheet(形态沿用记忆库 overlay 的背板 + 居中面板)。
|
|
2204
|
+
".mneme-pipe-sheet{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:60;width:min(860px,calc(100vw - 48px));max-height:calc(100vh - 96px);border-radius:14px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,0.3)}",
|
|
2205
|
+
".mneme-pipe-sheetbar{flex:none;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
2206
|
+
".mneme-pipe-sheetbody{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}",
|
|
2207
|
+
// 空态/加载/错误共用文案块。
|
|
2208
|
+
".mneme-pipe-empty{flex:none;padding:18px 14px;text-align:center;font-size:13px;line-height:20px;color:var(--mneme-fg-dim)}",
|
|
2209
|
+
".mneme-pipe-emptyTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
2210
|
+
// 工作流清单:**选择器**,不是「一屏」。它在上面占一条、图在下面占其余——
|
|
2211
|
+
// 第一版把它做成吃掉整屏、点条目才换屏,结果画布藏在一次点击后面且界面上没有任何
|
|
2212
|
+
// 提示,反馈连着两次说「看不到画布和节点」。上限用固定值不用百分比:百分比在
|
|
2213
|
+
// 高度不确定的祖先下会被当成 none,又变回吃掉整屏。
|
|
2214
|
+
".mneme-pipe-wflist{flex:none;max-height:220px;min-height:0;overflow:auto;padding:8px 14px 10px;display:flex;flex-direction:column;gap:8px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
2215
|
+
".mneme-pipe-wfhead{flex:none;display:flex;align-items:baseline;justify-content:space-between;gap:8px;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2216
|
+
".mneme-pipe-wf{display:block;width:100%;text-align:left;font-family:inherit;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:9px 11px;cursor:pointer;color:var(--dsw-alias-label-primary)}",
|
|
2217
|
+
".mneme-pipe-wf:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2218
|
+
".mneme-pipe-wf:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2219
|
+
// 当前展开的那条:只有颜色不同,图仍由下面的画布独占——选择器不该抢主语的位置。
|
|
2220
|
+
".mneme-pipe-wf.mneme-on{border-color:var(--dsw-alias-state-business-primary)}",
|
|
2221
|
+
".mneme-pipe-wfTop{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
|
|
2222
|
+
".mneme-pipe-wfName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:0}",
|
|
2223
|
+
".mneme-pipe-wfScope{flex:none;font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2224
|
+
".mneme-pipe-wfMeta{display:block;margin-top:3px;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2225
|
+
".mneme-pipe-wfId{display:block;margin-top:2px;font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2226
|
+
".mneme-pipe-broken{flex:none;font-size:12px;line-height:17px;color:var(--mneme-warn);padding:6px 14px 0}",
|
|
1638
2227
|
].join("\n");
|
|
1639
2228
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
|
|
1640
2229
|
const tag = document.createElement("style");
|
|
@@ -2190,10 +2779,24 @@ window.__ModuleLoader__.load({
|
|
|
2190
2779
|
// 后端 src/config.js 的 INJECT_CHILD_FLAGS(运行时闸门),两侧漂移由
|
|
2191
2780
|
// test/inject-parent-gate.test.js 钉住。
|
|
2192
2781
|
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
|
|
2782
|
+
// Issue #427 建议 1:折叠线挪到「新手之外」。group.basic 是首屏层,只列新
|
|
2783
|
+
// 用户敢动的总开关;group.scope 也在首屏——它回答新用户的第二个必答题
|
|
2784
|
+
// 「它记的东西会不会被别的会话看到」。其余组整组收进「全部设置」折叠
|
|
2785
|
+
// (showAll)。成员从原组里移走而不是渲染时过滤:同一项永远只渲染一次。
|
|
2786
|
+
// 首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + scope 2 开关 +
|
|
2787
|
+
// 「全部设置」按钮 = 8(验收线 ≤ 8;RuntimeProvisionBlock 配好即消失,
|
|
2788
|
+
// 常态首屏更少)。
|
|
2789
|
+
const FEATURE_FIRST_SCREEN = ["group.basic", "group.scope"];
|
|
2193
2790
|
const FEATURE_GROUPS = [
|
|
2194
|
-
{ key: "group.
|
|
2195
|
-
{ key: "group.
|
|
2196
|
-
|
|
2791
|
+
{ key: "group.basic", items: ["autoInject", "autoSummarize", "autoDream"] },
|
|
2792
|
+
{ key: "group.core", items: ["hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce", "preInjectGate.enabled", "preInjectGate.enforce", "sensitiveScanEnabled",
|
|
2793
|
+
// #399 的留存总闸(见下方 distillSub:天数行是它的子块)。放在组末,
|
|
2794
|
+
// 子块才能紧跟在它后面展开——items.map 之后的子块是按组尾渲染的。
|
|
2795
|
+
"distillRecords.enabled"] },
|
|
2796
|
+
// v0.9.0 M.O.(beta):开关挂在 enhance 组。它不是行为开关而是
|
|
2797
|
+
// 座位闸门——关=一个座位都不注册(见 apply 内的 M.O. 块)。
|
|
2798
|
+
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled", "actionPipelineEnabled"] },
|
|
2799
|
+
{ key: "group.dream", items: ["sleepModeEnabled"] },
|
|
2197
2800
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
2198
2801
|
{ key: "group.scope", items: ["scopeEnabled", "strictScope"] }
|
|
2199
2802
|
];
|
|
@@ -2204,6 +2807,12 @@ window.__ModuleLoader__.load({
|
|
|
2204
2807
|
// Issue #239 第 4 项:错峰时段串(巩固侧与蒸馏侧)。此前只有后端白名单、
|
|
2205
2808
|
// 面板调不到——两个错峰键一个能调一个不能比都不给更让人困惑。
|
|
2206
2809
|
"dreamPeakHours", "summarizePeakHours"];
|
|
2810
|
+
// 整数键(blur/Enter 提交,非法值不提交、退回生效值):
|
|
2811
|
+
// issue #399 的 distillRecords.retentionDays。此前它与 enabled 一样只有
|
|
2812
|
+
// 后端白名单、面板够不着——而 CONFIGURATION.md 明写「面板可启停」。
|
|
2813
|
+
// 其余整数键(429 调速器参数、distillMaxChars、codingBoostFactor)是有意
|
|
2814
|
+
// 不上 UI 的调优噪音,见上方注释,别顺手搬进来。
|
|
2815
|
+
const FEATURE_NUMBERS = ["distillRecords.retentionDays"];
|
|
2207
2816
|
const EMBED_PROVIDERS = ["openai", "local", "ollama"];
|
|
2208
2817
|
// 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
|
|
2209
2818
|
const ENTITY_REASONING = ["none", "low", "medium", "high"];
|
|
@@ -2211,10 +2820,14 @@ window.__ModuleLoader__.load({
|
|
|
2211
2820
|
// 蒸馏防推理烧预算)。后端枚举含 off,面板必须能选到,否则操作者用不上。
|
|
2212
2821
|
const SUMMARIZE_REASONING = ["off", "none", "low", "medium", "high"];
|
|
2213
2822
|
|
|
2214
|
-
function FeatureRow({ name, hint, on, disabled, onToggle, sub }) {
|
|
2823
|
+
function FeatureRow({ name, hint, on, disabled, onToggle, sub, tag }) {
|
|
2215
2824
|
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
2216
2825
|
h("div", { className: "mneme-featmain" },
|
|
2217
|
-
h("div", { className: "mneme-featname" }, name
|
|
2826
|
+
h("div", { className: "mneme-featname" }, name,
|
|
2827
|
+
// 「默认 / 已改」徽标(#427 建议 3):不用点开配置文件就知道哪
|
|
2828
|
+
// 些是出厂值、哪些被你改过;title 给完整解释(键盘/读屏可达)。
|
|
2829
|
+
tag && h("span", { className: "mneme-badge " + tag.kind, title: tag.title }, tag.text)
|
|
2830
|
+
),
|
|
2218
2831
|
h("div", { className: "mneme-feathint" }, hint)
|
|
2219
2832
|
),
|
|
2220
2833
|
h("button", {
|
|
@@ -2232,10 +2845,18 @@ window.__ModuleLoader__.load({
|
|
|
2232
2845
|
function FeaturesCard({ t }) {
|
|
2233
2846
|
const [state, setState] = useState(null); // { overrides, effective }
|
|
2234
2847
|
const [error, setError] = useState("");
|
|
2848
|
+
// 保存失败的提示要单独的座位:上面那个 error 只在首屏加载失败(state 还是
|
|
2849
|
+
// null)时渲染,state 一到手它就永不出现——于是每一次 put 失败都静默,
|
|
2850
|
+
// 草稿留在输入框里,人以为改成功了。
|
|
2851
|
+
const [saveError, setSaveError] = useState("");
|
|
2235
2852
|
const [busy, setBusy] = useState(false);
|
|
2236
2853
|
const [savedTick, setSavedTick] = useState(false);
|
|
2237
2854
|
const [showAdv, setShowAdv] = useState(false);
|
|
2855
|
+
// Issue #427 建议 1:「全部设置」是第一层折叠(默认收起),「高级」是它
|
|
2856
|
+
// 内部的第二层。两个状态独立:收起全部设置时高级的展开与否被记住。
|
|
2857
|
+
const [showAll, setShowAll] = useState(false);
|
|
2238
2858
|
const [strs, setStrs] = useState({}); // 字符串输入的本地草稿:key -> string
|
|
2859
|
+
const [nums, setNums] = useState({}); // 数值输入的本地草稿:key -> string(提交时才转整数)
|
|
2239
2860
|
// 巩固/睡眠模型路由下拉的数据源:GET /llm-providers 探测(云端 v0.7.26+
|
|
2240
2861
|
// 的插件侧端点)。null = 端点不可用(404/失败)→ 回退纯文本输入,不挡旧后端。
|
|
2241
2862
|
const [routes, setRoutes] = useState(null);
|
|
@@ -2255,6 +2876,9 @@ window.__ModuleLoader__.load({
|
|
|
2255
2876
|
const draft = {};
|
|
2256
2877
|
for (const key of FEATURE_STRINGS) draft[key] = String(e[key] ?? "");
|
|
2257
2878
|
setStrs(draft);
|
|
2879
|
+
const numDraft = {};
|
|
2880
|
+
for (const key of FEATURE_NUMBERS) numDraft[key] = String(e[key] ?? "");
|
|
2881
|
+
setNums(numDraft);
|
|
2258
2882
|
})
|
|
2259
2883
|
.catch(() => { if (!cancelled) setError(t("memory.features.loadFailed")); });
|
|
2260
2884
|
return () => { cancelled = true; };
|
|
@@ -2272,6 +2896,7 @@ window.__ModuleLoader__.load({
|
|
|
2272
2896
|
|
|
2273
2897
|
const eff = (state && state.effective) || {};
|
|
2274
2898
|
|
|
2899
|
+
// 返回值 = 这次写入有没有落库。调用方要据此决定草稿的去留。
|
|
2275
2900
|
const put = async (patch) => {
|
|
2276
2901
|
setBusy(true);
|
|
2277
2902
|
try {
|
|
@@ -2280,13 +2905,25 @@ window.__ModuleLoader__.load({
|
|
|
2280
2905
|
headers: { "Content-Type": "application/json" },
|
|
2281
2906
|
body: JSON.stringify(patch)
|
|
2282
2907
|
});
|
|
2283
|
-
if (!res.ok)
|
|
2908
|
+
if (!res.ok) {
|
|
2909
|
+
// 服务端的校验失败把键名与范围写进 error(api.js 里那条 400 的注释
|
|
2910
|
+
// 就是这么设计的),读出来给人看。拿不到 body 才退回状态码——
|
|
2911
|
+
// 总之不能只剩一句「失败」让人猜是哪个开关、错在哪。
|
|
2912
|
+
const detail = await res.json()
|
|
2913
|
+
.then((j) => (j && j.error) || "")
|
|
2914
|
+
.catch(() => "");
|
|
2915
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
2916
|
+
}
|
|
2284
2917
|
setState(await res.json());
|
|
2918
|
+
setSaveError("");
|
|
2285
2919
|
announce(t("memory.features.restartHint"));
|
|
2286
2920
|
setSavedTick(true);
|
|
2287
2921
|
setTimeout(() => setSavedTick(false), 1800);
|
|
2288
|
-
|
|
2289
|
-
|
|
2922
|
+
return true;
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
const why = (err && err.message) || String(err);
|
|
2925
|
+
setSaveError(`${t("memory.features.saveFailed")} — ${why}`);
|
|
2926
|
+
return false;
|
|
2290
2927
|
} finally {
|
|
2291
2928
|
setBusy(false);
|
|
2292
2929
|
}
|
|
@@ -2294,15 +2931,45 @@ window.__ModuleLoader__.load({
|
|
|
2294
2931
|
|
|
2295
2932
|
// 字符串项失焦/回车提交:trim 后与生效值相同则回填放弃,否则交给
|
|
2296
2933
|
// 后端校验(ollamaBaseUrl 的协议、embedProvider 的枚举在后端把关)。
|
|
2934
|
+
// 后端拒了就退回生效值并出声——留着被拒的文本,人以为改成功了。
|
|
2297
2935
|
const commitString = (key) => {
|
|
2298
2936
|
const raw = (strs[key] ?? "").trim();
|
|
2299
2937
|
if (raw === String(eff[key] ?? "")) {
|
|
2300
2938
|
setStrs((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
2301
2939
|
return;
|
|
2302
2940
|
}
|
|
2303
|
-
put({ [key]: raw })
|
|
2941
|
+
put({ [key]: raw }).then((ok) => {
|
|
2942
|
+
if (!ok) setStrs((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
2943
|
+
});
|
|
2304
2944
|
};
|
|
2305
2945
|
|
|
2946
|
+
// 整数项失焦/回车提交。与字符串项同一形状,多三道闸:本地先挡非整数,
|
|
2947
|
+
// 挡不住的交后端(retentionDays 的范围 1–3650 由白名单把关),后端也拒了
|
|
2948
|
+
// 就把草稿退回生效值。前两道退回时也不出声——那是本地的即时反馈;第三道
|
|
2949
|
+
// 必须出声(put 会把原因写进 saveError),否则「框里的数字」与「在用的
|
|
2950
|
+
// 保留天数」会分家,而人看不出来。
|
|
2951
|
+
const commitNumber = (key) => {
|
|
2952
|
+
const raw = (nums[key] ?? "").trim();
|
|
2953
|
+
if (raw === String(eff[key] ?? "")) return;
|
|
2954
|
+
if (!/^\d+$/.test(raw)) {
|
|
2955
|
+
setNums((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
put({ [key]: Number(raw) }).then((ok) => {
|
|
2959
|
+
if (!ok) setNums((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
2960
|
+
});
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2963
|
+
// 「默认 / 已改」的信号来源是 state.overrides:featureSnapshot 只把
|
|
2964
|
+
// settings.getFeatureFlags() 里真实落库的键放进 overrides,键不在其中
|
|
2965
|
+
// = 值仍来自 config schema 的默认值。面板自己判断,不加后端字段、
|
|
2966
|
+
// 不动 config.js(#427 的验收红线之一)。
|
|
2967
|
+
const ovr = (state && state.overrides) || {};
|
|
2968
|
+
const flagTag = (k) => ({
|
|
2969
|
+
kind: Object.prototype.hasOwnProperty.call(ovr, k) ? "mneme-badge--changed" : "mneme-badge--default",
|
|
2970
|
+
text: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged" : "memory.features.tagDefault"),
|
|
2971
|
+
title: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged.hint" : "memory.features.tagDefault.hint")
|
|
2972
|
+
});
|
|
2306
2973
|
const boolRow = (k, opts = {}) => h(FeatureRow, {
|
|
2307
2974
|
key: k,
|
|
2308
2975
|
name: t(`memory.features.${k}`),
|
|
@@ -2311,6 +2978,7 @@ window.__ModuleLoader__.load({
|
|
|
2311
2978
|
on: !!eff[k],
|
|
2312
2979
|
disabled: busy,
|
|
2313
2980
|
sub: !!opts.sub,
|
|
2981
|
+
tag: flagTag(k),
|
|
2314
2982
|
onToggle: () => put({ [k]: !eff[k] })
|
|
2315
2983
|
});
|
|
2316
2984
|
|
|
@@ -2328,9 +2996,11 @@ window.__ModuleLoader__.load({
|
|
|
2328
2996
|
// 连通性测试:POST /test-model,空 provider/model = 按巩固路由解析
|
|
2329
2997
|
// (agent 默认)。durationMs 优先后端值,缺失时客户端兜底计时;成功行
|
|
2330
2998
|
// 附模型的实际回复(reply,后端截 100 字符)——「真的答了 ok」而非只报通。
|
|
2331
|
-
// reasoningEffort 透传该路由配置的档位(issue #215
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
2999
|
+
// reasoningEffort 透传该路由配置的档位(issue #215),'none'/未配置省略
|
|
3000
|
+
// 字段,与后端 src/api.js 的同口径。**档位被拒不再是失败**:后端与真实
|
|
3001
|
+
// run 一样丢掉字段重试一次,回以 effortDropped:true,这里单独说明「连通
|
|
3002
|
+
// 了、但不是用你配的档位」——否则用户会照着红色失败去换 Provider,而
|
|
3003
|
+
// 后台其实一直在正常跑(这正是它此前给出误导性诊断的原因)。
|
|
2334
3004
|
const runModelTest = async (provider, model, setTestState, reasoningEffort) => {
|
|
2335
3005
|
setTestState({ running: true });
|
|
2336
3006
|
const started = Date.now();
|
|
@@ -2356,6 +3026,8 @@ window.__ModuleLoader__.load({
|
|
|
2356
3026
|
setTestState({
|
|
2357
3027
|
ok: true,
|
|
2358
3028
|
ms,
|
|
3029
|
+
// 只有后端明确置位才算降级(旧后端无此字段 → 不显示,不误报)。
|
|
3030
|
+
dropped: j.effortDropped === true,
|
|
2359
3031
|
detail: [j.modelId, j.reply ? "「" + j.reply + "」" : ""].filter(Boolean).join(" · ")
|
|
2360
3032
|
});
|
|
2361
3033
|
} catch (err) {
|
|
@@ -2423,6 +3095,7 @@ window.__ModuleLoader__.load({
|
|
|
2423
3095
|
(testState && !testState.running
|
|
2424
3096
|
? h("div", { className: "mneme-featsubhint" },
|
|
2425
3097
|
(testState.ok ? "✓ " + t("memory.features.modelTestOk") : "✗ " + t("memory.features.modelTestFail"))
|
|
3098
|
+
+ (testState.ok && testState.dropped ? " · " + t("memory.features.modelTestDowngraded") : "")
|
|
2426
3099
|
+ (typeof testState.ms === "number" ? " · " + (testState.ms / 1000).toFixed(1) + "s" : "")
|
|
2427
3100
|
+ (testState.detail ? " · " + testState.detail : ""))
|
|
2428
3101
|
: h("div", { className: "mneme-featsubhint" }, t("memory.features.modelTestHint"))))
|
|
@@ -2439,6 +3112,22 @@ window.__ModuleLoader__.load({
|
|
|
2439
3112
|
onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); }
|
|
2440
3113
|
}));
|
|
2441
3114
|
|
|
3115
|
+
// type 刻意用 text + inputMode 而不是 number:number 输入框在部分内容被
|
|
3116
|
+
// 判非法时 e.target.value 会变成空串,草稿与用户所见脱节;这里要自己拿
|
|
3117
|
+
// 到原文才能挡下非整数并退回生效值。
|
|
3118
|
+
const numRow = (key) => h("div", { className: "mneme-featnum", key },
|
|
3119
|
+
h("span", { className: "mneme-featnumlabel" }, t(`memory.features.${key}`)),
|
|
3120
|
+
h("input", {
|
|
3121
|
+
className: "mneme-numinput",
|
|
3122
|
+
type: "text",
|
|
3123
|
+
inputMode: "numeric",
|
|
3124
|
+
value: nums[key] ?? "",
|
|
3125
|
+
"aria-label": t(`memory.features.${key}`),
|
|
3126
|
+
onChange: (e) => setNums((c) => ({ ...c, [key]: e.target.value })),
|
|
3127
|
+
onBlur: () => commitNumber(key),
|
|
3128
|
+
onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); }
|
|
3129
|
+
}));
|
|
3130
|
+
|
|
2442
3131
|
// 语义检索路线:provider 下拉即时提交;local/ollama 选中时展开各自
|
|
2443
3132
|
// 的连接字段,openai 的连接信息由「向量搜索」卡片持有,只给指引。
|
|
2444
3133
|
const embedSub = h("div", { className: "mneme-featsub" },
|
|
@@ -2520,6 +3209,14 @@ window.__ModuleLoader__.load({
|
|
|
2520
3209
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.summarizePeakHours.hint"))
|
|
2521
3210
|
);
|
|
2522
3211
|
|
|
3212
|
+
// #399 的留存开关紧跟在蒸馏总闸下:天数行只在留存开着时展开(关着一行
|
|
3213
|
+
// 不写,改天数没有意义)。放在这而不是挂在 flagRow 上,是因为
|
|
3214
|
+
// FEATURE_CHILDREN 只认布尔子项,而这里要的是一个整数输入行。
|
|
3215
|
+
const distillSub = eff["distillRecords.enabled"] && h("div", { className: "mneme-featsub" },
|
|
3216
|
+
numRow("distillRecords.retentionDays"),
|
|
3217
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.distillRecords.retentionDays.hint"))
|
|
3218
|
+
);
|
|
3219
|
+
|
|
2523
3220
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
2524
3221
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
2525
3222
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -2530,29 +3227,58 @@ window.__ModuleLoader__.load({
|
|
|
2530
3227
|
savedTick && h("span", { className: "mneme-saved", style: { marginLeft: 8, fontWeight: 400 } }, t("memory.features.restartHint"))
|
|
2531
3228
|
),
|
|
2532
3229
|
h("div", { className: "mneme-set-desc" }, t("memory.features.desc")),
|
|
3230
|
+
// 见 saveError 的声明处:这条分支不能跟着加载失败那条走。
|
|
3231
|
+
saveError && h("div", { className: "mneme-mainterr" }, saveError),
|
|
2533
3232
|
h(RuntimeProvisionBlock, { t }),
|
|
2534
3233
|
state === null && !error
|
|
2535
3234
|
? h("div", { className: "mneme-set-hint" }, "…")
|
|
2536
|
-
:
|
|
2537
|
-
|
|
3235
|
+
: (() => {
|
|
3236
|
+
// Issue #427:分组按「实现」分改不动需求本身,改的是「折叠线划在
|
|
3237
|
+
// 哪里」。renderGroup 是唯一的组渲染路径(含组尾子块),首屏层与
|
|
3238
|
+
// 「全部设置」共用它,保证 g.items.map(flagRow) 只写一次、子项
|
|
3239
|
+
// 只可能经 flagRow 渲染(test/inject-parent-gate.test.js 的门)。
|
|
3240
|
+
const renderGroup = (g) => h(react.Fragment, { key: g.key },
|
|
2538
3241
|
h("div", { className: "mneme-featgroup" }, t(`memory.features.${g.key}`)),
|
|
2539
3242
|
g.items.map(flagRow),
|
|
2540
|
-
|
|
3243
|
+
// 子块按组尾追加。distillSub 紧跟在 core 组最后一行
|
|
3244
|
+
// (distillRecords.enabled)之后,天数行才贴着它的总闸。
|
|
3245
|
+
g.key === "group.core" && h(react.Fragment, null, distillSub, summarizeSub),
|
|
2541
3246
|
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
2542
3247
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
2543
|
-
)
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
},
|
|
2551
|
-
h(
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3248
|
+
);
|
|
3249
|
+
const firstScreen = FEATURE_GROUPS.filter((g) => FEATURE_FIRST_SCREEN.includes(g.key));
|
|
3250
|
+
const restGroups = FEATURE_GROUPS.filter((g) => !FEATURE_FIRST_SCREEN.includes(g.key));
|
|
3251
|
+
return h(react.Fragment, null,
|
|
3252
|
+
firstScreen.map(renderGroup),
|
|
3253
|
+
// 「全部设置」= 第一层折叠(默认收起,#427 建议 1):里面的
|
|
3254
|
+
// 30 余项对新手是噪音,对新用户可见的只有上面的总开关层。
|
|
3255
|
+
h("div", { className: "mneme-featgroup" },
|
|
3256
|
+
h("button", {
|
|
3257
|
+
type: "button",
|
|
3258
|
+
className: "mneme-footbtn",
|
|
3259
|
+
"aria-expanded": String(showAll),
|
|
3260
|
+
onClick: () => setShowAll(!showAll)
|
|
3261
|
+
},
|
|
3262
|
+
h(Icon, { name: showAll ? "chevronDown" : "chevronRight", size: 12 }),
|
|
3263
|
+
t("memory.features.allToggle"))
|
|
3264
|
+
),
|
|
3265
|
+
showAll && h(react.Fragment, null,
|
|
3266
|
+
restGroups.map(renderGroup),
|
|
3267
|
+
// 「高级」降为第二层,嵌在「全部设置」内部。
|
|
3268
|
+
h("div", { className: "mneme-featgroup" },
|
|
3269
|
+
h("button", {
|
|
3270
|
+
type: "button",
|
|
3271
|
+
className: "mneme-footbtn",
|
|
3272
|
+
"aria-expanded": String(showAdv),
|
|
3273
|
+
onClick: () => setShowAdv(!showAdv)
|
|
3274
|
+
},
|
|
3275
|
+
h(Icon, { name: showAdv ? "chevronDown" : "chevronRight", size: 12 }),
|
|
3276
|
+
t("memory.features.advancedToggle"))
|
|
3277
|
+
),
|
|
3278
|
+
showAdv && FEATURE_ADVANCED_BOOLS.map((k) => boolRow(k))
|
|
3279
|
+
)
|
|
3280
|
+
);
|
|
3281
|
+
})()
|
|
2556
3282
|
);
|
|
2557
3283
|
}
|
|
2558
3284
|
|
|
@@ -2982,7 +3708,7 @@ window.__ModuleLoader__.load({
|
|
|
2982
3708
|
h("textarea", { className: "mneme-set-input mneme-set-input--area", style: { minHeight: 56 }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
|
|
2983
3709
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
2984
3710
|
h("button", { className: "mneme-btn", onClick: addCommand }, t("memory.settings.cmdAdd")),
|
|
2985
|
-
cmdError && h("span", { style: { fontSize: 12, color: "var(--
|
|
3711
|
+
cmdError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, cmdError)
|
|
2986
3712
|
)
|
|
2987
3713
|
)
|
|
2988
3714
|
),
|
|
@@ -2993,7 +3719,7 @@ window.__ModuleLoader__.load({
|
|
|
2993
3719
|
h("section", { className: "mneme-set-card" },
|
|
2994
3720
|
h("div", { className: "mneme-set-title" }, t("memory.settings.mode.title")),
|
|
2995
3721
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.mode.desc")),
|
|
2996
|
-
modeError && h("div", { style: { fontSize: 12, color: "var(--
|
|
3722
|
+
modeError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, modeError),
|
|
2997
3723
|
mode === null && !modeError
|
|
2998
3724
|
? h("div", { className: "mneme-set-hint" }, "…")
|
|
2999
3725
|
: h(react.Fragment, null,
|
|
@@ -3044,7 +3770,7 @@ window.__ModuleLoader__.load({
|
|
|
3044
3770
|
h("section", { className: "mneme-set-card" },
|
|
3045
3771
|
h("div", { className: "mneme-set-title" }, t("memory.settings.extapi.title")),
|
|
3046
3772
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.extapi.desc")),
|
|
3047
|
-
extapiError && h("div", { style: { fontSize: 12, color: "var(--
|
|
3773
|
+
extapiError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, extapiError),
|
|
3048
3774
|
extapi === null && !extapiError
|
|
3049
3775
|
? h("div", { className: "mneme-set-hint" }, "…")
|
|
3050
3776
|
: h(react.Fragment, null,
|
|
@@ -3288,7 +4014,7 @@ window.__ModuleLoader__.load({
|
|
|
3288
4014
|
loading
|
|
3289
4015
|
? h("div", { className: "mneme-statusnum" }, "…")
|
|
3290
4016
|
: error
|
|
3291
|
-
? h("div", { className: "mneme-statuscap", style: { color: "var(--
|
|
4017
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
|
|
3292
4018
|
: h(react.Fragment, null,
|
|
3293
4019
|
h("div", { className: "mneme-statusnum" }, num),
|
|
3294
4020
|
rows && rows.length
|
|
@@ -3546,6 +4272,250 @@ window.__ModuleLoader__.load({
|
|
|
3546
4272
|
);
|
|
3547
4273
|
}
|
|
3548
4274
|
|
|
4275
|
+
// 人读体积:回收报告里全是字节数(六到七位),不折成 KiB/MiB 就看不出「到底有没有
|
|
4276
|
+
// 变小」——而那正是这张卡要回答的问题。
|
|
4277
|
+
function formatBytes(n) {
|
|
4278
|
+
const v = Number(n);
|
|
4279
|
+
if (!Number.isFinite(v) || v < 0) return "—";
|
|
4280
|
+
if (v < 1024) return `${v} B`;
|
|
4281
|
+
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
4282
|
+
let x = v / 1024;
|
|
4283
|
+
let i = 0;
|
|
4284
|
+
while (x >= 1024 && i < units.length - 1) { x /= 1024; i++; }
|
|
4285
|
+
return `${x.toFixed(x >= 100 ? 0 : x >= 10 ? 1 : 2)} ${units[i]}`;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// 存储回收卡(issue #386 第 4 条,状态页「维护」区)。`dsh-mneme reclaim` 此前只有
|
|
4289
|
+
// CLI 与 standalone 两张脸,面板侧一个字都没有——「不可逆的内容丢弃」这种能力要么
|
|
4290
|
+
// 能被发现,要么等于不存在。
|
|
4291
|
+
//
|
|
4292
|
+
// 三条不变量,别在「顺手简化」时弄丢:
|
|
4293
|
+
// ① 执行是两步。第一次点只进确认态,把这次要动的数字摆到眼前;第二次点才真的发
|
|
4294
|
+
// POST。不用 window.confirm——宿主 webview 里它不一定可用,而且它挡不住「到底
|
|
4295
|
+
// 要清多少行」这个问题。
|
|
4296
|
+
// ② VACUUM 失败 ≠ 成功。清理的两个 UPDATE 各自提交且不可逆,VACUUM 只负责把文件
|
|
4297
|
+
// 缩回去,失败最常见的原因是锁被别的进程占着。所以失败时必须同时说清四件事:
|
|
4298
|
+
// 清理已完成、不可逆、文件暂时不会变小、下一步重跑即可(清理是幂等的)。绝不
|
|
4299
|
+
// 能渲染成一次干净的「成功」。
|
|
4300
|
+
// ③ 保留窗口只在点「重新估算」时发一次请求。键入即发 = 想输 30 却先敲出 3 的那一刻
|
|
4301
|
+
// 就换了估算,数字会跳。而且估算一旦与输入框不一致,确认态里摆出来的就是另一个
|
|
4302
|
+
// 动作的数字——所以那种状态下执行钮直接禁用(见下面的 stale)。
|
|
4303
|
+
//
|
|
4304
|
+
// 报告字段由 src/maintenance.js 的 reclaim() 决定,卡片只读不改。
|
|
4305
|
+
function MaintenanceCard({ t }) {
|
|
4306
|
+
// 与 src/maintenance.js 的 DEFAULT_INPUT_RETENTION_DAYS 同值:这里只用来初始化
|
|
4307
|
+
// 输入框与首屏那次 GET,真正的默认值仍由服务端兜底。
|
|
4308
|
+
const DEFAULT_WINDOW_DAYS = 7;
|
|
4309
|
+
// 与后端 MAX_RECLAIM_WINDOW_DAYS 同值。超界窗口服务端会回 400,但本地先拦一道:
|
|
4310
|
+
// 否则用户点「重新估算」只会看到一句服务端错误,还以为是网络问题。
|
|
4311
|
+
const MAX_WINDOW_DAYS = 3650;
|
|
4312
|
+
// 面板走 in-process 路由,与 CLI/standalone 底下是同一条 reclaim 实现。
|
|
4313
|
+
const RECLAIM_PATH = "/api/dsh-mneme/maintenance/reclaim";
|
|
4314
|
+
const [days, setDays] = useState(String(DEFAULT_WINDOW_DAYS));
|
|
4315
|
+
const [vacuum, setVacuum] = useState(false);
|
|
4316
|
+
const [report, setReport] = useState(null);
|
|
4317
|
+
const [loading, setLoading] = useState(true);
|
|
4318
|
+
const [estimating, setEstimating] = useState(false);
|
|
4319
|
+
const [error, setError] = useState("");
|
|
4320
|
+
// 确认态:true = 第一次点过了,此刻按钮换成实心红确认钮。
|
|
4321
|
+
const [armed, setArmed] = useState(false);
|
|
4322
|
+
const [running, setRunning] = useState(false);
|
|
4323
|
+
// 成功与失败分开存:合成一个对象容易在「先失败后成功」时把上一次的残留显示出来。
|
|
4324
|
+
const [result, setResult] = useState(null);
|
|
4325
|
+
const [resultError, setResultError] = useState("");
|
|
4326
|
+
|
|
4327
|
+
// 挂载即预估:打开状态页就该看见「现在能收多少」,而不是先点一下才有数字。
|
|
4328
|
+
useEffect(() => {
|
|
4329
|
+
let cancelled = false;
|
|
4330
|
+
apiFetch(`${RECLAIM_PATH}?olderThanDays=${DEFAULT_WINDOW_DAYS}`)
|
|
4331
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
4332
|
+
.then((j) => { if (!cancelled) { setReport(j); setLoading(false); } })
|
|
4333
|
+
.catch(() => { if (!cancelled) { setError(t("memory.status.maint.failed")); setLoading(false); } });
|
|
4334
|
+
return () => { cancelled = true; };
|
|
4335
|
+
}, []);
|
|
4336
|
+
|
|
4337
|
+
// 只由「重新估算」按钮调用,不在 onChange 里调用(见上面 ③)。
|
|
4338
|
+
const estimate = () => {
|
|
4339
|
+
if (!/^\d+$/.test(days)) { setError(t("memory.status.maint.badWindow")); setArmed(false); return; }
|
|
4340
|
+
// 上界也在这里拦:`1e20` 与 400 位数字都能过 /^\d+$/(后者化成 Infinity),
|
|
4341
|
+
// 后端会回 400,但那样用户看到的是一句「回收失败」,不如直接说窗口不合法。
|
|
4342
|
+
if (Number(days) > MAX_WINDOW_DAYS) { setError(t("memory.status.maint.badWindow")); setArmed(false); return; }
|
|
4343
|
+
setEstimating(true);
|
|
4344
|
+
setError("");
|
|
4345
|
+
// 窗口一变,上一轮的确认态连同它显示的数字一起作废;上一次执行的回执也别再跟
|
|
4346
|
+
// 新的估算混在一起。
|
|
4347
|
+
setArmed(false);
|
|
4348
|
+
setResult(null);
|
|
4349
|
+
setResultError("");
|
|
4350
|
+
apiFetch(`${RECLAIM_PATH}?olderThanDays=${days}`)
|
|
4351
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
4352
|
+
.then((j) => { setReport(j); setEstimating(false); })
|
|
4353
|
+
.catch(() => { setError(t("memory.status.maint.failed")); setEstimating(false); });
|
|
4354
|
+
};
|
|
4355
|
+
|
|
4356
|
+
// 第二次点击(确认态里)才走这里。dryRun:false 与 confirm:true 是后端那条不可逆
|
|
4357
|
+
// 动作的确认门:缺 confirm 会回 400 confirm-required 并附一份 dry-run 报告。两个
|
|
4358
|
+
// 字面量逐字保留——改名或省掉一个都等于把门拆了。
|
|
4359
|
+
const execute = () => {
|
|
4360
|
+
setRunning(true);
|
|
4361
|
+
setResultError("");
|
|
4362
|
+
apiFetch(RECLAIM_PATH, {
|
|
4363
|
+
method: "POST",
|
|
4364
|
+
headers: { "Content-Type": "application/json" },
|
|
4365
|
+
body: JSON.stringify({ dryRun: false, confirm: true, olderThanDays: Number(days), vacuum })
|
|
4366
|
+
})
|
|
4367
|
+
.then((res) => res.json().catch(() => null).then((body) => ({ ok: res.ok, status: res.status, body })))
|
|
4368
|
+
.then((r) => {
|
|
4369
|
+
// 非 2xx 一律落到这条分支并带着服务端的 error 串返回:成功态只在守卫另一侧。
|
|
4370
|
+
if (!r.ok) {
|
|
4371
|
+
setResultError(`${t("memory.status.maint.failed")} · ${(r.body && r.body.error) || `HTTP ${r.status}`}`);
|
|
4372
|
+
setRunning(false);
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4375
|
+
setResult(r.body);
|
|
4376
|
+
setArmed(false);
|
|
4377
|
+
setRunning(false);
|
|
4378
|
+
})
|
|
4379
|
+
.catch(() => { setResultError(t("memory.status.maint.failed")); setRunning(false); });
|
|
4380
|
+
};
|
|
4381
|
+
|
|
4382
|
+
const size = (report && report.size) || {};
|
|
4383
|
+
const planned = (report && report.planned) || {};
|
|
4384
|
+
const rv = (result && result.vacuum) || {};
|
|
4385
|
+
const rsz = (result && result.size) || {};
|
|
4386
|
+
const rcl = (result && result.cleared) || {};
|
|
4387
|
+
// 估算数字与输入框不一致(刚改过窗口还没重新估算)时禁止执行:否则确认态里摆出来
|
|
4388
|
+
// 的是另一个窗口的数字,而对不可逆操作来说这是最坏的一种错位。
|
|
4389
|
+
const stale = !report || String(report.olderThanDays) !== days;
|
|
4390
|
+
// vacuum.error 是 string|null;非 null 就是「清理成功但文件没缩」这种半成功态。
|
|
4391
|
+
const vacuumError = rv.error == null ? null : String(rv.error);
|
|
4392
|
+
const freed = Number(rsz.before_bytes ?? 0) - Number(rsz.after_bytes ?? 0);
|
|
4393
|
+
|
|
4394
|
+
return h("div", { className: "mneme-statuscard mneme-statuscard--wide" },
|
|
4395
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.maint")),
|
|
4396
|
+
loading
|
|
4397
|
+
? h("div", { className: "mneme-statusnum" }, "…")
|
|
4398
|
+
: h("div", { className: "mneme-statusnum" }, formatBytes(size.before_bytes)),
|
|
4399
|
+
report ? h("div", { className: "mneme-statrows" },
|
|
4400
|
+
h("div", { className: "mneme-statrow" },
|
|
4401
|
+
h("span", null, t("memory.status.maint.freelist")),
|
|
4402
|
+
h("span", { className: "mneme-statrowval" }, `${formatBytes(size.freelist_bytes)} · ${size.freelist_pages ?? 0}`)),
|
|
4403
|
+
h("div", { className: "mneme-statrow" },
|
|
4404
|
+
h("span", null, t("memory.status.maint.plannedInputs")),
|
|
4405
|
+
h("span", { className: "mneme-statrowval" }, String(planned.dream_run_inputs ?? 0))),
|
|
4406
|
+
h("div", { className: "mneme-statrow" },
|
|
4407
|
+
h("span", null, t("memory.status.maint.plannedEmbeddings")),
|
|
4408
|
+
h("span", { className: "mneme-statrowval" }, String(planned.archived_embeddings ?? 0))),
|
|
4409
|
+
h("div", { className: "mneme-statrow" },
|
|
4410
|
+
h("span", null, t("memory.status.maint.window")),
|
|
4411
|
+
h("span", { className: "mneme-statrowval" }, `${report.olderThanDays} ${t("memory.status.maint.windowUnit")}`))
|
|
4412
|
+
) : null,
|
|
4413
|
+
// 控制区:窗口只在点按钮时重新估算;VACUUM 默认关,并明说它会短暂独占数据库。
|
|
4414
|
+
h("div", { className: "mneme-maintrow" },
|
|
4415
|
+
h("span", { className: "mneme-maintlbl" }, t("memory.status.maint.windowLabel")),
|
|
4416
|
+
h("input", {
|
|
4417
|
+
className: "mneme-set-input",
|
|
4418
|
+
inputMode: "numeric",
|
|
4419
|
+
value: days,
|
|
4420
|
+
disabled: armed || running,
|
|
4421
|
+
onChange: (e) => setDays(e.target.value)
|
|
4422
|
+
}),
|
|
4423
|
+
h("button", {
|
|
4424
|
+
type: "button",
|
|
4425
|
+
className: "mneme-btn",
|
|
4426
|
+
disabled: armed || running || estimating,
|
|
4427
|
+
onClick: estimate
|
|
4428
|
+
}, estimating ? t("memory.status.maint.reestimating") : t("memory.status.maint.reestimate"))
|
|
4429
|
+
),
|
|
4430
|
+
h("label", { className: "mneme-maintcheck" },
|
|
4431
|
+
h("input", {
|
|
4432
|
+
type: "checkbox",
|
|
4433
|
+
checked: vacuum,
|
|
4434
|
+
disabled: armed || running,
|
|
4435
|
+
onChange: (e) => setVacuum(e.target.checked)
|
|
4436
|
+
}),
|
|
4437
|
+
h("span", null, t("memory.status.maint.vacuum"))
|
|
4438
|
+
),
|
|
4439
|
+
h("div", { className: "mneme-statuscap" }, t("memory.status.maint.vacuumHint")),
|
|
4440
|
+
stale && !loading && !estimating
|
|
4441
|
+
? h("div", { className: "mneme-statuscap" }, t("memory.status.maint.stale"))
|
|
4442
|
+
: null,
|
|
4443
|
+
// 两步确认。未武装时只有「执行回收」(点了只进确认态,不发请求);武装后换成实心
|
|
4444
|
+
// 红确认钮,并把窗口/VACUUM 在确认态里锁住——保证屏幕上的数字就是即将发出的动作。
|
|
4445
|
+
armed
|
|
4446
|
+
? h(react.Fragment, null,
|
|
4447
|
+
h("div", { className: "mneme-maintwarn" },
|
|
4448
|
+
h("div", { className: "mneme-maintwarnhead" },
|
|
4449
|
+
t("memory.status.maint.confirmHint")
|
|
4450
|
+
.replace("{inputs}", String(planned.dream_run_inputs ?? 0))
|
|
4451
|
+
.replace("{embeddings}", String(planned.archived_embeddings ?? 0))),
|
|
4452
|
+
h("div", { className: "mneme-statuscap" },
|
|
4453
|
+
`${t("memory.status.maint.window")} ${report.olderThanDays} ${t("memory.status.maint.windowUnit")} · `
|
|
4454
|
+
+ (vacuum ? t("memory.status.maint.vacuum") : t("memory.status.maint.vacuumOff")))
|
|
4455
|
+
),
|
|
4456
|
+
h("div", { className: "mneme-maintrow" },
|
|
4457
|
+
h("button", {
|
|
4458
|
+
type: "button",
|
|
4459
|
+
className: "mneme-btn mneme-btndangerconfirm",
|
|
4460
|
+
disabled: running,
|
|
4461
|
+
onClick: execute
|
|
4462
|
+
}, running ? t("memory.status.maint.running") : t("memory.status.maint.confirm")),
|
|
4463
|
+
h("button", {
|
|
4464
|
+
type: "button",
|
|
4465
|
+
className: "mneme-btn",
|
|
4466
|
+
disabled: running,
|
|
4467
|
+
onClick: () => setArmed(false)
|
|
4468
|
+
}, t("memory.status.maint.cancel"))
|
|
4469
|
+
)
|
|
4470
|
+
)
|
|
4471
|
+
: h("div", { className: "mneme-maintrow" },
|
|
4472
|
+
h("button", {
|
|
4473
|
+
type: "button",
|
|
4474
|
+
className: "mneme-btn mneme-btndanger",
|
|
4475
|
+
disabled: running || loading || estimating || stale,
|
|
4476
|
+
onClick: () => setArmed(true)
|
|
4477
|
+
}, t("memory.status.maint.execute"))
|
|
4478
|
+
),
|
|
4479
|
+
result ? h("div", { className: "mneme-maintresult" },
|
|
4480
|
+
h("div", { className: "mneme-maintresulthead" },
|
|
4481
|
+
vacuumError ? t("memory.status.maint.vacuumFailed") : t("memory.status.maint.done")),
|
|
4482
|
+
h("div", { className: "mneme-statrows" },
|
|
4483
|
+
h("div", { className: "mneme-statrow" },
|
|
4484
|
+
h("span", null, t("memory.status.maint.clearedInputs")),
|
|
4485
|
+
h("span", { className: "mneme-statrowval" }, String(rcl.dream_run_inputs ?? 0))),
|
|
4486
|
+
h("div", { className: "mneme-statrow" },
|
|
4487
|
+
h("span", null, t("memory.status.maint.clearedEmbeddings")),
|
|
4488
|
+
h("span", { className: "mneme-statrowval" }, String(rcl.archived_embeddings ?? 0))),
|
|
4489
|
+
h("div", { className: "mneme-statrow" },
|
|
4490
|
+
h("span", null, t("memory.status.maint.size")),
|
|
4491
|
+
h("span", { className: "mneme-statrowval" }, `${formatBytes(rsz.before_bytes)} → ${formatBytes(rsz.after_bytes)}`)),
|
|
4492
|
+
h("div", { className: "mneme-statrow" },
|
|
4493
|
+
h("span", null, t("memory.status.maint.reclaimed")),
|
|
4494
|
+
h("span", { className: "mneme-statrowval" }, formatBytes(freed))),
|
|
4495
|
+
h("div", { className: "mneme-statrow" },
|
|
4496
|
+
h("span", null, t("memory.status.maint.receipt")),
|
|
4497
|
+
h("span", { className: "mneme-statrowval" },
|
|
4498
|
+
result.receipt == null ? t("memory.status.maint.receiptNone") : String(result.receipt))),
|
|
4499
|
+
h("div", { className: "mneme-statrow" },
|
|
4500
|
+
h("span", null, t("memory.status.maint.duration")),
|
|
4501
|
+
h("span", { className: "mneme-statrowval" }, `${Number(result.duration_ms ?? 0)} ms`))
|
|
4502
|
+
),
|
|
4503
|
+
// VACUUM 失败 ≠ 成功:清理已经落库且不可逆,失败的只是「把文件缩回去」这一步。
|
|
4504
|
+
// 这里必须把「已完成 + 不可逆 + 暂时不会变小」和下一步(重跑即可,清理是幂等
|
|
4505
|
+
// 的)一起说清,否则用户只看到「没变小」会以为白清了。
|
|
4506
|
+
vacuumError
|
|
4507
|
+
? h("div", { className: "mneme-maintwarn" },
|
|
4508
|
+
h("div", { className: "mneme-maintwarnhead" },
|
|
4509
|
+
`${t("memory.status.maint.vacuumFailed")} · ${vacuumError}`),
|
|
4510
|
+
h("div", { className: "mneme-statuscap" }, t("memory.status.maint.vacuumFailedHint")))
|
|
4511
|
+
: h("div", { className: "mneme-statuscap" },
|
|
4512
|
+
rv.ran ? t("memory.status.maint.vacuumRan") : t("memory.status.maint.vacuumOff"))
|
|
4513
|
+
) : null,
|
|
4514
|
+
resultError ? h("div", { className: "mneme-mainterr" }, resultError) : null,
|
|
4515
|
+
error ? h("div", { className: "mneme-mainterr" }, error) : null
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
|
|
3549
4519
|
// Markdown 镜像健康卡(issue #386 第 3 条)。后端 /health 对镜像读失败是
|
|
3550
4520
|
// fail-closed(dirty=null → unknown);面板侧同样 fail-closed:fetch 失败
|
|
3551
4521
|
// 或任何非 ok/degraded 的 status 一律渲染成「未知」,绝不显示「正常」——
|
|
@@ -3585,34 +4555,256 @@ window.__ModuleLoader__.load({
|
|
|
3585
4555
|
});
|
|
3586
4556
|
}
|
|
3587
4557
|
|
|
3588
|
-
//
|
|
3589
|
-
|
|
3590
|
-
|
|
4558
|
+
// --- Token 去向三屏(2026-10 可观测性改造)---
|
|
4559
|
+
// 原 LlmStatusCard 只有一个「LLM 消耗」总数,且 by_source 把两个最大成本项
|
|
4560
|
+
// dream_consolidate / dream_summarize 合并成 autoDream(2026-10 审计里巩固
|
|
4561
|
+
// 单项就占 37%),「token 花在哪」不可见。这里换成三屏:KPI 带(总量/调用/
|
|
4562
|
+
// 巩固占比/待确认冲突)、按组件 donut、按天柱状 + 8 周热力;原 9 张状态卡
|
|
4563
|
+
// 降级到折叠区。数据走 getLlmAuditStats 新增的 by_operation / by_day 两个
|
|
4564
|
+
// 聚合(后端已排好序,前端按 TOKEN_OPS 恒定顺序取用,不依赖 API 排序)。
|
|
4565
|
+
const TOKEN_OPS = [
|
|
4566
|
+
"dream_consolidate",
|
|
4567
|
+
"summarize_compress",
|
|
4568
|
+
"entity_extract",
|
|
4569
|
+
"dream_summarize",
|
|
4570
|
+
"sleep_pattern",
|
|
4571
|
+
"write_admission"
|
|
4572
|
+
];
|
|
4573
|
+
const tokenOpLabel = (t, op) => {
|
|
4574
|
+
const key = `memory.status.token.op.${op}`;
|
|
4575
|
+
const label = t(key);
|
|
4576
|
+
return label !== key ? label : String(op);
|
|
4577
|
+
};
|
|
4578
|
+
const tokenCompact = (n) => {
|
|
4579
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
4580
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
4581
|
+
return String(n);
|
|
4582
|
+
};
|
|
4583
|
+
const heatLevel = (n) => (n > 2500000 ? 4 : n > 1500000 ? 3 : n > 750000 ? 2 : n > 0 ? 1 : 0);
|
|
4584
|
+
|
|
4585
|
+
function TokenFlowSection({ t }) {
|
|
4586
|
+
const [stats, setStats] = useState(null);
|
|
4587
|
+
const [heatStats, setHeatStats] = useState(null);
|
|
4588
|
+
const [statsError, setStatsError] = useState(false);
|
|
4589
|
+
const [conf, setConf] = useState({ loading: true, error: false, pending: 0 });
|
|
4590
|
+
const [focusOp, setFocusOp] = useState(null);
|
|
3591
4591
|
useEffect(() => {
|
|
3592
4592
|
let cancelled = false;
|
|
3593
4593
|
apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=7")
|
|
3594
4594
|
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
3595
4595
|
.then((j) => {
|
|
3596
4596
|
if (cancelled) return;
|
|
3597
|
-
|
|
3598
|
-
|
|
4597
|
+
setStats(j && typeof j === "object" ? (j.stats || j) : {});
|
|
4598
|
+
setStatsError(false);
|
|
3599
4599
|
})
|
|
3600
|
-
.catch(() => { if (!cancelled)
|
|
3601
|
-
|
|
4600
|
+
.catch(() => { if (!cancelled) setStatsError(true); });
|
|
4601
|
+
// 8 周热力要 56 天窗口,而 days=7 的 by_day 最多只有 8 个 UTC 日期——
|
|
4602
|
+
// 复用同一份响应会让 56 格里 48 格恒为 0(自动评审 #412 指出)。两个
|
|
4603
|
+
// 窗口各取所需:7 天喂 KPI 与柱状,56 天只喂热力。
|
|
4604
|
+
apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=56")
|
|
4605
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
4606
|
+
.then((j) => {
|
|
4607
|
+
if (cancelled) return;
|
|
4608
|
+
setHeatStats(j && typeof j === "object" ? (j.stats || j) : {});
|
|
4609
|
+
})
|
|
4610
|
+
.catch(() => { if (!cancelled) setHeatStats({}); });
|
|
4611
|
+
// KPI 第 4 格的待确认冲突与 DreamStatusCards 同源(/dream-status),
|
|
4612
|
+
// 同样监听 mneme:conflicts-changed 重取(#295),两处计数永不漂移。
|
|
4613
|
+
const loadConflicts = () => {
|
|
4614
|
+
apiFetch("/api/dsh-mneme/dream-status")
|
|
4615
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
4616
|
+
.then((d) => {
|
|
4617
|
+
if (cancelled) return;
|
|
4618
|
+
setConf({ loading: false, error: false, pending: Number((d && d.pendingConflicts) ?? 0) });
|
|
4619
|
+
})
|
|
4620
|
+
.catch(() => { if (!cancelled) setConf({ loading: false, error: true, pending: 0 }); });
|
|
4621
|
+
};
|
|
4622
|
+
loadConflicts();
|
|
4623
|
+
window.addEventListener("mneme:conflicts-changed", loadConflicts);
|
|
4624
|
+
return () => { cancelled = true; window.removeEventListener("mneme:conflicts-changed", loadConflicts); };
|
|
3602
4625
|
}, []);
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
4626
|
+
|
|
4627
|
+
const loading = !stats && !statsError;
|
|
4628
|
+
const byOp = stats && Array.isArray(stats.by_operation) ? stats.by_operation : [];
|
|
4629
|
+
// 按恒定顺序取行:API 排序变化不影响图例次序;未知操作留到渲染时兜底。
|
|
4630
|
+
const ops = TOKEN_OPS.map((op) => {
|
|
4631
|
+
const row = byOp.find((r) => r.operation === op);
|
|
4632
|
+
return { op, total: Number((row && row.total_tokens) ?? 0) };
|
|
3610
4633
|
});
|
|
4634
|
+
const unknownOps = byOp.filter((r) => !TOKEN_OPS.includes(r.operation));
|
|
4635
|
+
const allOps = ops.concat(unknownOps.map((r) => ({ op: r.operation, total: Number(r.total_tokens ?? 0) })));
|
|
4636
|
+
const grand = allOps.reduce((a, r) => a + r.total, 0);
|
|
4637
|
+
const consolidate = ops[0].total;
|
|
4638
|
+
// donut:r=15.9155 让周长≈100,dasharray 直接填百分比;段与段留缝(取
|
|
4639
|
+
// 半个百分点与四分之一弧长的较小者),小弧不被缝吃掉。write_admission
|
|
4640
|
+
// 的 token 恒 0 是设计(只计量不拦截),自然不画弧,图例仍保留一行。
|
|
4641
|
+
let cursor = 0;
|
|
4642
|
+
const segs = allOps
|
|
4643
|
+
.map((r, i) => {
|
|
4644
|
+
const len = grand > 0 ? (r.total / grand) * 100 : 0;
|
|
4645
|
+
const gap = Math.min(0.25, len / 4);
|
|
4646
|
+
const seg = Math.max(len - gap * 2, 0.4);
|
|
4647
|
+
const off = -(cursor + gap);
|
|
4648
|
+
cursor += len;
|
|
4649
|
+
return { ...r, i, len, seg, off };
|
|
4650
|
+
})
|
|
4651
|
+
.filter((s) => s.len > 0);
|
|
4652
|
+
const opIdx = (op) => {
|
|
4653
|
+
const i = TOKEN_OPS.indexOf(op);
|
|
4654
|
+
return i >= 0 ? i : Math.max(0, Math.min(allOps.findIndex((r) => r.op === op), 5));
|
|
4655
|
+
};
|
|
4656
|
+
// 7 天柱状:上限向上取整到整 M,四条网格线按 niceMax/4 等距。
|
|
4657
|
+
// by_day 只返回有审计行的日期(SQL 是 GROUP BY,缺日无行),而 since=now-7d
|
|
4658
|
+
// 的窗口横跨 8 个 UTC 日期——直接映射会让柱子缺列、横轴不是连续 7 天(自动
|
|
4659
|
+
// 评审 #412 指出)。这里以「今天 UTC」为右端补成连续 7 天,缺日填 0;响应还
|
|
4660
|
+
// 没到时保持空数组,否则加载态会先画 7 根零高柱子(stats 为 null 与 {} 分属
|
|
4661
|
+
// 「未知」与「已知为空」,只有后者该补零)。
|
|
4662
|
+
const rawDays = stats && Array.isArray(stats.by_day) ? stats.by_day : null;
|
|
4663
|
+
const rawDayMap = new Map((rawDays ?? []).map((d) => [String((d && d.day) ?? ""), Number((d && d.total_tokens) ?? 0)]));
|
|
4664
|
+
const endDay = new Date(`${new Date().toISOString().slice(0, 10)}T00:00:00Z`);
|
|
4665
|
+
const days = rawDays
|
|
4666
|
+
? Array.from({ length: 7 }, (_, i) => {
|
|
4667
|
+
const key = new Date(endDay.getTime() - (6 - i) * 86400000).toISOString().slice(0, 10);
|
|
4668
|
+
return { day: key, total: rawDayMap.get(key) ?? 0 };
|
|
4669
|
+
})
|
|
4670
|
+
: [];
|
|
4671
|
+
const dayMax = Math.max(1, ...days.map((d) => d.total));
|
|
4672
|
+
const niceMax = Math.max(1e6, Math.ceil(dayMax / 1e6) * 1e6);
|
|
4673
|
+
const gridVals = [niceMax, niceMax * 0.75, niceMax * 0.5, niceMax * 0.25];
|
|
4674
|
+
// 8 周热力:数据必须来自 56 天窗口(heatStats),右缘对齐该窗口最后一天
|
|
4675
|
+
// (UTC),左侧补齐到周一;数据窗口之外的格子隐藏留形。日期一律走 UTC——
|
|
4676
|
+
// by_day 的 day 就是 UTC 日期。用 heatStats 而非 stats:否则 56 格里
|
|
4677
|
+
// 48 格恒 0。
|
|
4678
|
+
const heatDays = (heatStats && Array.isArray(heatStats.by_day) ? heatStats.by_day : []).map((d) => ({
|
|
4679
|
+
day: String((d && d.day) ?? ""),
|
|
4680
|
+
total: Number((d && d.total_tokens) ?? 0)
|
|
4681
|
+
}));
|
|
4682
|
+
const heatCells = [];
|
|
4683
|
+
if (heatDays.length) {
|
|
4684
|
+
const byDayMap = new Map(heatDays.map((d) => [d.day, d.total]));
|
|
4685
|
+
const end = new Date(`${heatDays[heatDays.length - 1].day}T00:00:00Z`);
|
|
4686
|
+
const start = new Date(end.getTime() - 55 * 86400000);
|
|
4687
|
+
for (let i = 0; i < 56; i++) {
|
|
4688
|
+
const d = new Date(start.getTime() + i * 86400000);
|
|
4689
|
+
const key = d.toISOString().slice(0, 10);
|
|
4690
|
+
heatCells.push({ key, total: byDayMap.get(key) ?? 0 });
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
const kpi = (title, num, cap) =>
|
|
4694
|
+
h(StatusCard, { t, title, loading, error: statsError, num, cap });
|
|
4695
|
+
const segClass = (s) =>
|
|
4696
|
+
`mneme-seg mneme-seg--${opIdx(s.op) + 1}${focusOp === s.op ? " focused" : ""}`;
|
|
4697
|
+
const rowProps = (op) => ({
|
|
4698
|
+
className: focusOp && focusOp !== op ? "mneme-legendrow dim" : focusOp === op ? "mneme-legendrow focused" : "mneme-legendrow",
|
|
4699
|
+
onMouseEnter: () => setFocusOp(op),
|
|
4700
|
+
onMouseLeave: () => setFocusOp(null)
|
|
4701
|
+
});
|
|
4702
|
+
|
|
4703
|
+
return h(react.Fragment, null,
|
|
4704
|
+
h("div", { className: "mneme-statushead" }, t("memory.status.sec.token")),
|
|
4705
|
+
h("div", { className: "mneme-kpiband" },
|
|
4706
|
+
kpi(
|
|
4707
|
+
t("memory.status.token.total"),
|
|
4708
|
+
Number((stats && stats.total_tokens) ?? 0).toLocaleString(),
|
|
4709
|
+
`${t("memory.status.token.in")} ${Number((stats && stats.input_tokens) ?? 0).toLocaleString()} · ${t("memory.status.token.out")} ${Number((stats && stats.output_tokens) ?? 0).toLocaleString()}`
|
|
4710
|
+
),
|
|
4711
|
+
kpi(t("memory.status.token.calls"), Number((stats && stats.total_calls) ?? 0).toLocaleString(), t("memory.status.token.callsCap")),
|
|
4712
|
+
kpi(
|
|
4713
|
+
t("memory.status.token.share"),
|
|
4714
|
+
grand > 0 ? `${((consolidate / grand) * 100).toFixed(1)}%` : "0.0%",
|
|
4715
|
+
t("memory.status.token.shareFoot").replace("{n}", consolidate.toLocaleString())
|
|
4716
|
+
),
|
|
4717
|
+
h(StatusCard, {
|
|
4718
|
+
t,
|
|
4719
|
+
title: t("memory.status.token.conflicts"),
|
|
4720
|
+
loading: conf.loading,
|
|
4721
|
+
error: conf.error,
|
|
4722
|
+
num: conf.pending.toLocaleString(),
|
|
4723
|
+
cap: t("memory.status.token.conflictsFoot")
|
|
4724
|
+
})
|
|
4725
|
+
),
|
|
4726
|
+
h("div", { className: "mneme-tokenflow" },
|
|
4727
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.donut")),
|
|
4728
|
+
statsError
|
|
4729
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
|
|
4730
|
+
: h("div", { className: "mneme-donutwrap" },
|
|
4731
|
+
h("div", { className: "mneme-donutbox" },
|
|
4732
|
+
h("svg", {
|
|
4733
|
+
className: focusOp ? "mneme-donut has-focus" : "mneme-donut",
|
|
4734
|
+
viewBox: "0 0 42 42",
|
|
4735
|
+
"aria-hidden": "true"
|
|
4736
|
+
},
|
|
4737
|
+
h("g", { transform: "rotate(-90 21 21)" },
|
|
4738
|
+
segs.map((s) => h("circle", {
|
|
4739
|
+
key: s.op,
|
|
4740
|
+
className: segClass(s),
|
|
4741
|
+
cx: 21,
|
|
4742
|
+
cy: 21,
|
|
4743
|
+
r: 15.9155,
|
|
4744
|
+
"stroke-dasharray": `${s.seg.toFixed(2)} ${(100 - s.seg).toFixed(2)}`,
|
|
4745
|
+
"stroke-dashoffset": s.off.toFixed(2)
|
|
4746
|
+
}))
|
|
4747
|
+
)
|
|
4748
|
+
),
|
|
4749
|
+
h("div", { className: "mneme-donut-center" },
|
|
4750
|
+
h("b", null, tokenCompact(grand)),
|
|
4751
|
+
h("span", null, t("memory.status.token.last7"))
|
|
4752
|
+
)
|
|
4753
|
+
),
|
|
4754
|
+
h("div", { className: "mneme-legend" },
|
|
4755
|
+
allOps.map((r) => h("div", Object.assign({ key: r.op }, rowProps(r.op)),
|
|
4756
|
+
h("span", { className: `mneme-legenddot mneme-dot--${opIdx(r.op) + 1}` }),
|
|
4757
|
+
h("span", { className: "mneme-legendname" }, tokenOpLabel(t, r.op)),
|
|
4758
|
+
r.op === "write_admission"
|
|
4759
|
+
? h("span", { className: "mneme-legendsub" }, t("memory.status.token.meterOnly"))
|
|
4760
|
+
: null,
|
|
4761
|
+
h("span", { className: "mneme-legendval" }, r.total.toLocaleString()),
|
|
4762
|
+
h("span", { className: "mneme-legendpct" },
|
|
4763
|
+
grand > 0 ? `${((r.total / grand) * 100).toFixed(1)}%` : "0.0%")
|
|
4764
|
+
))
|
|
4765
|
+
)
|
|
4766
|
+
),
|
|
4767
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.days")),
|
|
4768
|
+
h("div", { className: "mneme-bars" },
|
|
4769
|
+
gridVals.map((v, i) => h("div", { key: v, className: "mneme-gridline", style: { top: `${i * 25}%` } },
|
|
4770
|
+
h("span", { className: "mneme-gridlabel" }, `${(v / 1e6).toFixed(0)}M`)
|
|
4771
|
+
)),
|
|
4772
|
+
h("div", { className: "mneme-barcols" },
|
|
4773
|
+
days.map((d) => h("div", { className: "mneme-barcol", key: d.day },
|
|
4774
|
+
h("div", {
|
|
4775
|
+
className: "mneme-bar",
|
|
4776
|
+
style: { height: `${Math.max((d.total / niceMax) * 100, 0.4)}%` },
|
|
4777
|
+
title: `${d.day} · ${d.total.toLocaleString()}`
|
|
4778
|
+
})
|
|
4779
|
+
))
|
|
4780
|
+
)
|
|
4781
|
+
),
|
|
4782
|
+
h("div", { className: "mneme-barlabels" },
|
|
4783
|
+
days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
|
|
4784
|
+
),
|
|
4785
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
|
|
4786
|
+
h("div", { className: "mneme-heatgrid" },
|
|
4787
|
+
heatCells.map((c) => h("div", {
|
|
4788
|
+
key: c.key,
|
|
4789
|
+
className: heatLevel(c.total) ? `mneme-heatcell l${heatLevel(c.total)}` : "mneme-heatcell",
|
|
4790
|
+
title: `${c.key} · ${c.total.toLocaleString()}`
|
|
4791
|
+
}))
|
|
4792
|
+
),
|
|
4793
|
+
h("div", { className: "mneme-heatlegend" },
|
|
4794
|
+
h("span", null, t("memory.status.token.heatLess")),
|
|
4795
|
+
h("span", { className: "mneme-heatcell" }),
|
|
4796
|
+
[1, 2, 3, 4].map((lv) => h("span", { key: lv, className: `mneme-heatcell l${lv}` })),
|
|
4797
|
+
h("span", null, t("memory.status.token.heatMore"))
|
|
4798
|
+
)
|
|
4799
|
+
)
|
|
4800
|
+
);
|
|
3611
4801
|
}
|
|
3612
4802
|
|
|
3613
4803
|
// 巩固状态卡(×2):最近一次 autoDream 运行 + 待确认冲突计数。数据来自
|
|
3614
4804
|
// /dream-status,一次取回两张卡共用;失败只影响这两张卡自身。
|
|
3615
|
-
|
|
4805
|
+
// showConflicts=false 用于 2026-10 改造后的降级折叠区:待确认冲突已升入
|
|
4806
|
+
// KPI 带,这里不再重复渲染冲突卡,只留最近运行卡。
|
|
4807
|
+
function DreamStatusCards({ t, onGotoQueue, showConflicts = true }) {
|
|
3616
4808
|
const [state, setState] = useState({ loading: true, error: false, lastRun: null, pending: 0 });
|
|
3617
4809
|
useEffect(() => {
|
|
3618
4810
|
let cancelled = false;
|
|
@@ -3647,24 +4839,26 @@ window.__ModuleLoader__.load({
|
|
|
3647
4839
|
const gotoQueue = () => { if (pending && onGotoQueue) onGotoQueue(); };
|
|
3648
4840
|
return h(react.Fragment, null,
|
|
3649
4841
|
h(StatusCard, { t, title: t("memory.status.dream"), loading: state.loading, error: state.error, num, cap }),
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
4842
|
+
showConflicts
|
|
4843
|
+
? h("div", {
|
|
4844
|
+
role: pending ? "button" : undefined,
|
|
4845
|
+
tabIndex: pending ? 0 : undefined,
|
|
4846
|
+
"aria-label": pending ? t("memory.status.conflictQueue.goto") : undefined,
|
|
4847
|
+
className: pending ? "mneme-conflict-jump" : undefined,
|
|
4848
|
+
style: pending ? { cursor: "pointer" } : undefined,
|
|
4849
|
+
onClick: pending ? gotoQueue : undefined,
|
|
4850
|
+
onKeyDown: pending ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); gotoQueue(); } } : undefined
|
|
4851
|
+
},
|
|
4852
|
+
h(StatusCard, {
|
|
4853
|
+
t,
|
|
4854
|
+
title: t("memory.status.conflicts"),
|
|
4855
|
+
loading: state.loading,
|
|
4856
|
+
error: state.error,
|
|
4857
|
+
num: state.pending.toLocaleString(),
|
|
4858
|
+
cap: t("memory.status.conflictsHint")
|
|
4859
|
+
})
|
|
4860
|
+
)
|
|
4861
|
+
: null
|
|
3668
4862
|
);
|
|
3669
4863
|
}
|
|
3670
4864
|
|
|
@@ -3682,7 +4876,7 @@ window.__ModuleLoader__.load({
|
|
|
3682
4876
|
return () => { cancelled = true; };
|
|
3683
4877
|
}, []);
|
|
3684
4878
|
if (state.loading || !state.suppressed) return null;
|
|
3685
|
-
return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--
|
|
4879
|
+
return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--mneme-warn)" } },
|
|
3686
4880
|
h("div", { className: "mneme-statusnum", style: { fontSize: "16px", lineHeight: "24px" } }, t("memory.status.injectSuppressed")),
|
|
3687
4881
|
h("div", { className: "mneme-statuscap" }, t("memory.status.injectSuppressedHint"))
|
|
3688
4882
|
);
|
|
@@ -3757,6 +4951,17 @@ window.__ModuleLoader__.load({
|
|
|
3757
4951
|
const [items, setItems] = useState(null);
|
|
3758
4952
|
const [loadError, setLoadError] = useState(false);
|
|
3759
4953
|
const [busy, setBusy] = useState(false);
|
|
4954
|
+
// 展开态按冲突条目记(一对两侧同时展开,便于逐行对照);复制态按
|
|
4955
|
+
// 「条目:侧别」记,避免一次复制把别的按钮也点亮。
|
|
4956
|
+
const [openItems, setOpenItems] = useState({});
|
|
4957
|
+
const [copiedKey, setCopiedKey] = useState(null);
|
|
4958
|
+
const toggleItem = (id) => setOpenItems((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
4959
|
+
const copySide = (key, content) => {
|
|
4960
|
+
navigator.clipboard?.writeText(String(content || "")).then(
|
|
4961
|
+
() => { setCopiedKey(key); announce(t("memory.status.conflictQueue.copiedSide")); setTimeout(() => setCopiedKey((k) => (k === key ? null : k)), 1500); },
|
|
4962
|
+
() => {}
|
|
4963
|
+
);
|
|
4964
|
+
};
|
|
3760
4965
|
const load = (silent) => {
|
|
3761
4966
|
apiFetch("/api/dsh-mneme/conflicts")
|
|
3762
4967
|
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
@@ -3791,18 +4996,39 @@ window.__ModuleLoader__.load({
|
|
|
3791
4996
|
.catch(() => {})
|
|
3792
4997
|
.finally(() => setBusy(false));
|
|
3793
4998
|
};
|
|
3794
|
-
const side = (s, label, sideClass, ops, diffSide) => h("div", { className: `mneme-conflict-side ${sideClass}` },
|
|
4999
|
+
const side = (s, label, sideClass, ops, diffSide, open, itemKey) => h("div", { className: `mneme-conflict-side ${sideClass}` },
|
|
3795
5000
|
h("div", { className: "mneme-conflict-sidelabel" }, label),
|
|
3796
5001
|
s.missing
|
|
3797
5002
|
? h("div", { className: "mneme-conflict-missing" }, t("memory.status.conflictQueue.missing"))
|
|
3798
5003
|
: h(react.Fragment, null,
|
|
3799
5004
|
h("div", { className: "mneme-conflict-sidetitle", title: s.title }, s.title || "…"),
|
|
5005
|
+
// 判读依据行:类型 / 重要度 / 字数 / 新旧 + 单侧全文复制。报障者
|
|
5006
|
+
// 「只好两个都删或胡乱选」的直接原因是这一行了无信息——只有
|
|
5007
|
+
// A 方/B 方两个标签,看不出哪条更新、哪条更长、哪条更重要。
|
|
5008
|
+
h("div", { className: "mneme-conflict-meta" },
|
|
5009
|
+
h("span", { className: "mneme-conflict-metaval" }, typeLabel(t, s.type)),
|
|
5010
|
+
s.importance !== null && s.importance !== undefined
|
|
5011
|
+
? h("span", null, t("memory.status.conflictQueue.importance").replace("{n}", String(s.importance)))
|
|
5012
|
+
: null,
|
|
5013
|
+
h("span", null, t("memory.status.conflictQueue.chars").replace("{n}", String((s.content || "").length))),
|
|
5014
|
+
h("span", null, (s.updated_at ? t("memory.status.conflictQueue.updated") : t("memory.status.conflictQueue.created"))
|
|
5015
|
+
.replace("{time}", formatRelativeTime(s.updated_at || s.created_at, t))),
|
|
5016
|
+
h("button", {
|
|
5017
|
+
className: "mneme-footbtn",
|
|
5018
|
+
onClick: () => copySide(`${itemKey}:${diffSide}`, s.content)
|
|
5019
|
+
}, copiedKey === `${itemKey}:${diffSide}`
|
|
5020
|
+
? t("memory.status.conflictQueue.copiedSide")
|
|
5021
|
+
: t("memory.status.conflictQueue.copySide"))),
|
|
3800
5022
|
// #177:词级 diff 可用(LCS 无损对齐成功)时用高亮视图替代纯文本。
|
|
3801
5023
|
// 侧别过滤(#295 评审修正):A 侧渲染 same+del(它被删的部分高亮),
|
|
3802
5024
|
// B 侧渲染 same+ins(它新增的部分高亮)——每列忠实于自己的原文。
|
|
3803
5025
|
// title 提示颜色语义;diff 不可用时保留原 snippet。
|
|
5026
|
+
// 展开态:diff 去掉 4 行钳制、纯文本不再截 140 字(改走 pre-wrap 滚动区)。
|
|
3804
5027
|
ops
|
|
3805
|
-
? h("div", {
|
|
5028
|
+
? h("div", {
|
|
5029
|
+
className: open ? "mneme-conflict-diff mneme-conflict-diff--full" : "mneme-conflict-diff",
|
|
5030
|
+
title: t("memory.status.conflictQueue.diffTitle")
|
|
5031
|
+
},
|
|
3806
5032
|
ops.map((o, k) => {
|
|
3807
5033
|
if (o.kind === "same") return o.text;
|
|
3808
5034
|
if (o.kind === "del" && diffSide === "a") {
|
|
@@ -3813,7 +5039,8 @@ window.__ModuleLoader__.load({
|
|
|
3813
5039
|
}
|
|
3814
5040
|
return null; // 对侧的编辑片段不出现在本列
|
|
3815
5041
|
}))
|
|
3816
|
-
: h("div", { className: "mneme-conflict-
|
|
5042
|
+
: h("div", { className: open ? "mneme-conflict-text" : "mneme-conflict-snippet" },
|
|
5043
|
+
open ? (s.content || "") : (s.content || "").slice(0, 140)),
|
|
3817
5044
|
// #177:预裁决阶段两侧都还活着——「已归档」徽章换成「冻结中」,
|
|
3818
5045
|
// 原注销记说明(applyHint)挪进 tooltip,不再整段占一行动态区。
|
|
3819
5046
|
h("span", {
|
|
@@ -3846,6 +5073,7 @@ window.__ModuleLoader__.load({
|
|
|
3846
5073
|
h("div", { className: "mneme-conflict-empty-body" }, t("memory.status.conflictQueue.emptyBody")))),
|
|
3847
5074
|
items.map((it) => {
|
|
3848
5075
|
const sim = similarityOf(it.reason);
|
|
5076
|
+
const open = openItems[it.id] === true;
|
|
3849
5077
|
const aText = (it.memory_a && it.memory_a.content) || "";
|
|
3850
5078
|
const bText = (it.memory_b && it.memory_b.content) || "";
|
|
3851
5079
|
// diff 与相似度条互补:reason 给不出数字时才跑 LCS(两者表达同一信息)。
|
|
@@ -3860,11 +5088,16 @@ window.__ModuleLoader__.load({
|
|
|
3860
5088
|
h("span", { className: "mneme-conflict-simfill", style: { width: `${Math.round(sim * 100)}%` } })),
|
|
3861
5089
|
h("span", null, `${Math.round(sim * 100)}%`)),
|
|
3862
5090
|
h("div", { className: "mneme-conflict-pair" },
|
|
3863
|
-
side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a"),
|
|
3864
|
-
side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b")),
|
|
5091
|
+
side(it.memory_a, t("memory.status.conflictQueue.sideA"), "mneme-conflict-side--a", diff, "a", open, it.id),
|
|
5092
|
+
side(it.memory_b, t("memory.status.conflictQueue.sideB"), "mneme-conflict-side--b", diff, "b", open, it.id)),
|
|
3865
5093
|
h("div", { className: "mneme-conflict-actions" },
|
|
3866
5094
|
h("span", { className: "mneme-conflict-hint", title: t("memory.status.conflictQueue.applyHintTitle") },
|
|
3867
5095
|
t("memory.status.conflictQueue.applyHint")),
|
|
5096
|
+
h("button", {
|
|
5097
|
+
className: "mneme-footbtn",
|
|
5098
|
+
"aria-expanded": open,
|
|
5099
|
+
onClick: () => toggleItem(it.id)
|
|
5100
|
+
}, t(open ? "memory.status.conflictQueue.collapse" : "memory.status.conflictQueue.expand")),
|
|
3868
5101
|
h("button", { className: "mneme-conflict-primary", disabled: busy, "aria-label": `${t("memory.status.conflictQueue.keepA")}: ${(it.memory_a && it.memory_a.title) || ""}`, onClick: () => resolve(it.id, "a") }, t("memory.status.conflictQueue.keepA")),
|
|
3869
5102
|
h("button", { className: "mneme-conflict-primary", disabled: busy, "aria-label": `${t("memory.status.conflictQueue.keepB")}: ${(it.memory_b && it.memory_b.title) || ""}`, onClick: () => resolve(it.id, "b") }, t("memory.status.conflictQueue.keepB")),
|
|
3870
5103
|
h("button", { className: "mneme-footbtn", disabled: busy, onClick: () => resolve(it.id, null) }, t("memory.status.conflictQueue.markReviewed"))));
|
|
@@ -4152,24 +5385,41 @@ window.__ModuleLoader__.load({
|
|
|
4152
5385
|
queueRef.current.focus({ preventScroll: true });
|
|
4153
5386
|
if (queueRef.current.scrollIntoView) queueRef.current.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
4154
5387
|
};
|
|
5388
|
+
// 折叠区受控开合:收起时不挂载子卡,10 张卡各自的请求一次都不发——
|
|
5389
|
+
// 展开才是「我要看后台细节」的明确信号,首屏只承担 Token 三屏的开销。
|
|
5390
|
+
const [legacyOpen, setLegacyOpen] = useState(false);
|
|
4155
5391
|
return h("div", { className: "mneme-status" },
|
|
4156
|
-
|
|
4157
|
-
h("
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
h(
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
5392
|
+
h(TokenFlowSection, { t }),
|
|
5393
|
+
h("details", {
|
|
5394
|
+
className: "mneme-legacy",
|
|
5395
|
+
open: legacyOpen
|
|
5396
|
+
},
|
|
5397
|
+
// 开合挂在 summary 上而非 details:details 上的 onClick 会连折叠区里
|
|
5398
|
+
// 子卡片的点击一起吃掉(preventDefault 拦掉冒泡上来的激活事件),
|
|
5399
|
+
// 而 summary 是浏览器原生的开合把手,点它以外的地方不该切换(自动
|
|
5400
|
+
// 评审 #412 指出)。
|
|
5401
|
+
h("summary", { onClick: (e) => { e.preventDefault(); setLegacyOpen(!legacyOpen); } }, t("memory.status.token.detail")),
|
|
5402
|
+
legacyOpen ? h(react.Fragment, null,
|
|
5403
|
+
// 分组标题把九张卡拆成「库内一览 / 后台运转」两段扫读单元
|
|
5404
|
+
h("div", { className: "mneme-statushead" }, t("memory.status.sec.overview")),
|
|
5405
|
+
h("div", { className: "mneme-statusgrid mneme-statusgrid--overview" },
|
|
5406
|
+
h(MemoriesStatusCard, { t }),
|
|
5407
|
+
h(EntitiesStatusCard, { t }),
|
|
5408
|
+
h(VectorStatusCard, { t }),
|
|
5409
|
+
h(MirrorHealthCard, { t })
|
|
5410
|
+
),
|
|
5411
|
+
h("div", { className: "mneme-statushead" }, t("memory.status.sec.engine")),
|
|
5412
|
+
h("div", { className: "mneme-statusgrid" },
|
|
5413
|
+
h(DreamStatusCards, { t, showConflicts: false }),
|
|
5414
|
+
h(HeatStatusCard, { t }),
|
|
5415
|
+
h(RecallStatsCard, { t }),
|
|
5416
|
+
h(InjectStatusCard, { t }),
|
|
5417
|
+
h(InjectPreviewCard, { t })
|
|
5418
|
+
)
|
|
5419
|
+
) : null
|
|
4172
5420
|
),
|
|
5421
|
+
h("div", { className: "mneme-statushead" }, t("memory.status.sec.maintenance")),
|
|
5422
|
+
h("div", { className: "mneme-statusgrid" }, h(MaintenanceCard, { t })),
|
|
4173
5423
|
h("div", { ref: queueRef }, h(ConflictsQueue, { t })),
|
|
4174
5424
|
h(WorkbenchSection, { t, onBrowse })
|
|
4175
5425
|
);
|
|
@@ -4377,7 +5627,7 @@ window.__ModuleLoader__.load({
|
|
|
4377
5627
|
? h(react.Fragment, null,
|
|
4378
5628
|
confirmWiden
|
|
4379
5629
|
? h(react.Fragment, null,
|
|
4380
|
-
h("span", { style: { fontSize: 12, color: "var(--
|
|
5630
|
+
h("span", { style: { fontSize: 12, color: "var(--mneme-warn)" } },
|
|
4381
5631
|
t("memory.explorer.detail.scopeWidenHint")),
|
|
4382
5632
|
h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: busy, onClick: save },
|
|
4383
5633
|
t("memory.explorer.detail.scopeWidenConfirm")),
|
|
@@ -4400,7 +5650,7 @@ window.__ModuleLoader__.load({
|
|
|
4400
5650
|
}
|
|
4401
5651
|
}, t("memory.explorer.detail.cancel")),
|
|
4402
5652
|
savedTick && h("span", { className: "mneme-saved" }, t("memory.explorer.detail.saved")),
|
|
4403
|
-
saveError && h("span", { style: { fontSize: 12, color: "var(--
|
|
5653
|
+
saveError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, t("memory.explorer.detail.archiveFailed"))
|
|
4404
5654
|
)
|
|
4405
5655
|
: h(react.Fragment, null,
|
|
4406
5656
|
h("button", { type: "button", className: "mneme-btn", onClick: () => setEditing(true) }, t("memory.explorer.detail.edit")),
|
|
@@ -4428,7 +5678,7 @@ window.__ModuleLoader__.load({
|
|
|
4428
5678
|
)
|
|
4429
5679
|
: h("button", { type: "button", className: "mneme-btn mneme-btndanger", onClick: () => setConfirmDelete(true) },
|
|
4430
5680
|
t("memory.explorer.delete")),
|
|
4431
|
-
deleteError && h("span", { style: { fontSize: 12, color: "var(--
|
|
5681
|
+
deleteError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } },
|
|
4432
5682
|
t("memory.explorer.deleteFailed"))
|
|
4433
5683
|
)
|
|
4434
5684
|
)
|
|
@@ -4483,7 +5733,7 @@ window.__ModuleLoader__.load({
|
|
|
4483
5733
|
onChange: (e) => { setFile((e.target.files && e.target.files[0]) || null); setErr(""); }
|
|
4484
5734
|
})
|
|
4485
5735
|
),
|
|
4486
|
-
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--
|
|
5736
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
4487
5737
|
msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
|
|
4488
5738
|
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
4489
5739
|
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
|
|
@@ -4493,6 +5743,117 @@ window.__ModuleLoader__.load({
|
|
|
4493
5743
|
);
|
|
4494
5744
|
}
|
|
4495
5745
|
|
|
5746
|
+
// #386 第 5 条:面板手动新增一条记忆。字段与 memory_save 工具对齐
|
|
5747
|
+
//(type/title/content/importance/tags),但**不放 scope/sensitivity**:归属的
|
|
5748
|
+
// 显式声明走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖
|
|
5749
|
+
// 章会绕开那道确认。后端的去重键仍是 (type, title, scope, sensitivity),所以
|
|
5750
|
+
// 同类型同标题会并入既有行——服务端回的 action 决定提示语说「已新建」还是
|
|
5751
|
+
// 「已并入」,两者对用户是不同的事实,不能都报「已保存」。
|
|
5752
|
+
function CreateDialog({ t, onClose, onCreated }) {
|
|
5753
|
+
const [type, setType] = useState("preference");
|
|
5754
|
+
const [title, setTitle] = useState("");
|
|
5755
|
+
const [content, setContent] = useState("");
|
|
5756
|
+
const [importance, setImportance] = useState(3);
|
|
5757
|
+
const [tags, setTags] = useState("");
|
|
5758
|
+
const [busy, setBusy] = useState(false);
|
|
5759
|
+
const [msg, setMsg] = useState("");
|
|
5760
|
+
const [err, setErr] = useState("");
|
|
5761
|
+
const submit = () => {
|
|
5762
|
+
// 前端先拦一次空标题/空正文:后端也会 400,但往返一趟只为报「标题不能
|
|
5763
|
+
// 为空」不值当。服务端校验仍是权威——它同时管着类型白名单与去重。
|
|
5764
|
+
if (!title.trim()) { setErr(t("memory.explorer.newNoTitle")); return; }
|
|
5765
|
+
if (!content.trim()) { setErr(t("memory.explorer.newNoContent")); return; }
|
|
5766
|
+
setBusy(true);
|
|
5767
|
+
setErr("");
|
|
5768
|
+
apiFetch("/api/dsh-mneme/create", {
|
|
5769
|
+
method: "POST",
|
|
5770
|
+
headers: { "Content-Type": "application/json" },
|
|
5771
|
+
body: JSON.stringify({
|
|
5772
|
+
type,
|
|
5773
|
+
title: title.trim(),
|
|
5774
|
+
content: content.trim(),
|
|
5775
|
+
importance,
|
|
5776
|
+
tags: tags.split(",").map((s) => s.trim()).filter(Boolean)
|
|
5777
|
+
})
|
|
5778
|
+
})
|
|
5779
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
5780
|
+
.then((d) => {
|
|
5781
|
+
setMsg(d && d.action === "merged" ? t("memory.explorer.newMerged") : t("memory.explorer.newCreated"));
|
|
5782
|
+
// 成功后**不**重置 busy。这 900ms 是留给提示的阅读时间,而「新建」按钮此刻
|
|
5783
|
+
// 若重新可点,第二次同 type+title 会落进同一行,service 走 appendContent 把
|
|
5784
|
+
// 正文再追加一遍,提示还会从「已新建」翻成「已并入」——一次误触就写坏一条
|
|
5785
|
+
// 记忆。保持 busy 直到关窗,按钮全程 disabled(成功路径组件随即卸载,没有
|
|
5786
|
+
// 需要复原的状态)。
|
|
5787
|
+
// 停 900ms 让提示可读,再关窗并把新建的行选中(见 onCreated)。
|
|
5788
|
+
setTimeout(() => { onCreated(d); onClose(); }, 900);
|
|
5789
|
+
})
|
|
5790
|
+
.catch(() => { setBusy(false); setErr(t("memory.explorer.newFailed")); });
|
|
5791
|
+
};
|
|
5792
|
+
return h("div", { className: "mneme-impdialog", role: "region", "aria-label": t("memory.explorer.newTitle") },
|
|
5793
|
+
h("div", { className: "mneme-set-title" }, t("memory.explorer.newTitle")),
|
|
5794
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.newHint")),
|
|
5795
|
+
h("div", { className: "mneme-improw" },
|
|
5796
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.newType")),
|
|
5797
|
+
h("select", { className: "mneme-select", value: type, onChange: (e) => setType(e.target.value) },
|
|
5798
|
+
// document 刻意不在列:它是 agent 注册长文档的指针行,铸造口唯一
|
|
5799
|
+
//(registerDocument,#230),后端路由也会 400。列出来只会让人点了
|
|
5800
|
+
// 才被拒。
|
|
5801
|
+
["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"].map((k) =>
|
|
5802
|
+
h("option", { key: k, value: k }, typeLabel(t, k))))
|
|
5803
|
+
),
|
|
5804
|
+
h("div", { className: "mneme-improw" },
|
|
5805
|
+
h("input", {
|
|
5806
|
+
type: "text",
|
|
5807
|
+
className: "mneme-set-input",
|
|
5808
|
+
value: title,
|
|
5809
|
+
placeholder: t("memory.explorer.newTitlePlaceholder"),
|
|
5810
|
+
"aria-label": t("memory.explorer.newTitlePlaceholder"),
|
|
5811
|
+
onChange: (e) => { setTitle(e.target.value); setErr(""); }
|
|
5812
|
+
})
|
|
5813
|
+
),
|
|
5814
|
+
h("div", { className: "mneme-improw" },
|
|
5815
|
+
h("textarea", {
|
|
5816
|
+
className: "mneme-set-input mneme-set-input--area",
|
|
5817
|
+
value: content,
|
|
5818
|
+
placeholder: t("memory.explorer.newContentPlaceholder"),
|
|
5819
|
+
"aria-label": t("memory.explorer.newContentPlaceholder"),
|
|
5820
|
+
rows: 4,
|
|
5821
|
+
onChange: (e) => { setContent(e.target.value); setErr(""); }
|
|
5822
|
+
})
|
|
5823
|
+
),
|
|
5824
|
+
h("div", { className: "mneme-improw" },
|
|
5825
|
+
h("input", {
|
|
5826
|
+
type: "text",
|
|
5827
|
+
className: "mneme-set-input",
|
|
5828
|
+
value: tags,
|
|
5829
|
+
placeholder: t("memory.explorer.newTagsPlaceholder"),
|
|
5830
|
+
"aria-label": t("memory.explorer.newTagsPlaceholder"),
|
|
5831
|
+
onChange: (e) => setTags(e.target.value)
|
|
5832
|
+
})
|
|
5833
|
+
),
|
|
5834
|
+
h("div", { className: "mneme-improw" },
|
|
5835
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.importance")),
|
|
5836
|
+
h("div", { className: "mneme-staredit", role: "radiogroup", "aria-label": t("memory.explorer.detail.editImportance") },
|
|
5837
|
+
[1, 2, 3, 4, 5].map((n) => h("button", {
|
|
5838
|
+
key: n,
|
|
5839
|
+
type: "button",
|
|
5840
|
+
role: "radio",
|
|
5841
|
+
"aria-checked": String(importance === n),
|
|
5842
|
+
"aria-label": t("memory.explorer.detail.starLabel").replace("{n}", String(n)),
|
|
5843
|
+
className: importance >= n ? "mneme-starbtn mneme-starbtn--on" : "mneme-starbtn",
|
|
5844
|
+
onClick: () => setImportance(n)
|
|
5845
|
+
}, h(StarGlyph, { size: 15, filled: importance >= n }))))
|
|
5846
|
+
),
|
|
5847
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
5848
|
+
msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
|
|
5849
|
+
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
5850
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
|
|
5851
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: submit },
|
|
5852
|
+
busy && !msg ? t("memory.explorer.newSaving") : t("memory.explorer.newConfirm"))
|
|
5853
|
+
)
|
|
5854
|
+
);
|
|
5855
|
+
}
|
|
5856
|
+
|
|
4496
5857
|
function MemoryExplorer({ t }) {
|
|
4497
5858
|
const [view, setView] = useState("memory"); // memory | entity | status | settings
|
|
4498
5859
|
const [items, setItems] = useState([]); // loaded browse pages, chrono desc
|
|
@@ -4528,6 +5889,7 @@ window.__ModuleLoader__.load({
|
|
|
4528
5889
|
const [pendingIds, setPendingIds] = useState(() => new Set());
|
|
4529
5890
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
4530
5891
|
const [importOpen, setImportOpen] = useState(false);
|
|
5892
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
4531
5893
|
const itemRefs = useRef(new Map());
|
|
4532
5894
|
const moreRef = useRef(null);
|
|
4533
5895
|
|
|
@@ -4877,9 +6239,11 @@ window.__ModuleLoader__.load({
|
|
|
4877
6239
|
"aria-label": t("memory.explorer.more"),
|
|
4878
6240
|
"aria-expanded": String(menuOpen),
|
|
4879
6241
|
title: t("memory.explorer.more"),
|
|
4880
|
-
onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); }
|
|
6242
|
+
onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); setCreateOpen(false); }
|
|
4881
6243
|
}, "⋯"),
|
|
4882
6244
|
menuOpen && h("div", { className: "mneme-menu", role: "menu" },
|
|
6245
|
+
h("button", { type: "button", role: "menuitem", onClick: () => { setCreateOpen(true); setMenuOpen(false); } },
|
|
6246
|
+
t("memory.explorer.newMemory")),
|
|
4883
6247
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("json"); } },
|
|
4884
6248
|
t("memory.explorer.exportJson")),
|
|
4885
6249
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("markdown"); } },
|
|
@@ -4891,6 +6255,13 @@ window.__ModuleLoader__.load({
|
|
|
4891
6255
|
t,
|
|
4892
6256
|
onClose: () => setImportOpen(false),
|
|
4893
6257
|
onImported: () => setReloadKey((k) => k + 1)
|
|
6258
|
+
}),
|
|
6259
|
+
createOpen && h(CreateDialog, {
|
|
6260
|
+
t,
|
|
6261
|
+
onClose: () => setCreateOpen(false),
|
|
6262
|
+
// 新建后既刷新列表也选中新行:并入既有条目时 action 是 merged,
|
|
6263
|
+
// 返回的 id 就是被并入那一行——两种情况都该把它滚进视野。
|
|
6264
|
+
onCreated: (d) => { setReloadKey((k) => k + 1); if (d && d.id) setSelectedId(d.id); }
|
|
4894
6265
|
})
|
|
4895
6266
|
)
|
|
4896
6267
|
),
|
|
@@ -5365,6 +6736,1045 @@ window.__ModuleLoader__.load({
|
|
|
5365
6736
|
), host);
|
|
5366
6737
|
}
|
|
5367
6738
|
|
|
6739
|
+
// ---------------------------------------------------------------------
|
|
6740
|
+
// v0.9.0 M.O.(beta,只读)
|
|
6741
|
+
// 定性:trace viewer 不是编辑画布——节点=步骤、边=依赖、一次运行=一条
|
|
6742
|
+
// 被高亮走过的路径。本版数据全部是 §4 mneme.pipeline/v1 形状的假数据,
|
|
6743
|
+
// 服务端管线落地前先验证形态与交互。
|
|
6744
|
+
// ---------------------------------------------------------------------
|
|
6745
|
+
const PIPELINE_ACTIONS_TAB_ID = "dsh-mneme:actions";
|
|
6746
|
+
const PIPELINE_ACTIONS_TAB_KIND = "mneme-actions";
|
|
6747
|
+
|
|
6748
|
+
// 节点状态四值,refuted 是全篇核心信号(最扎眼)。
|
|
6749
|
+
const PIPELINE_STATES = ["candidate", "trusted", "stale", "refuted"];
|
|
6750
|
+
|
|
6751
|
+
// 数据面 = **项目里**的工作流(服务端 /api/dsh-mneme/pipeline 投影成 mneme.pipeline/v1)。
|
|
6752
|
+
// 项目从哪来:只有客户端拿得到会话的工作目录,所以这里解析出 cwd、当 ?project= 带上;
|
|
6753
|
+
// 服务端没有「当前项目」这个概念,也不许替谁猜一个。
|
|
6754
|
+
// 为什么四个座位共用缓存、且缓存**按项目 + 按工作流**分桶:四个座位读的必须是同一份
|
|
6755
|
+
// ——「会话头说有 3 条、右栏说有 5 条」这种分家没人查得出来;同理,同一个进程里开着
|
|
6756
|
+
// 两个不同项目的会话时,不分桶就会把 A 项目的清单端给 B 项目看,而画面看不出错。
|
|
6757
|
+
const pipelineDocs = new Map();
|
|
6758
|
+
const pipelinePendingByKey = new Map();
|
|
6759
|
+
function pipelineKey(project, id) { return (project || "") + "\u0000" + (id || ""); }
|
|
6760
|
+
function loadPipeline(project, id) {
|
|
6761
|
+
const key = pipelineKey(project, id);
|
|
6762
|
+
if (!pipelinePendingByKey.has(key)) {
|
|
6763
|
+
const params = [];
|
|
6764
|
+
if (project) params.push("project=" + encodeURIComponent(project));
|
|
6765
|
+
if (id) params.push("id=" + encodeURIComponent(id));
|
|
6766
|
+
const url = "/api/dsh-mneme/pipeline" + (params.length ? "?" + params.join("&") : "");
|
|
6767
|
+
pipelinePendingByKey.set(key, apiFetch(url)
|
|
6768
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
6769
|
+
.catch(() => null)
|
|
6770
|
+
.then((doc) => { pipelineDocs.set(key, doc); return doc; }));
|
|
6771
|
+
}
|
|
6772
|
+
return pipelinePendingByKey.get(key);
|
|
6773
|
+
}
|
|
6774
|
+
// undefined = 还在读;null = 读不出来;对象 = 拿到了(其中 pipeline 为 null 表示
|
|
6775
|
+
// 这个项目里还没有工作流——那是空态,不是错误,别把两者画成同一张脸)。
|
|
6776
|
+
// id 为空 = 取清单(服务端会顺带给出第一条的完整投影,够结算卡用)。
|
|
6777
|
+
function usePipeline(project, id) {
|
|
6778
|
+
const key = pipelineKey(project, id);
|
|
6779
|
+
const [doc, setDoc] = useState(pipelineDocs.has(key) ? pipelineDocs.get(key) : undefined);
|
|
6780
|
+
useEffect(() => {
|
|
6781
|
+
let alive = true;
|
|
6782
|
+
loadPipeline(project, id).then((d) => { if (alive) setDoc(d); });
|
|
6783
|
+
return () => { alive = false; };
|
|
6784
|
+
}, [key]);
|
|
6785
|
+
return doc;
|
|
6786
|
+
}
|
|
6787
|
+
// 会话工作目录。两条来源依次试,各自兜自己的异常——一条抛了不能把另一条也带走;
|
|
6788
|
+
// 两处都试不到就**返回 null**(面板退到全局层,并如实标「项目未知」),绝不退回进程
|
|
6789
|
+
// 目录之类的东西——那会把「读不到」变成「读到别处」,而后者在界面上看不出来,是最难
|
|
6790
|
+
// 查的一类错。
|
|
6791
|
+
// 首选会话列表快照:byId[sessionId].cwd 是宿主给客户端的公开形状(dsh-context 用的
|
|
6792
|
+
// 就是这条);sessions.get() 是宿主侧写法,客户端不一定有,所以放第二位。
|
|
6793
|
+
function sessionProject(ctx, sessionId) {
|
|
6794
|
+
if (!sessionId || !ctx || typeof ctx.get !== "function") return null;
|
|
6795
|
+
const warn = (err) => console.warn("[dsh-mneme] session cwd lookup failed:", err);
|
|
6796
|
+
const pick = (row) => {
|
|
6797
|
+
if (!row) return null;
|
|
6798
|
+
const cwd = (row.header && row.header.cwd) || row.cwd;
|
|
6799
|
+
return typeof cwd === "string" && cwd ? cwd : null;
|
|
6800
|
+
};
|
|
6801
|
+
let sessions = null;
|
|
6802
|
+
try { sessions = ctx.get("sessions"); } catch (err) { warn(err); }
|
|
6803
|
+
if (sessions) {
|
|
6804
|
+
try {
|
|
6805
|
+
const snap = sessions.list && typeof sessions.list.getSnapshot === "function"
|
|
6806
|
+
? sessions.list.getSnapshot()
|
|
6807
|
+
: null;
|
|
6808
|
+
const hit = pick(snap && snap.byId ? snap.byId[sessionId] : null);
|
|
6809
|
+
if (hit) return hit;
|
|
6810
|
+
} catch (err) { warn(err); }
|
|
6811
|
+
try {
|
|
6812
|
+
if (typeof sessions.get === "function") {
|
|
6813
|
+
const hit = pick(sessions.get(sessionId));
|
|
6814
|
+
if (hit) return hit;
|
|
6815
|
+
}
|
|
6816
|
+
} catch (err) { warn(err); }
|
|
6817
|
+
}
|
|
6818
|
+
return null;
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6821
|
+
// DAG 分层布局:Kahn 最长路分层(层数 = 到该节点最长边数),
|
|
6822
|
+
// 防环——正常分层算完后如果还有节点没落位(说明有环),
|
|
6823
|
+
// 按输入顺序塞进最后一层,绝不死循环。节点数少,几十行够用。
|
|
6824
|
+
function layoutDag(nodes, edges) {
|
|
6825
|
+
const byId = {};
|
|
6826
|
+
for (const n of nodes) byId[n.id] = n;
|
|
6827
|
+
const indeg = {};
|
|
6828
|
+
const out = {};
|
|
6829
|
+
for (const n of nodes) { indeg[n.id] = 0; out[n.id] = []; }
|
|
6830
|
+
for (const e of edges) {
|
|
6831
|
+
if (!byId[e.from] || !byId[e.to] || e.from === e.to) continue;
|
|
6832
|
+
out[e.from].push(e.to);
|
|
6833
|
+
indeg[e.to] += 1;
|
|
6834
|
+
}
|
|
6835
|
+
const layer = {};
|
|
6836
|
+
const queue = nodes.filter((n) => indeg[n.id] === 0).map((n) => n.id);
|
|
6837
|
+
const remaining = {};
|
|
6838
|
+
for (const n of nodes) remaining[n.id] = indeg[n.id];
|
|
6839
|
+
while (queue.length) {
|
|
6840
|
+
const id = queue.shift();
|
|
6841
|
+
for (const to of out[id]) {
|
|
6842
|
+
layer[to] = Math.max(layer[to] || 0, (layer[id] || 0) + 1);
|
|
6843
|
+
remaining[to] -= 1;
|
|
6844
|
+
if (remaining[to] === 0) queue.push(to);
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
const maxLayer = nodes.reduce((m, n) => Math.max(m, layer[n.id] || 0), 0);
|
|
6848
|
+
const leftovers = nodes.filter((n) => remaining[n.id] > 0);
|
|
6849
|
+
for (const n of leftovers) layer[n.id] = maxLayer + 1; // 环兜底
|
|
6850
|
+
const layers = [];
|
|
6851
|
+
for (const n of nodes) {
|
|
6852
|
+
const L = layer[n.id] || 0;
|
|
6853
|
+
(layers[L] = layers[L] || []).push(n.id);
|
|
6854
|
+
}
|
|
6855
|
+
return { layer, layers: layers.map((l) => l || []) };
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
// 阶段带布局(2026-10-16):layoutDag 只按 DAG 深度分层、不读 phase,把 5 段
|
|
6859
|
+
// 19 步排成 18 层的单列长链(固有 354×1902px),第一屏既看不出「分几段」也
|
|
6860
|
+
// 读不出「谁在做」。这里按 step.phase 分组(保持作者书写顺序——phase 是人手
|
|
6861
|
+
// 写的分类词,不能按字母重排)、段内左到右每行两列换行:画布固有宽度落回
|
|
6862
|
+
// 354px(右栏 400px 不缩放就能读),高度从 1902 腰斩到 ~836。
|
|
6863
|
+
// 纯函数、不碰 DOM:探针 .scratch/mo-ux-probe-layout.mjs 会把它抠出来真跑,
|
|
6864
|
+
// 改签名或引用外部变量都会让那条可重跑核算失明。
|
|
6865
|
+
function layoutPipelineBands(nodes, edges, C) {
|
|
6866
|
+
const NW = (C && C.NW) || 150;
|
|
6867
|
+
const NH = (C && C.NH) || 54;
|
|
6868
|
+
const GX = (C && C.GX) || 30;
|
|
6869
|
+
const PAD = (C && C.PAD) || 12;
|
|
6870
|
+
const COLS = 2, ROW_GAP = 14, BAND_HEAD = 26, BAND_GAP = 18;
|
|
6871
|
+
const order = [];
|
|
6872
|
+
const byPhase = {};
|
|
6873
|
+
for (const n of nodes) {
|
|
6874
|
+
const ph = n.phase || "";
|
|
6875
|
+
if (!byPhase[ph]) { byPhase[ph] = []; order.push(ph); }
|
|
6876
|
+
byPhase[ph].push(n.id);
|
|
6877
|
+
}
|
|
6878
|
+
const pos = {};
|
|
6879
|
+
const bands = [];
|
|
6880
|
+
const bandRects = [];
|
|
6881
|
+
const W = PAD * 2 + COLS * NW + (COLS - 1) * GX;
|
|
6882
|
+
let y = PAD;
|
|
6883
|
+
for (const ph of order) {
|
|
6884
|
+
const ids = byPhase[ph];
|
|
6885
|
+
const rows = Math.ceil(ids.length / COLS);
|
|
6886
|
+
const top = y;
|
|
6887
|
+
y += BAND_HEAD;
|
|
6888
|
+
ids.forEach((id, i) => {
|
|
6889
|
+
const r = Math.floor(i / COLS), c = i % COLS;
|
|
6890
|
+
pos[id] = { x: PAD + c * (NW + GX), y: y + r * (NH + ROW_GAP) };
|
|
6891
|
+
});
|
|
6892
|
+
y += rows * NH + (rows - 1) * ROW_GAP;
|
|
6893
|
+
bands.push({ name: ph, count: ids.length, top, bottom: y });
|
|
6894
|
+
bandRects.push({ name: ph, count: ids.length, x: PAD - 6, y: top, w: W - 2 * (PAD - 6), h: y - top });
|
|
6895
|
+
y += BAND_GAP;
|
|
6896
|
+
}
|
|
6897
|
+
const H = y - BAND_GAP + PAD;
|
|
6898
|
+
return { pos, W, H, bands, bandRects };
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
// 标题两行拆分:pipeClip 一刀截 11 字把「定版本号与发布范围」截成「…发布范…」。
|
|
6902
|
+
// 节点加高到 54px 后装得下两行——标本 19 个标题里 7 个超 11 字、最长 22 字,
|
|
6903
|
+
// 恰好两行放全;超长兜底仍留省略号。
|
|
6904
|
+
function pipeTitleLines(s, n) {
|
|
6905
|
+
const str = String(s || "");
|
|
6906
|
+
if (str.length <= n) return [str];
|
|
6907
|
+
const head = str.slice(0, n);
|
|
6908
|
+
const rest = str.slice(n);
|
|
6909
|
+
return [head, rest.length > n ? rest.slice(0, n - 1) + "…" : rest];
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6912
|
+
function pipeFmtWhen(iso) {
|
|
6913
|
+
if (!iso) return "";
|
|
6914
|
+
const d = new Date(iso);
|
|
6915
|
+
if (isNaN(d.getTime())) return "";
|
|
6916
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
6917
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
6918
|
+
}
|
|
6919
|
+
function pipeClip(s, n) {
|
|
6920
|
+
const str = String(s || "");
|
|
6921
|
+
return str.length > n ? str.slice(0, n - 1) + "…" : str;
|
|
6922
|
+
}
|
|
6923
|
+
function pipeDaysAgo(iso) {
|
|
6924
|
+
if (!iso) return null;
|
|
6925
|
+
const d = new Date(iso);
|
|
6926
|
+
if (isNaN(d.getTime())) return null;
|
|
6927
|
+
return Math.max(0, Math.floor((Date.now() - d.getTime()) / 86400000));
|
|
6928
|
+
}
|
|
6929
|
+
|
|
6930
|
+
// 运行档位:一次运行一个芯片;失败/被推翻的运行照常列出(留痕)。
|
|
6931
|
+
function PipelineRunChips(props) {
|
|
6932
|
+
const { t, runs, selectedRunId, onSelectRun } = props;
|
|
6933
|
+
return h("div", { className: "mneme-pipe-runs", role: "group", "aria-label": t("memory.actions.runs") },
|
|
6934
|
+
h("span", { className: "mneme-pipe-scope" }, t("memory.actions.runs")),
|
|
6935
|
+
(runs || []).map((r, i) => h("button", {
|
|
6936
|
+
key: r.id || i,
|
|
6937
|
+
type: "button",
|
|
6938
|
+
className: "mneme-pipe-run" + (r.id === selectedRunId ? " mneme-on" : ""),
|
|
6939
|
+
onClick: () => onSelectRun && onSelectRun(r.id),
|
|
6940
|
+
title: t("memory.actions.run." + (r.outcome || "ok"))
|
|
6941
|
+
},
|
|
6942
|
+
h("span", { className: "mneme-pipe-runDot mneme-pipe-runDot--" + (r.outcome || "ok"), "aria-hidden": "true" }),
|
|
6943
|
+
t("memory.actions.runOf").replace("{n}", String(i + 1)) + " · " + pipeFmtWhen(r.at)
|
|
6944
|
+
))
|
|
6945
|
+
);
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6948
|
+
// 宽态 DAG:按 phase 分阶段带的 SVG,一次运行 = 一条高亮路径(trace viewer 读法)。
|
|
6949
|
+
// 分组与几何来自 layoutPipelineBands(纯函数,探针真跑它),这里只管画。
|
|
6950
|
+
function PipelineGraphSvg(props) {
|
|
6951
|
+
const { t, data, run, selectedNodeId, onSelectNode } = props;
|
|
6952
|
+
const nodes = data.nodes || [];
|
|
6953
|
+
const edges = data.edges || [];
|
|
6954
|
+
const pathSet = {};
|
|
6955
|
+
const edgeState = {};
|
|
6956
|
+
if (run && Array.isArray(run.path)) {
|
|
6957
|
+
for (const id of run.path) pathSet[id] = true;
|
|
6958
|
+
for (let i = 0; i + 1 < run.path.length; i++) {
|
|
6959
|
+
const a = run.path[i];
|
|
6960
|
+
const b = run.path[i + 1];
|
|
6961
|
+
const isLast = i + 2 === run.path.length;
|
|
6962
|
+
edgeState[a + ">" + b] = run.outcome === "failed" && isLast ? "fail" : "on";
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
const usedSet = {};
|
|
6966
|
+
if (run && Array.isArray(run.usedNodeIds)) {
|
|
6967
|
+
for (const id of run.usedNodeIds) usedSet[id] = true;
|
|
6968
|
+
}
|
|
6969
|
+
const NW = 150, NH = 54, GX = 30, GY = 66, PAD = 12;
|
|
6970
|
+
const laid = layoutPipelineBands(nodes, edges, { NW, NH, GX, GY, PAD });
|
|
6971
|
+
const pos = laid.pos;
|
|
6972
|
+
const W = laid.W;
|
|
6973
|
+
const Hh = laid.H;
|
|
6974
|
+
const KIND_RX = { step: 8, gate: 4, branch: 16, artifact: 21 };
|
|
6975
|
+
return h("svg", {
|
|
6976
|
+
className: "mneme-pipe-svg",
|
|
6977
|
+
viewBox: `0 0 ${W} ${Hh}`,
|
|
6978
|
+
width: W,
|
|
6979
|
+
height: Hh,
|
|
6980
|
+
role: "img",
|
|
6981
|
+
"aria-label": t("memory.actions.title")
|
|
6982
|
+
},
|
|
6983
|
+
// 阶段带:段底 + 段名与该段步数。分段信息画在节点之下、边之下——
|
|
6984
|
+
// 它是背景不是主角,但必须先上屏,第一屏才读得出「分几段」。
|
|
6985
|
+
laid.bandRects.map((b, i) => h("g", { key: "band" + i },
|
|
6986
|
+
h("rect", {
|
|
6987
|
+
className: "mneme-pipe-bandbg",
|
|
6988
|
+
x: b.x, y: b.y, width: b.w, height: b.h, rx: 10
|
|
6989
|
+
}),
|
|
6990
|
+
i > 0 ? h("line", {
|
|
6991
|
+
className: "mneme-pipe-bandline",
|
|
6992
|
+
x1: PAD, x2: W - PAD, y1: b.y - 9, y2: b.y - 9
|
|
6993
|
+
}) : null,
|
|
6994
|
+
h("text", {
|
|
6995
|
+
className: "mneme-pipe-bandT",
|
|
6996
|
+
x: PAD + 4, y: b.y + 17
|
|
6997
|
+
}, (b.name || "") + " · " + t("memory.actions.band.count").replace("{n}", String(b.count)))
|
|
6998
|
+
)),
|
|
6999
|
+
edges.map((e) => {
|
|
7000
|
+
const a = pos[e.from];
|
|
7001
|
+
const b = pos[e.to];
|
|
7002
|
+
if (!a || !b) return null;
|
|
7003
|
+
const st = edgeState[e.from + ">" + e.to];
|
|
7004
|
+
const cls = st === "fail" ? "mneme-pipe-edgeFail" : st === "on" ? "mneme-pipe-edgeOn" : "mneme-pipe-edge";
|
|
7005
|
+
// 同行相邻(蛇形读法):右缘出、左缘进,短水平贝塞尔;
|
|
7006
|
+
// 跨行/跨段仍走「底出顶进」的竖向贝塞尔。同行旧画法会把线甩出段外
|
|
7007
|
+
// (真机截图验出),横连读起来就是「往下一段走」。
|
|
7008
|
+
let d;
|
|
7009
|
+
if (b.y === a.y && b.x > a.x) {
|
|
7010
|
+
const ym = a.y + NH / 2;
|
|
7011
|
+
d = `M ${a.x + NW} ${ym} C ${a.x + NW + GX / 2} ${ym}, ${b.x - GX / 2} ${ym}, ${b.x} ${ym}`;
|
|
7012
|
+
} else {
|
|
7013
|
+
const x1 = a.x + NW / 2, y1 = a.y + NH, x2 = b.x + NW / 2, y2 = b.y;
|
|
7014
|
+
d = `M ${x1} ${y1} C ${x1} ${y1 + 26}, ${x2} ${y2 - 26}, ${x2} ${y2}`;
|
|
7015
|
+
}
|
|
7016
|
+
return h("path", {
|
|
7017
|
+
key: e.id || (e.from + ">" + e.to),
|
|
7018
|
+
className: cls,
|
|
7019
|
+
d
|
|
7020
|
+
}, e.label ? h("title", null, e.label) : null);
|
|
7021
|
+
}),
|
|
7022
|
+
nodes.map((n) => {
|
|
7023
|
+
const p = pos[n.id];
|
|
7024
|
+
if (!p) return null;
|
|
7025
|
+
const rx = KIND_RX[n.kind] != null ? KIND_RX[n.kind] : 8;
|
|
7026
|
+
const on = !!(run && pathSet[n.id]);
|
|
7027
|
+
const gate = n.kind === "gate";
|
|
7028
|
+
const cls = "mneme-pipe-node mneme-pipe-node--" + (n.state || "candidate")
|
|
7029
|
+
+ (usedSet[n.id] ? " mneme-pipe-node--onpath" : "")
|
|
7030
|
+
+ (gate ? " mneme-pipe-node--gate" : "");
|
|
7031
|
+
const lines = pipeTitleLines(n.title, 10);
|
|
7032
|
+
// meta 行三段:状态(四态)· who(主/人/CI)· risk(R0-R3)。
|
|
7033
|
+
// who/risk 都是投影自带字段,此前一个都没上屏;risk 缺失按 R0 兜底。
|
|
7034
|
+
const meta = [
|
|
7035
|
+
t("memory.actions.state." + (n.state || "candidate")),
|
|
7036
|
+
n.who ? t("memory.actions.who." + n.who) : null,
|
|
7037
|
+
n.risk || "R0"
|
|
7038
|
+
].filter(Boolean).join(" · ");
|
|
7039
|
+
return h("g", {
|
|
7040
|
+
key: n.id,
|
|
7041
|
+
onClick: () => onSelectNode && onSelectNode(n.id),
|
|
7042
|
+
style: { cursor: "pointer" }
|
|
7043
|
+
},
|
|
7044
|
+
h("rect", {
|
|
7045
|
+
className: cls,
|
|
7046
|
+
x: p.x, y: p.y, width: NW, height: NH, rx
|
|
7047
|
+
}),
|
|
7048
|
+
// 风险色条:左缘一条竖线做冗余编码,信息本体在 meta 行的 R 文本里。
|
|
7049
|
+
h("line", {
|
|
7050
|
+
className: "mneme-pipe-risk--" + String(n.risk || "R0").toLowerCase(),
|
|
7051
|
+
x1: p.x + 3, y1: p.y + 8, x2: p.x + 3, y2: p.y + NH - 8,
|
|
7052
|
+
"stroke-width": 3, "stroke-linecap": "round"
|
|
7053
|
+
}),
|
|
7054
|
+
// 闸门角标:gate 步会「停下等批准」,与反事实圈同列放右上、圈在其上方——
|
|
7055
|
+
// 左上角压 risk 色条(真机截图验出的布局冲突)。
|
|
7056
|
+
gate ? h("text", {
|
|
7057
|
+
className: "mneme-pipe-gatetag",
|
|
7058
|
+
x: p.x + NW - 12, y: p.y + 24, textAnchor: "middle"
|
|
7059
|
+
}, t("memory.actions.gate.tag")) : null,
|
|
7060
|
+
// 反事实标记:空心圈 = 还没闸门(counterfactual 为 null),
|
|
7061
|
+
// 实心圈 = 有对照证据。做在右上角,一眼区分。
|
|
7062
|
+
h("circle", {
|
|
7063
|
+
cx: p.x + NW - 12, cy: p.y + 11, r: 4,
|
|
7064
|
+
fill: n.counterfactual ? "var(--mneme-ok)" : "none",
|
|
7065
|
+
stroke: n.counterfactual ? "var(--mneme-ok)" : "var(--mneme-graphic-dim)",
|
|
7066
|
+
"stroke-width": 1.5
|
|
7067
|
+
}, h("title", null, n.counterfactual ? t("memory.actions.gate.has") : t("memory.actions.gate.none"))),
|
|
7068
|
+
lines.length === 1
|
|
7069
|
+
? h("text", {
|
|
7070
|
+
className: "mneme-pipe-nodeT",
|
|
7071
|
+
x: p.x + NW / 2, y: p.y + 24, textAnchor: "middle"
|
|
7072
|
+
}, lines[0])
|
|
7073
|
+
: [
|
|
7074
|
+
h("text", {
|
|
7075
|
+
key: "t1", className: "mneme-pipe-nodeT",
|
|
7076
|
+
x: p.x + NW / 2, y: p.y + 18, textAnchor: "middle"
|
|
7077
|
+
}, lines[0]),
|
|
7078
|
+
h("text", {
|
|
7079
|
+
key: "t2", className: "mneme-pipe-nodeT",
|
|
7080
|
+
x: p.x + NW / 2, y: p.y + 31, textAnchor: "middle"
|
|
7081
|
+
}, lines[1])
|
|
7082
|
+
],
|
|
7083
|
+
h("text", {
|
|
7084
|
+
className: "mneme-pipe-nodeS",
|
|
7085
|
+
x: p.x + NW / 2, y: p.y + 46, textAnchor: "middle"
|
|
7086
|
+
}, meta),
|
|
7087
|
+
h("title", null, n.situation || n.title)
|
|
7088
|
+
);
|
|
7089
|
+
})
|
|
7090
|
+
);
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
// 图例:四个状态一眼分清 + 闸门有无。「被推翻」最扎眼是全篇核心信号。
|
|
7094
|
+
function PipelineLegend(props) {
|
|
7095
|
+
const { t } = props;
|
|
7096
|
+
return h("div", { className: "mneme-pipe-legend" },
|
|
7097
|
+
t("memory.actions.legend.states"),
|
|
7098
|
+
PIPELINE_STATES.map((s) => h("span", { key: s, className: "mneme-pipe-pill mneme-pipe-pill--" + s }, t("memory.actions.state." + s))),
|
|
7099
|
+
h("span", { className: "mneme-pipe-gate", "aria-hidden": "true" }),
|
|
7100
|
+
t("memory.actions.gate.none"),
|
|
7101
|
+
h("span", { className: "mneme-pipe-gate mneme-pipe-gate--has", "aria-hidden": "true" }),
|
|
7102
|
+
t("memory.actions.gate.has")
|
|
7103
|
+
);
|
|
7104
|
+
}
|
|
7105
|
+
|
|
7106
|
+
// 节点详情:情形 / 动作 / 为什么 / 反事实(有无两种形态)/ 复用 / 来源。
|
|
7107
|
+
function PipelineNodeDetail(props) {
|
|
7108
|
+
const { t, node } = props;
|
|
7109
|
+
if (!node) return null;
|
|
7110
|
+
const credit = node.credit || {};
|
|
7111
|
+
const days = pipeDaysAgo(credit.lastUsedAt);
|
|
7112
|
+
const rows = [
|
|
7113
|
+
["memory.actions.detail.situation", node.situation],
|
|
7114
|
+
["memory.actions.detail.action", node.action],
|
|
7115
|
+
["memory.actions.detail.why", node.why]
|
|
7116
|
+
].filter(([, v]) => v);
|
|
7117
|
+
return h("div", { className: "mneme-pipe-detail", role: "region", "aria-label": node.title },
|
|
7118
|
+
h("div", { className: "mneme-pipe-detailTitle" },
|
|
7119
|
+
h("span", { className: "mneme-pipe-pill mneme-pipe-pill--" + (node.state || "candidate") }, t("memory.actions.state." + (node.state || "candidate"))),
|
|
7120
|
+
" " + node.title
|
|
7121
|
+
),
|
|
7122
|
+
rows.map(([k, v]) => h("div", { key: k },
|
|
7123
|
+
h("span", { className: "mneme-pipe-dt" }, t(k) + ":"), v
|
|
7124
|
+
)),
|
|
7125
|
+
node.counterfactual
|
|
7126
|
+
? h("div", { className: "mneme-pipe-cf" },
|
|
7127
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.cf") + ":"),
|
|
7128
|
+
node.counterfactual.contrast || "")
|
|
7129
|
+
: h("div", { className: "mneme-pipe-cf mneme-pipe-cf--none" }, t("memory.actions.detail.noCf")),
|
|
7130
|
+
h("div", null,
|
|
7131
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.credit") + ":"),
|
|
7132
|
+
t("memory.actions.creditValue")
|
|
7133
|
+
.replace("{n}", String(credit.uses != null ? credit.uses : 0))
|
|
7134
|
+
.replace("{d}", days == null ? "—" : String(days))
|
|
7135
|
+
),
|
|
7136
|
+
node.source && (node.source.memoryIds || []).length
|
|
7137
|
+
? h("div", null,
|
|
7138
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.source") + ":"),
|
|
7139
|
+
(node.source.memoryIds || []).join("、"))
|
|
7140
|
+
: null
|
|
7141
|
+
);
|
|
7142
|
+
}
|
|
7143
|
+
|
|
7144
|
+
// 窄态(浮层 / better-sidebar ~500px):竖排列表——换布局,不是缩放。
|
|
7145
|
+
// 边收成左侧轨道线 + 「依赖」芯片;details 手风琴承载详情。
|
|
7146
|
+
function PipelineList(props) {
|
|
7147
|
+
const { t, data, run, selectedNodeId, onSelectNode } = props;
|
|
7148
|
+
const nodes = data.nodes || [];
|
|
7149
|
+
const edges = data.edges || [];
|
|
7150
|
+
const usedSet = {};
|
|
7151
|
+
if (run && Array.isArray(run.usedNodeIds)) for (const id of run.usedNodeIds) usedSet[id] = true;
|
|
7152
|
+
const onPath = {};
|
|
7153
|
+
if (run && Array.isArray(run.path)) for (const id of run.path) onPath[id] = true;
|
|
7154
|
+
const deps = {};
|
|
7155
|
+
for (const e of edges) (deps[e.to] = deps[e.to] || []).push(e.from);
|
|
7156
|
+
const { layers } = layoutDag(nodes, edges);
|
|
7157
|
+
const flat = [];
|
|
7158
|
+
layers.forEach((col) => col.forEach((id) => {
|
|
7159
|
+
const n = nodes.find((x) => x.id === id);
|
|
7160
|
+
if (n) flat.push(n);
|
|
7161
|
+
}));
|
|
7162
|
+
return h("div", { className: "mneme-pipe-list" },
|
|
7163
|
+
flat.map((n) => h("details", {
|
|
7164
|
+
key: n.id,
|
|
7165
|
+
className: "mneme-pipe-item",
|
|
7166
|
+
open: selectedNodeId === n.id || undefined,
|
|
7167
|
+
onToggle: (ev) => { if (ev.target && ev.target.open) onSelectNode && onSelectNode(n.id); }
|
|
7168
|
+
},
|
|
7169
|
+
h("summary", null,
|
|
7170
|
+
h("span", {
|
|
7171
|
+
className: "mneme-pipe-itemDot mneme-pipe-itemDot--" + (n.state || "candidate") +
|
|
7172
|
+
(onPath[n.id] ? " mneme-pipe-itemDot--onpath" : ""),
|
|
7173
|
+
"aria-hidden": "true"
|
|
7174
|
+
}),
|
|
7175
|
+
h("span", { className: "mneme-pipe-itemName" + (n.state === "refuted" ? " mneme-pipe-itemName--refuted" : "") }, n.title),
|
|
7176
|
+
h("span", { className: "mneme-pipe-itemMeta" },
|
|
7177
|
+
h("span", { className: "mneme-pipe-pill mneme-pipe-pill--" + (n.state || "candidate") }, t("memory.actions.state." + (n.state || "candidate"))),
|
|
7178
|
+
(deps[n.id] || []).length ? h("span", { className: "mneme-pipe-dep" }, t("memory.actions.dep") + " " + deps[n.id].join(" ")) : null,
|
|
7179
|
+
n.counterfactual ? null : h("span", { className: "mneme-pipe-dep" }, t("memory.actions.gate.none"))
|
|
7180
|
+
)
|
|
7181
|
+
),
|
|
7182
|
+
h("div", { className: "mneme-pipe-itemBody" },
|
|
7183
|
+
n.situation ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.situation") + ":"), n.situation) : null,
|
|
7184
|
+
n.action ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.action") + ":"), n.action) : null,
|
|
7185
|
+
n.why ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.why") + ":"), n.why) : null,
|
|
7186
|
+
n.counterfactual
|
|
7187
|
+
? h("div", { className: "mneme-pipe-cf" }, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.cf") + ":"), n.counterfactual.contrast || "")
|
|
7188
|
+
: h("div", { className: "mneme-pipe-cf mneme-pipe-cf--none" }, t("memory.actions.detail.noCf"))
|
|
7189
|
+
)
|
|
7190
|
+
))
|
|
7191
|
+
);
|
|
7192
|
+
}
|
|
7193
|
+
|
|
7194
|
+
// 条目清单(名字 + 规模 + 作用域):图上方的**选择器**。
|
|
7195
|
+
// 它解决的是「这是哪条工作流」——一进来就是一张图时这个问题没有落点,多条之间也没得选。
|
|
7196
|
+
// 但它不占整屏:图和它同屏,见下面的 PipelineBrowser。
|
|
7197
|
+
function PipelineWorkflowList(props) {
|
|
7198
|
+
const { t, data, activeId, onOpen } = props;
|
|
7199
|
+
const list = (data && data.available) || [];
|
|
7200
|
+
const scopeOf = (row) => (row.scope === "global"
|
|
7201
|
+
? t("memory.actions.scope.global")
|
|
7202
|
+
: t("memory.actions.scope.project").replace("{name}", row.scopeLabel || row.scope));
|
|
7203
|
+
return h("div", { className: "mneme-pipe-wflist" },
|
|
7204
|
+
h("div", { className: "mneme-pipe-wfhead" },
|
|
7205
|
+
h("span", null, t("memory.actions.list.heading")),
|
|
7206
|
+
h("span", null, t("memory.actions.list.count").replace("{n}", String(list.length)))
|
|
7207
|
+
),
|
|
7208
|
+
list.length
|
|
7209
|
+
? list.map((row) => h("button", {
|
|
7210
|
+
key: row.id,
|
|
7211
|
+
type: "button",
|
|
7212
|
+
className: "mneme-pipe-wf" + (row.id === activeId ? " mneme-on" : ""),
|
|
7213
|
+
"aria-current": row.id === activeId ? "true" : undefined,
|
|
7214
|
+
onClick: () => onOpen(row.id)
|
|
7215
|
+
},
|
|
7216
|
+
h("span", { className: "mneme-pipe-wfTop" },
|
|
7217
|
+
h("span", { className: "mneme-pipe-wfName" }, row.title || row.id),
|
|
7218
|
+
h("span", { className: "mneme-pipe-wfScope" }, scopeOf(row))
|
|
7219
|
+
),
|
|
7220
|
+
h("span", { className: "mneme-pipe-wfMeta" }, t("memory.actions.list.meta")
|
|
7221
|
+
.replace("{steps}", String(row.steps || 0))
|
|
7222
|
+
.replace("{gates}", String(row.gates || 0))
|
|
7223
|
+
.replace("{cf}", String(row.contrast || 0))),
|
|
7224
|
+
h("span", { className: "mneme-pipe-wfId" }, row.id)
|
|
7225
|
+
))
|
|
7226
|
+
// 「读得出来但一份都没有」与「读不出来」是两回事,别画成同一张脸:
|
|
7227
|
+
// 前者要告诉人文件该放哪,后者只有 loadPipeline 返回 null 时才是。
|
|
7228
|
+
: h("div", { className: "mneme-pipe-empty" },
|
|
7229
|
+
h("div", { className: "mneme-pipe-emptyTitle" }, t("memory.actions.empty.title")),
|
|
7230
|
+
t("memory.actions.empty.body")
|
|
7231
|
+
),
|
|
7232
|
+
data && data.unreadable && data.unreadable.length
|
|
7233
|
+
? h("div", { className: "mneme-pipe-broken" },
|
|
7234
|
+
t("memory.actions.broken") + " " + data.unreadable.map((u) => u.detail || u.dir).join(";"))
|
|
7235
|
+
: null
|
|
7236
|
+
);
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7239
|
+
// 清单与图**同屏**:上面清单(选择器),下面画布(主语),默认展开第一条。
|
|
7240
|
+
// 形状换过两次,两次的教训都留在这里,免得再翻回去:
|
|
7241
|
+
// 一进来就是图 —— 没有名字、没有落点,用户要的是「像 Hermes 那样一串条目」;
|
|
7242
|
+
// 清单 ⟷ 图两屏来回切 —— 条目有了,但画布藏在一次点击后面、界面上没有任何提示,
|
|
7243
|
+
// 反馈连着两次说「看不到画布和节点」。
|
|
7244
|
+
// 两个 usePipeline 都是无条件调用:hook 不能条件调用,而「默认选第一条」这件事
|
|
7245
|
+
// 恰好可以放在两个读取之间,不必为此拆组件。
|
|
7246
|
+
function PipelineBrowser(props) {
|
|
7247
|
+
const { t, project } = props;
|
|
7248
|
+
const [picked, setPicked] = useState(null);
|
|
7249
|
+
const listDoc = usePipeline(project, null);
|
|
7250
|
+
const rows = (listDoc && listDoc.available) || [];
|
|
7251
|
+
// 没手选过就跟第一条;手选之后清单再刷新也不跳回去(否则一刷新就把人的选择抹掉)。
|
|
7252
|
+
const activeId = picked || (rows.length ? rows[0].id : null);
|
|
7253
|
+
const detailDoc = usePipeline(project, activeId);
|
|
7254
|
+
return h("div", {
|
|
7255
|
+
className: "mneme-pipe",
|
|
7256
|
+
style: { height: "100%", display: "flex", flexDirection: "column", minHeight: "0" }
|
|
7257
|
+
},
|
|
7258
|
+
h(PipelineWorkflowList, { t, data: listDoc, activeId, onOpen: setPicked }),
|
|
7259
|
+
h(PipelineView, { t, data: detailDoc })
|
|
7260
|
+
);
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7263
|
+
// 画布缩放。为什么要有:重排前 19 步是一条 18 层竖链(固有 354×1902),被 width:100%
|
|
7264
|
+
// 拉到栏宽之后高度上万——只能上下滚;重排成阶段带后(固有 354×836)仍有
|
|
7265
|
+
// 「凑近看某一段」的需求。按钮管精确、滚轮管顺手。
|
|
7266
|
+
const PIPELINE_ZOOM_MIN = 0.3;
|
|
7267
|
+
const PIPELINE_ZOOM_MAX = 4;
|
|
7268
|
+
const clampZoom = (z) => Math.min(PIPELINE_ZOOM_MAX, Math.max(PIPELINE_ZOOM_MIN, z));
|
|
7269
|
+
|
|
7270
|
+
function PipelineZoomBar(props) {
|
|
7271
|
+
const { t, zoom, onZoom } = props;
|
|
7272
|
+
return h("div", { className: "mneme-pipe-zoombar" },
|
|
7273
|
+
h("button", {
|
|
7274
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7275
|
+
title: t("memory.actions.zoom.out"), "aria-label": t("memory.actions.zoom.out"),
|
|
7276
|
+
onClick: () => onZoom(zoom / 1.25)
|
|
7277
|
+
}, "−"),
|
|
7278
|
+
h("span", { className: "mneme-pipe-zoomval" },
|
|
7279
|
+
t("memory.actions.zoom.value").replace("{n}", String(Math.round(zoom * 100)))),
|
|
7280
|
+
h("button", {
|
|
7281
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7282
|
+
title: t("memory.actions.zoom.in"), "aria-label": t("memory.actions.zoom.in"),
|
|
7283
|
+
onClick: () => onZoom(zoom * 1.25)
|
|
7284
|
+
}, "+"),
|
|
7285
|
+
h("button", {
|
|
7286
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7287
|
+
title: t("memory.actions.zoom.hint"),
|
|
7288
|
+
onClick: () => onZoom(1)
|
|
7289
|
+
}, t("memory.actions.zoom.fit"))
|
|
7290
|
+
);
|
|
7291
|
+
}
|
|
7292
|
+
|
|
7293
|
+
// 主视图:宽态 SVG 图 ↔ 窄态列表按容器宽切换(640px 阈值,ResizeObserver)。
|
|
7294
|
+
// 根挂 .mneme-pipeview 拿语义色变量层。
|
|
7295
|
+
function PipelineView(props) {
|
|
7296
|
+
const { t, data } = props;
|
|
7297
|
+
const nodes = (data && data.nodes) || [];
|
|
7298
|
+
const runs = (data && data.runs) || [];
|
|
7299
|
+
const [selectedRunId, setSelectedRunId] = react.useState(runs.length ? runs[0].id : null);
|
|
7300
|
+
const [selectedNodeId, setSelectedNodeId] = react.useState(null);
|
|
7301
|
+
const rootRef = react.useRef(null);
|
|
7302
|
+
const graphRef = react.useRef(null);
|
|
7303
|
+
const [narrow, setNarrow] = react.useState(false);
|
|
7304
|
+
const [zoom, setZoom] = react.useState(1);
|
|
7305
|
+
react.useEffect(() => {
|
|
7306
|
+
const el = rootRef.current;
|
|
7307
|
+
if (!el || typeof ResizeObserver !== "function") return;
|
|
7308
|
+
const ro = new ResizeObserver((entries) => {
|
|
7309
|
+
const w = entries && entries[0] ? entries[0].contentRect.width : 0;
|
|
7310
|
+
// 阈值从 640 降到 320:竖链与阶段带的固有宽度都是 354px——栏窄一点反而更该画图。
|
|
7311
|
+
// 640 那档是按当年那张摊得很开的假数据定的,真数据下它只会把右栏(通常 400~600px)
|
|
7312
|
+
// 全部推给「一列节点卡片」那一支,于是「看不到图谱」。
|
|
7313
|
+
setNarrow(w > 0 && w < 320);
|
|
7314
|
+
});
|
|
7315
|
+
ro.observe(el);
|
|
7316
|
+
return () => ro.disconnect();
|
|
7317
|
+
}, []);
|
|
7318
|
+
// 滚轮缩放必须**自己挂非被动监听**:React 的 onWheel 是挂在 root 上的被动监听,
|
|
7319
|
+
// preventDefault 会被浏览器忽略(控制台里那句 "Unable to preventDefault inside passive
|
|
7320
|
+
// event listener"),结果是 Ctrl+滚轮既缩放画布、又缩放整页,谁也没拦住。
|
|
7321
|
+
// 只认 Ctrl/⌘:普通滚轮仍旧是滚动画布——劫持它会让人滚不到下面的图例。
|
|
7322
|
+
react.useEffect(() => {
|
|
7323
|
+
const el = graphRef.current;
|
|
7324
|
+
if (!el || typeof el.addEventListener !== "function") return undefined;
|
|
7325
|
+
const onWheel = (ev) => {
|
|
7326
|
+
if (!ev.ctrlKey && !ev.metaKey) return;
|
|
7327
|
+
ev.preventDefault();
|
|
7328
|
+
setZoom((z) => clampZoom(z * (ev.deltaY < 0 ? 1.15 : 1 / 1.15)));
|
|
7329
|
+
};
|
|
7330
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
7331
|
+
return () => el.removeEventListener("wheel", onWheel);
|
|
7332
|
+
}, [narrow]);
|
|
7333
|
+
|
|
7334
|
+
const run = runs.find((r) => r.id === selectedRunId) || null;
|
|
7335
|
+
const selectedNode = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) : null;
|
|
7336
|
+
if (!nodes.length) {
|
|
7337
|
+
return h("div", { className: "mneme-pipeview" },
|
|
7338
|
+
h("div", { className: "mneme-pipe-empty" },
|
|
7339
|
+
h("div", { className: "mneme-pipe-emptyTitle" }, t("memory.actions.empty.title")),
|
|
7340
|
+
t("memory.actions.empty.body")
|
|
7341
|
+
),
|
|
7342
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice"))
|
|
7343
|
+
);
|
|
7344
|
+
}
|
|
7345
|
+
return h("div", { className: "mneme-pipeview", ref: rootRef, style: { display: "flex", flexDirection: "column", flex: "1", minHeight: "0" } },
|
|
7346
|
+
h(PipelineRunChips, { t, runs, selectedRunId, onSelectRun: setSelectedRunId }),
|
|
7347
|
+
// 流程概览带:投影里 pipeline.done / trigger 此前一个都没渲染——第一屏
|
|
7348
|
+
// 说不出「这条流程要达成什么、什么时候起跑」。放画布上方、做成 HTML
|
|
7349
|
+
// 块而非 SVG 文本:125 字的完成判据要能换行。数据由投影保证存在。
|
|
7350
|
+
(data && data.pipeline) ? h("div", { className: "mneme-pipe-overview" },
|
|
7351
|
+
h("div", null,
|
|
7352
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.ov.done") + ":"),
|
|
7353
|
+
data.pipeline.done
|
|
7354
|
+
),
|
|
7355
|
+
h("div", null,
|
|
7356
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.ov.trigger") + ":"),
|
|
7357
|
+
(data.pipeline.trigger || []).map((x) => x && x.value).filter(Boolean).join("、") || null
|
|
7358
|
+
)
|
|
7359
|
+
) : null,
|
|
7360
|
+
// 一条运行记录都没有 = 全部候选。这句话得说出来:卡片上「候选」两个字本身
|
|
7361
|
+
// 不解释任何东西,而用户在这一屏最容易问的就是「为什么全是候选」。
|
|
7362
|
+
runs.length ? null : h("div", { className: "mneme-hint" }, t("memory.actions.noRuns")),
|
|
7363
|
+
// 缩放条放在分支**外面**:不论宽窄都在,既是控制,也是「新代码到没到」的肉眼标记。
|
|
7364
|
+
h(PipelineZoomBar, { t, zoom, onZoom: (z) => setZoom(clampZoom(z)) }),
|
|
7365
|
+
narrow
|
|
7366
|
+
? h(PipelineList, { t, data, run, selectedNodeId, onSelectNode: setSelectedNodeId })
|
|
7367
|
+
: h("div", { className: "mneme-pipe-graph", ref: graphRef },
|
|
7368
|
+
// 宽度用百分比而不是 transform:百分比改的是**布局尺寸**,容器照常出滚动条,
|
|
7369
|
+
// 于是顺带就有了平移;transform 只改绘制,放大出去的那一半永远滚不到。
|
|
7370
|
+
h("div", { className: "mneme-pipe-zoomwrap", style: { width: (zoom * 100) + "%" } },
|
|
7371
|
+
h(PipelineGraphSvg, { t, data, run, selectedNodeId, onSelectNode: setSelectedNodeId })
|
|
7372
|
+
)
|
|
7373
|
+
),
|
|
7374
|
+
h(PipelineLegend, { t }),
|
|
7375
|
+
selectedNode ? h(PipelineNodeDetail, { t, node: selectedNode }) : null,
|
|
7376
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice"))
|
|
7377
|
+
);
|
|
7378
|
+
}
|
|
7379
|
+
|
|
7380
|
+
// 右栏 tab 的正文。单独包一层组件、而不是把 hook 塞进 slots.register 的回调里:
|
|
7381
|
+
// 那个回调是不是当组件渲染由宿主决定,hook 放在名字明确的组件里才不必赌这件事。
|
|
7382
|
+
function PipelinePane(props) {
|
|
7383
|
+
const { t, project } = props;
|
|
7384
|
+
return h(PipelineBrowser, { t, project });
|
|
7385
|
+
}
|
|
7386
|
+
|
|
7387
|
+
// 流水线标记:两个步骤块 + 依赖连线 + 状态点;开合状态用右上角
|
|
7388
|
+
// 实心/空心点区分(会话头图标要反映浮层开合)。
|
|
7389
|
+
function PipelineMark(props) {
|
|
7390
|
+
const { size = 16, open } = props || {};
|
|
7391
|
+
return h("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", focusable: "false" },
|
|
7392
|
+
h("path", { d: "M2.5 3h4.2v4.2H2.5z M9.3 8.8h4.2V13H9.3z", fill: "none", stroke: "currentColor", "stroke-width": 1.4 }),
|
|
7393
|
+
h("path", { d: "M6.7 5.1h4v3.7", fill: "none", stroke: "currentColor", "stroke-width": 1.2 }),
|
|
7394
|
+
h("circle", {
|
|
7395
|
+
cx: 13.2, cy: 2.8, r: 1.9,
|
|
7396
|
+
fill: open ? "currentColor" : "none",
|
|
7397
|
+
stroke: "currentColor", "stroke-width": 1.2
|
|
7398
|
+
})
|
|
7399
|
+
);
|
|
7400
|
+
}
|
|
7401
|
+
|
|
7402
|
+
// 会话头入口:悬停 150ms 开 / 移开 120ms 关 / 点击固定 / Escape 关 /
|
|
7403
|
+
// 点击外部关(交互规格与 Agent Teams 浮层一致)。浮层锚 trigger、
|
|
7404
|
+
// portal 到 body。老宿主没有右栏服务时补一个「完整视图」按钮退回
|
|
7405
|
+
// 居中 sheet(形态沿用记忆库 overlay 的背板 + 居中面板)。
|
|
7406
|
+
function PipelineHeaderEntry(props) {
|
|
7407
|
+
const { t, hasPane, openPane, project } = props;
|
|
7408
|
+
const [open, setOpen] = useState(false);
|
|
7409
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
7410
|
+
const openRef = useRef(false);
|
|
7411
|
+
const pinnedRef = useRef(false);
|
|
7412
|
+
const hoverTimer = useRef(null);
|
|
7413
|
+
const rootRef = useRef(null);
|
|
7414
|
+
const anchorRef = useRef(null);
|
|
7415
|
+
const panelRef = useRef(null);
|
|
7416
|
+
const labelRef = useRef(null);
|
|
7417
|
+
const sheetRef = useRef(null);
|
|
7418
|
+
|
|
7419
|
+
const changeOpen = (next) => {
|
|
7420
|
+
const was = openRef.current;
|
|
7421
|
+
const wasPinned = pinnedRef.current;
|
|
7422
|
+
openRef.current = next;
|
|
7423
|
+
setOpen(next);
|
|
7424
|
+
if (!next) pinnedRef.current = false;
|
|
7425
|
+
// announce 只挂「用户明确动作」的开合(点击固定 / Esc / 点外关);
|
|
7426
|
+
// 悬停开合是瞬态,播报反而刷屏——与 §5 无障碍约束对齐。
|
|
7427
|
+
if (next && !was && wasPinned) announce(t("memory.actions.announce.open"));
|
|
7428
|
+
if (!next && was && wasPinned) announce(t("memory.actions.announce.close"));
|
|
7429
|
+
};
|
|
7430
|
+
const cancelHoverChange = () => {
|
|
7431
|
+
if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; }
|
|
7432
|
+
};
|
|
7433
|
+
const scheduleHoverOpen = () => {
|
|
7434
|
+
if (pinnedRef.current || openRef.current) return;
|
|
7435
|
+
// 窄容器下文字标签被 @container 隐藏(图标态)→ 不弹浮层,点击才开。
|
|
7436
|
+
if (labelRef.current && labelRef.current.offsetWidth === 0) return;
|
|
7437
|
+
cancelHoverChange();
|
|
7438
|
+
hoverTimer.current = setTimeout(() => {
|
|
7439
|
+
hoverTimer.current = null;
|
|
7440
|
+
openRef.current = true;
|
|
7441
|
+
setOpen(true);
|
|
7442
|
+
}, 150);
|
|
7443
|
+
};
|
|
7444
|
+
const scheduleHoverClose = () => {
|
|
7445
|
+
if (pinnedRef.current) return;
|
|
7446
|
+
cancelHoverChange();
|
|
7447
|
+
hoverTimer.current = setTimeout(() => {
|
|
7448
|
+
hoverTimer.current = null;
|
|
7449
|
+
openRef.current = false;
|
|
7450
|
+
setOpen(false);
|
|
7451
|
+
}, 120);
|
|
7452
|
+
};
|
|
7453
|
+
|
|
7454
|
+
// 钩子顺序稳定:useAnchoredPosition 的存在性是模块常量条件,两次
|
|
7455
|
+
// 判断在所有渲染中走同一分支,不违反 hook 规则。
|
|
7456
|
+
const positioned = primitives && typeof primitives.useAnchoredPosition === "function"
|
|
7457
|
+
? primitives.useAnchoredPosition({ open, anchorRef, panelRef, gap: 5, margin: 16 })
|
|
7458
|
+
: null;
|
|
7459
|
+
const panelStyle = positioned
|
|
7460
|
+
? { ...positioned }
|
|
7461
|
+
: { position: "fixed", left: 0, top: 0, visibility: "hidden" };
|
|
7462
|
+
|
|
7463
|
+
if (primitives && typeof primitives.useDismissOnOutsidePointer === "function") {
|
|
7464
|
+
primitives.useDismissOnOutsidePointer(rootRef, open, changeOpen, panelRef);
|
|
7465
|
+
}
|
|
7466
|
+
useEffect(() => {
|
|
7467
|
+
if (primitives && typeof primitives.useDismissOnOutsidePointer === "function") return;
|
|
7468
|
+
if (!open) return;
|
|
7469
|
+
const onDown = (ev) => {
|
|
7470
|
+
const panel = panelRef.current;
|
|
7471
|
+
const root = rootRef.current;
|
|
7472
|
+
if (panel && panel.contains(ev.target)) return;
|
|
7473
|
+
if (root && root.contains(ev.target)) return;
|
|
7474
|
+
changeOpen(false);
|
|
7475
|
+
};
|
|
7476
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
7477
|
+
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
7478
|
+
}, [open]);
|
|
7479
|
+
useEffect(() => () => cancelHoverChange(), []);
|
|
7480
|
+
|
|
7481
|
+
// 居中 sheet 是「宿主没有右栏服务」时的唯一完整入口,键盘用户必须能进能出。
|
|
7482
|
+
// 它 portal 到 body,焦点默认留在原 trigger 上,而挂在 sheet 元素上的
|
|
7483
|
+
// onKeyDown 既因没有 tabIndex 不被聚焦、也收不到外部键盘事件——Escape 于是
|
|
7484
|
+
// 不生效,只剩 Tab 摸到关闭按钮一条路。做法与 MemoryOverlay 一致:打开时把
|
|
7485
|
+
// 焦点移进 sheet,Escape 在 window 上收,关掉再把焦点还给 trigger。
|
|
7486
|
+
useEffect(() => {
|
|
7487
|
+
if (!sheetOpen) return undefined;
|
|
7488
|
+
if (sheetRef.current) sheetRef.current.focus();
|
|
7489
|
+
const onKey = (ev) => {
|
|
7490
|
+
if (ev.key !== "Escape") return;
|
|
7491
|
+
ev.preventDefault();
|
|
7492
|
+
ev.stopPropagation();
|
|
7493
|
+
setSheetOpen(false);
|
|
7494
|
+
if (anchorRef.current) anchorRef.current.focus();
|
|
7495
|
+
};
|
|
7496
|
+
window.addEventListener("keydown", onKey);
|
|
7497
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
7498
|
+
}, [sheetOpen]);
|
|
7499
|
+
|
|
7500
|
+
const closeAll = () => {
|
|
7501
|
+
changeOpen(false);
|
|
7502
|
+
if (anchorRef.current) anchorRef.current.focus();
|
|
7503
|
+
};
|
|
7504
|
+
const onTriggerClick = () => {
|
|
7505
|
+
if (!openRef.current) {
|
|
7506
|
+
pinnedRef.current = true;
|
|
7507
|
+
changeOpen(true);
|
|
7508
|
+
setTimeout(() => { if (panelRef.current) panelRef.current.focus(); }, 0);
|
|
7509
|
+
} else if (pinnedRef.current) {
|
|
7510
|
+
changeOpen(false);
|
|
7511
|
+
} else {
|
|
7512
|
+
pinnedRef.current = true;
|
|
7513
|
+
if (panelRef.current) panelRef.current.focus();
|
|
7514
|
+
}
|
|
7515
|
+
};
|
|
7516
|
+
const doc = usePipeline(project, null);
|
|
7517
|
+
// 头部这一格标的是**这份清单读的是哪个项目**(某条工作流归谁,标在清单每一行上):
|
|
7518
|
+
// 只取目录名,绝对路径不上界面——面板是会被截图的那一面。认不出项目时如实说,
|
|
7519
|
+
// 而不是编一个「全局」顶上:认不出和全局是两件事。
|
|
7520
|
+
const projectSource = ((doc && doc.sources) || []).find((s) => s.scope === "project");
|
|
7521
|
+
const scopeLabel = projectSource && projectSource.label
|
|
7522
|
+
? t("memory.actions.scope.project").replace("{name}", projectSource.label)
|
|
7523
|
+
: t("memory.actions.scope.none");
|
|
7524
|
+
// 数的是这个项目里读出几条工作流;没拿到数据时是 0,不是随便挑一个数。
|
|
7525
|
+
const workflowCount = doc && doc.available ? doc.available.length : 0;
|
|
7526
|
+
|
|
7527
|
+
const panel = (open && reactDom && typeof document !== "undefined")
|
|
7528
|
+
? reactDom.createPortal(
|
|
7529
|
+
h("div", {
|
|
7530
|
+
ref: panelRef,
|
|
7531
|
+
// 注意:client.test.js 锁死全文件不得出现 role:"dialog"
|
|
7532
|
+
// (v0.7.15 清退 dialog 面的纪律),浮层只带 aria-label。
|
|
7533
|
+
"aria-label": t("memory.actions.title"),
|
|
7534
|
+
tabIndex: -1,
|
|
7535
|
+
className: "mneme-pipe mneme-pipe-panel",
|
|
7536
|
+
style: panelStyle,
|
|
7537
|
+
onMouseEnter: cancelHoverChange,
|
|
7538
|
+
onMouseLeave: scheduleHoverClose,
|
|
7539
|
+
onKeyDown: (ev) => { if (ev.key === "Escape") { ev.preventDefault(); ev.stopPropagation(); closeAll(); } }
|
|
7540
|
+
},
|
|
7541
|
+
h("div", { className: "mneme-pipe-head" },
|
|
7542
|
+
h("span", { className: "mneme-pipe-title" }, t("memory.actions.title")),
|
|
7543
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7544
|
+
h("span", { className: "mneme-pipe-scope" }, scopeLabel),
|
|
7545
|
+
h("button", {
|
|
7546
|
+
type: "button",
|
|
7547
|
+
className: "mneme-pipe-run",
|
|
7548
|
+
// 有右栏服务:openTab(kind) 打开原生右栏 tab(页面类型无
|
|
7549
|
+
// patterns,按 kind 打开)并收起浮层;打不开或老宿主
|
|
7550
|
+
// (无 sidebarRight)退回本插件的居中 sheet——形态沿用
|
|
7551
|
+
// 记忆库 overlay 的背板 + 居中面板。
|
|
7552
|
+
onClick: () => {
|
|
7553
|
+
if (openPane && openPane()) { changeOpen(false); return; }
|
|
7554
|
+
changeOpen(false);
|
|
7555
|
+
setSheetOpen(true);
|
|
7556
|
+
}
|
|
7557
|
+
}, hasPane && openPane ? t("memory.actions.openPane") : t("memory.actions.viewFull"))
|
|
7558
|
+
),
|
|
7559
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice")),
|
|
7560
|
+
h(PipelineBrowser, { t, project })
|
|
7561
|
+
),
|
|
7562
|
+
document.body
|
|
7563
|
+
)
|
|
7564
|
+
: null;
|
|
7565
|
+
|
|
7566
|
+
const sheet = (sheetOpen && reactDom && typeof document !== "undefined")
|
|
7567
|
+
? reactDom.createPortal(
|
|
7568
|
+
h("div", {
|
|
7569
|
+
className: "mneme-backdrop",
|
|
7570
|
+
onClick: (ev) => { if (ev.target === ev.currentTarget) setSheetOpen(false); }
|
|
7571
|
+
},
|
|
7572
|
+
h("div", {
|
|
7573
|
+
ref: sheetRef,
|
|
7574
|
+
tabIndex: -1,
|
|
7575
|
+
className: "mneme-pipe mneme-pipe-sheet",
|
|
7576
|
+
"aria-label": t("memory.actions.title"),
|
|
7577
|
+
onKeyDown: (ev) => { if (ev.key === "Escape") { ev.stopPropagation(); setSheetOpen(false); } }
|
|
7578
|
+
},
|
|
7579
|
+
h("div", { className: "mneme-pipe-sheetbar" },
|
|
7580
|
+
h("span", { className: "mneme-pipe-title" }, t("memory.actions.title")),
|
|
7581
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7582
|
+
h("span", { className: "mneme-pipe-scope" }, scopeLabel),
|
|
7583
|
+
h("button", { type: "button", className: "mneme-pipe-run", onClick: () => setSheetOpen(false) }, t("memory.actions.close"))
|
|
7584
|
+
),
|
|
7585
|
+
h("div", { className: "mneme-pipe-sheetbody" }, h(PipelineBrowser, { t, project }))
|
|
7586
|
+
)
|
|
7587
|
+
),
|
|
7588
|
+
document.body
|
|
7589
|
+
)
|
|
7590
|
+
: null;
|
|
7591
|
+
|
|
7592
|
+
return h("div", {
|
|
7593
|
+
className: "mneme-pipe-root",
|
|
7594
|
+
ref: rootRef,
|
|
7595
|
+
onMouseEnter: scheduleHoverOpen,
|
|
7596
|
+
onMouseLeave: scheduleHoverClose
|
|
7597
|
+
},
|
|
7598
|
+
h("button", {
|
|
7599
|
+
type: "button",
|
|
7600
|
+
ref: anchorRef,
|
|
7601
|
+
className: "mneme-pipe-trigger",
|
|
7602
|
+
"aria-haspopup": "true",
|
|
7603
|
+
"aria-expanded": open ? "true" : "false",
|
|
7604
|
+
title: t("memory.actions.open"),
|
|
7605
|
+
onClick: onTriggerClick,
|
|
7606
|
+
onKeyDown: (ev) => { if (ev.key === "Escape" && open) { ev.preventDefault(); ev.stopPropagation(); closeAll(); } }
|
|
7607
|
+
},
|
|
7608
|
+
h(PipelineMark, { open }),
|
|
7609
|
+
h("span", { ref: labelRef, className: "mneme-pipe-triggerLabel" }, t("memory.actions.title")),
|
|
7610
|
+
workflowCount > 0 ? h("span", {
|
|
7611
|
+
className: "mneme-pipe-dot",
|
|
7612
|
+
title: t("memory.actions.list.count").replace("{n}", String(workflowCount))
|
|
7613
|
+
}, String(workflowCount)) : null
|
|
7614
|
+
),
|
|
7615
|
+
panel,
|
|
7616
|
+
sheet
|
|
7617
|
+
);
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
// 对话行结算卡(turnTail):死的落款——「这次跑哪条被用上了」。
|
|
7621
|
+
// 与会话头浮层读同一份数据;卡上勾选 = usedNodeIds(真正被复用),
|
|
7622
|
+
// 其余 path 节点只是被走过。没有运行记录时整张卡不出现——发一条「本次没用上任何
|
|
7623
|
+
// 做法」的空卡比不发更吵:它每一轮都会来一次。
|
|
7624
|
+
function TurnRunCard(props) {
|
|
7625
|
+
const { t, project } = props;
|
|
7626
|
+
const doc = usePipeline(project, null);
|
|
7627
|
+
const data = doc || {};
|
|
7628
|
+
const run = (data.runs || []).find((r) => r.id === "r1") || (data.runs || [])[0];
|
|
7629
|
+
if (!run) return null;
|
|
7630
|
+
const used = run.usedNodeIds || [];
|
|
7631
|
+
const refutedUsed = used.some((id) => {
|
|
7632
|
+
const n = (data.nodes || []).find((x) => x.id === id);
|
|
7633
|
+
return n && n.state === "refuted";
|
|
7634
|
+
});
|
|
7635
|
+
return h("div", { className: "mneme-pipe mneme-pipe-settle" },
|
|
7636
|
+
h("div", { className: "mneme-pipe-settleHead" },
|
|
7637
|
+
h("span", { className: "mneme-pipe-settleTitle" },
|
|
7638
|
+
h(PipelineMark, { size: 13 }),
|
|
7639
|
+
t("memory.actions.settle.title")
|
|
7640
|
+
),
|
|
7641
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7642
|
+
h("span", { className: "mneme-pipe-scope" },
|
|
7643
|
+
t("memory.actions.settle.line").replace("{title}", data.pipeline.title) + " · " + pipeFmtWhen(run.at))
|
|
7644
|
+
),
|
|
7645
|
+
h("div", { className: "mneme-pipe-steps" },
|
|
7646
|
+
(run.path || []).map((id, i) => {
|
|
7647
|
+
const n = (data.nodes || []).find((x) => x.id === id);
|
|
7648
|
+
if (!n) return null;
|
|
7649
|
+
const isUsed = used.includes(id);
|
|
7650
|
+
return h(react.Fragment, { key: id + ":" + i },
|
|
7651
|
+
i > 0 ? h("span", { className: "mneme-pipe-stepArrow", "aria-hidden": "true" }, "→") : null,
|
|
7652
|
+
h("span", {
|
|
7653
|
+
className: "mneme-pipe-stepChip" + (isUsed ? " mneme-pipe-stepChip--used" : ""),
|
|
7654
|
+
title: n.title
|
|
7655
|
+
},
|
|
7656
|
+
(isUsed ? "✓ " : "") + pipeClip(n.title, 12)
|
|
7657
|
+
)
|
|
7658
|
+
);
|
|
7659
|
+
})
|
|
7660
|
+
),
|
|
7661
|
+
h("div", { className: "mneme-pipe-scope" },
|
|
7662
|
+
t("memory.actions.settle.used").replace("{n}", String(used.length))
|
|
7663
|
+
),
|
|
7664
|
+
refutedUsed ? h("div", { className: "mneme-pipe-scope" }, t("memory.actions.settle.refutedWarn")) : null
|
|
7665
|
+
);
|
|
7666
|
+
}
|
|
7667
|
+
|
|
7668
|
+
// 座位注册(仅在旗标开启后被调用):会话头入口、对话行结算卡、
|
|
7669
|
+
// 右栏类型与正文。⚠️ 服务名 sidebarRight 与注册服务名 sidebarRightTabs
|
|
7670
|
+
// 是两个不同的名字——类型/引导挂 sidebarRightTabs,打开走
|
|
7671
|
+
// ctx.get("sidebarRight").openTab。无右栏服务时不注册 pane(浮层 +
|
|
7672
|
+
// 居中 sheet 退化已在 PipelineHeaderEntry 内)。
|
|
7673
|
+
function registerPipelineSeats(ctx, t) {
|
|
7674
|
+
let hasPane = false;
|
|
7675
|
+
try {
|
|
7676
|
+
const sb = typeof ctx.get === "function" ? ctx.get("sidebarRight") : null;
|
|
7677
|
+
hasPane = !!(sb && typeof sb.openTab === "function");
|
|
7678
|
+
} catch { hasPane = false; }
|
|
7679
|
+
// 右栏打开:页面类型(无 patterns)按 kind 打开——openTab(kind)。
|
|
7680
|
+
// 服务半可用 / kind 未注册抛错时返回 false,调用方退化到居中 sheet。
|
|
7681
|
+
const openPane = () => {
|
|
7682
|
+
try {
|
|
7683
|
+
const sb = typeof ctx.get === "function" ? ctx.get("sidebarRight") : null;
|
|
7684
|
+
if (sb && typeof sb.openTab === "function") {
|
|
7685
|
+
sb.openTab(PIPELINE_ACTIONS_TAB_KIND);
|
|
7686
|
+
return true;
|
|
7687
|
+
}
|
|
7688
|
+
} catch (err) {
|
|
7689
|
+
console.warn("[dsh-mneme] sidebarRight.openTab failed, falling back to sheet:", err);
|
|
7690
|
+
}
|
|
7691
|
+
return false;
|
|
7692
|
+
};
|
|
7693
|
+
|
|
7694
|
+
// 会话头入口:.actions 座位(.corner 被原生侧栏展开键占用,别用)。
|
|
7695
|
+
// 会话工作目录只有客户端拿得到,而「当前是哪个会话」只在座位注入里给——所以
|
|
7696
|
+
// 每个座位都要 inject: (sessionId) => ({ sessionId }),否则读到的永远是全局层。
|
|
7697
|
+
// 宿主若不吃这个选项,project 就是 null,面板如实说「项目未知」,不假装成功。
|
|
7698
|
+
ctx.slots.inject("conversation.session.header.actions", () => {
|
|
7699
|
+
return ctx.slots.register({
|
|
7700
|
+
name: "conversation.session.header.actions",
|
|
7701
|
+
id: "dsh-mneme:pipeline",
|
|
7702
|
+
order: 30,
|
|
7703
|
+
inject: (sessionId) => ({ sessionId })
|
|
7704
|
+
}, (props) => h(PipelineHeaderEntry, {
|
|
7705
|
+
t, hasPane, openPane, project: sessionProject(ctx, props && props.sessionId)
|
|
7706
|
+
}));
|
|
7707
|
+
});
|
|
7708
|
+
|
|
7709
|
+
// 对话行结算卡:回答「这次跑哪条被用上了」。
|
|
7710
|
+
ctx.slots.inject("conversation.chat.turnTail", () => {
|
|
7711
|
+
return ctx.slots.register({
|
|
7712
|
+
name: "conversation.chat.turnTail",
|
|
7713
|
+
id: "dsh-mneme:pipeline:settle",
|
|
7714
|
+
// kind/scope 是**声明方**(宿主)在 children 上写的,注册方不重复声明——
|
|
7715
|
+
// 座位本身已经声明成 { kind: "list", scope: "session" },多写一份只是噪音,
|
|
7716
|
+
// 万一注册侧按 schema 严校就是一次启动期抛错。
|
|
7717
|
+
inject: (sessionId) => ({ sessionId })
|
|
7718
|
+
}, (props) => h(TurnRunCard, { t, project: sessionProject(ctx, props && props.sessionId) }));
|
|
7719
|
+
});
|
|
7720
|
+
|
|
7721
|
+
// tab 的**类型**与它的**正文**必须各自兜自己的异常,而且类型注册要幂等。
|
|
7722
|
+
// 为什么:宿主对重复的类型 id 会抛(`sidebarRight: tab type id "…" is already registered`),
|
|
7723
|
+
// 而这两件事的生命周期不一样——类型注册挂在宿主服务 `sidebarRightTabs` 上、插件重挂(刷新 /
|
|
7724
|
+
// 热重载 / 会话重建)时**留着**;正文座位是挂插件 scope 的,会随插件回收、重挂时要重来。
|
|
7725
|
+
// 原来两件事写在同一个 try 里,于是第二次挂载时类型那一抛就把正文一起带走了:界面上
|
|
7726
|
+
// 只剩宿主的 fallback「这类内容还没有可用的查看方式」,而错误只落在浏览器控制台里
|
|
7727
|
+
// ——看不见、也搜不到,是最费时间的一类。
|
|
7728
|
+
try {
|
|
7729
|
+
const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : null;
|
|
7730
|
+
if (tabs && typeof tabs.register === "function") {
|
|
7731
|
+
const already = typeof tabs.get === "function" ? tabs.get(PIPELINE_ACTIONS_TAB_ID) : null;
|
|
7732
|
+
if (!already) {
|
|
7733
|
+
tabs.register({
|
|
7734
|
+
id: PIPELINE_ACTIONS_TAB_ID,
|
|
7735
|
+
kind: PIPELINE_ACTIONS_TAB_KIND,
|
|
7736
|
+
title: () => t("memory.actions.title"),
|
|
7737
|
+
guide: [{
|
|
7738
|
+
id: "dsh-mneme:pipeline:guide",
|
|
7739
|
+
order: 10,
|
|
7740
|
+
title: () => t("memory.actions.title"),
|
|
7741
|
+
description: () => t("memory.actions.notice"),
|
|
7742
|
+
icon: (size) => h(PipelineMark, { size, open: false })
|
|
7743
|
+
}]
|
|
7744
|
+
});
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
} catch (err) {
|
|
7748
|
+
console.warn("[dsh-mneme] action pipeline tab type registration failed:", err);
|
|
7749
|
+
}
|
|
7750
|
+
|
|
7751
|
+
try {
|
|
7752
|
+
const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : null;
|
|
7753
|
+
if (tabs && typeof tabs.register === "function") {
|
|
7754
|
+
// 正文/标题都是按键座位:key = 类型 id,宿主按当前 tab 取。
|
|
7755
|
+
ctx.slots.inject("sidebar.right.pane.tab", () => {
|
|
7756
|
+
return ctx.slots.register({
|
|
7757
|
+
name: "sidebar.right.pane.tab",
|
|
7758
|
+
key: PIPELINE_ACTIONS_TAB_ID,
|
|
7759
|
+
inject: (sessionId) => ({ sessionId })
|
|
7760
|
+
}, (props) => h(PipelinePane, { t, project: sessionProject(ctx, props && props.sessionId) }));
|
|
7761
|
+
});
|
|
7762
|
+
ctx.slots.inject("sidebar.right.pane.tab.title", () => {
|
|
7763
|
+
return ctx.slots.register({
|
|
7764
|
+
name: "sidebar.right.pane.tab.title",
|
|
7765
|
+
key: PIPELINE_ACTIONS_TAB_ID
|
|
7766
|
+
}, () => t("memory.actions.title"));
|
|
7767
|
+
});
|
|
7768
|
+
}
|
|
7769
|
+
} catch (err) {
|
|
7770
|
+
console.warn("[dsh-mneme] action pipeline pane body registration failed:", err);
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
if (!hasPane) {
|
|
7774
|
+
console.info("[dsh-mneme] action pipeline: no sidebarRight service, pane tab skipped (floating panel + sheet fallback active)");
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
|
|
5368
7778
|
function apply(ctx) {
|
|
5369
7779
|
const t = ctx.locale.bind(NS);
|
|
5370
7780
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
@@ -5436,10 +7846,30 @@ window.__ModuleLoader__.load({
|
|
|
5436
7846
|
}
|
|
5437
7847
|
return () => { dead = true; };
|
|
5438
7848
|
}, "dsh-mneme: better-sidebar mount");
|
|
7849
|
+
|
|
7850
|
+
// --- v0.9.0 M.O.(beta):旗标门控 ---
|
|
7851
|
+
// actionPipelineEnabled 关闭(默认)时一个座位都不注册——座位闸门
|
|
7852
|
+
// 在注册层而不是样式层(不是 display:none)。features 拉取失败时
|
|
7853
|
+
// 保持关闭(fail-closed),不猜。
|
|
7854
|
+
ctx.effect(() => {
|
|
7855
|
+
let dead = false;
|
|
7856
|
+
apiFetch("/api/dsh-mneme/features")
|
|
7857
|
+
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
|
|
7858
|
+
.then((j) => {
|
|
7859
|
+
if (dead) return;
|
|
7860
|
+
const eff = (j && j.effective) || {};
|
|
7861
|
+
if (!eff.actionPipelineEnabled) return;
|
|
7862
|
+
registerPipelineSeats(ctx, t);
|
|
7863
|
+
})
|
|
7864
|
+
.catch((err) => {
|
|
7865
|
+
console.warn("[dsh-mneme] features fetch failed, action pipeline stays off:", err);
|
|
7866
|
+
});
|
|
7867
|
+
return () => { dead = true; };
|
|
7868
|
+
}, "dsh-mneme: action-pipeline gate");
|
|
5439
7869
|
}
|
|
5440
7870
|
|
|
5441
7871
|
exports.apply = apply;
|
|
5442
7872
|
exports.inject = inject;
|
|
5443
7873
|
return module.exports;
|
|
5444
7874
|
}
|
|
5445
|
-
});
|
|
7875
|
+
});
|