@modusensus/dsh-mneme 0.8.16 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -16
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +8 -2
- package/docs/CONFIGURATION.md +27 -1
- package/docs/CONTINUITY.md +18 -3
- package/docs/SEMANTIC.md +3 -3
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +78 -12
- package/lib/api.js +403 -62
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2294 -423
- package/lib/config.js +63 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +17 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +5 -1
- package/lib/embedding.js +116 -15
- package/lib/entities/extractor.js +4 -0
- package/lib/index.js +81 -12
- package/lib/inject.js +40 -4
- package/lib/lang.js +124 -0
- package/lib/pipeline-activities.js +178 -0
- package/lib/pipeline-attribution.js +165 -0
- package/lib/pipeline-command.js +279 -0
- package/lib/pipeline-confirm.js +142 -0
- package/lib/pipeline-exec.js +174 -0
- package/lib/pipeline-nudge.js +1281 -0
- package/lib/pipeline-observations.js +181 -0
- package/lib/pipeline-observe.js +133 -0
- package/lib/pipeline-runner.js +346 -0
- package/lib/pipeline-runs.js +442 -0
- package/lib/pipeline.js +655 -0
- package/lib/scope.js +16 -4
- package/lib/semantic.js +66 -10
- package/lib/serve.js +1 -1
- package/lib/service.js +59 -18
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +138 -1
- package/lib/store.js +132 -6
- package/lib/summarize.js +30 -0
- package/lib/tools.js +11 -2
- package/package.json +3 -3
- package/scripts/changelog-prep.mjs +62 -7
- package/scripts/check-sync.js +8 -6
- package/scripts/publish-gate.mjs +197 -0
- package/scripts/release-docs.mjs +204 -0
- package/scripts/release-notes.mjs +162 -0
- package/src/api-standalone.js +78 -12
- package/src/api.js +403 -62
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +63 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +17 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +5 -1
- package/src/embedding.js +116 -15
- package/src/entities/extractor.js +4 -0
- package/src/index.js +81 -12
- package/src/inject.js +40 -4
- package/src/lang.js +124 -0
- package/src/pipeline-activities.js +178 -0
- package/src/pipeline-attribution.js +165 -0
- package/src/pipeline-command.js +279 -0
- package/src/pipeline-confirm.js +142 -0
- package/src/pipeline-exec.js +174 -0
- package/src/pipeline-nudge.js +1281 -0
- package/src/pipeline-observations.js +181 -0
- package/src/pipeline-observe.js +133 -0
- package/src/pipeline-runner.js +346 -0
- package/src/pipeline-runs.js +442 -0
- package/src/pipeline.js +655 -0
- package/src/scope.js +16 -4
- package/src/semantic.js +66 -10
- package/src/serve.js +1 -1
- package/src/service.js +59 -18
- package/src/session-usage.js +350 -0
- package/src/settings.js +138 -1
- package/src/store.js +132 -6
- package/src/summarize.js +30 -0
- package/src/tools.js +11 -2
- package/test/api-hardening.test.js +173 -0
- package/test/api.test.js +244 -7
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/changelog-prep.test.js +74 -8
- package/test/cli-enums.test.js +37 -0
- package/test/client.test.js +510 -2
- package/test/continuity.test.js +84 -0
- package/test/docs-config-keys.test.js +65 -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/helpers/write-admission-samples.js +6 -2
- package/test/inject-recall.test.js +32 -4
- package/test/lib-smoke.test.js +20 -0
- package/test/llm-audit.test.js +92 -1
- package/test/mint-source.test.js +202 -0
- package/test/persona-api.test.js +172 -0
- package/test/persona-flag.test.js +36 -0
- package/test/persona-inject.test.js +145 -0
- package/test/persona-settings.test.js +145 -0
- package/test/pipeline-activities.test.js +155 -0
- package/test/pipeline-attribution.test.js +175 -0
- package/test/pipeline-command.test.js +243 -0
- package/test/pipeline-confirm.test.js +148 -0
- package/test/pipeline-exec.test.js +172 -0
- package/test/pipeline-nudge.test.js +1036 -0
- package/test/pipeline-observations.test.js +157 -0
- package/test/pipeline-observe.test.js +127 -0
- package/test/pipeline-r1r2.test.js +149 -0
- package/test/pipeline-runner.test.js +442 -0
- package/test/pipeline-runs.test.js +767 -0
- package/test/pipeline.test.js +636 -0
- package/test/placement.test.js +1 -1
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/reindex-backfill.test.js +83 -4
- package/test/release-docs.test.js +130 -0
- package/test/release-notes.test.js +240 -0
- package/test/release-workflow.test.js +93 -0
- package/test/route-policy.test.js +113 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/security-doc.test.js +139 -0
- package/test/sensitive-scan.test.js +44 -0
- package/test/serve.test.js +1 -1
- package/test/session-usage.test.js +308 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/summarize.test.js +176 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
- package/test/workflow-guards.test.js +156 -0
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);
|
|
@@ -150,7 +147,11 @@ window.__ModuleLoader__.load({
|
|
|
150
147
|
flame: [["path", { d: "M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" }]],
|
|
151
148
|
// 意见与反馈入口的行首图标(Lucide 数据,同上 ISC 惯例内联)
|
|
152
149
|
bug: [["path", { d: "m8 2 1.88 1.88" }], ["path", { d: "M14.12 3.88 16 2" }], ["path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }], ["path", { d: "M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" }], ["path", { d: "M12 20v-9" }], ["path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }], ["path", { d: "M6 13H2" }], ["path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }], ["path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }], ["path", { d: "M22 13h-4" }], ["path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" }]],
|
|
153
|
-
mail: [["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }], ["path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" }]]
|
|
150
|
+
mail: [["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }], ["path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" }]],
|
|
151
|
+
// star 是设置页鼓励入口的行首图标(Lucide 数据,同上 ISC 惯例内联)。
|
|
152
|
+
// Icon 对未注册的名字静默 return null——漏了它就是一行缺图标、闸门全绿,
|
|
153
|
+
// 所以这条表与用法必须同处出现(见 test/client.test.js 的图标名锁)。
|
|
154
|
+
star: [["polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" }]]
|
|
154
155
|
};
|
|
155
156
|
const Icon = ({ name, size = 16, className }) => {
|
|
156
157
|
const parts = ICON_PATHS[name];
|
|
@@ -280,6 +281,31 @@ window.__ModuleLoader__.load({
|
|
|
280
281
|
"memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
|
|
281
282
|
"memory.settings.profileSave": "保存画像",
|
|
282
283
|
"memory.settings.profileSaved": "画像已保存",
|
|
284
|
+
"memory.settings.persona": "人格定义(beta)",
|
|
285
|
+
"memory.settings.personaHint": "给这位 Agent 一张角色卡:名字、性格、回复要求。开关打开且下方「人格定义」总闸开启时,每轮对话都会注入。",
|
|
286
|
+
"memory.settings.personaName": "名字",
|
|
287
|
+
"memory.settings.personaNamePlaceholder": "例如:Nyx",
|
|
288
|
+
"memory.settings.personaPersonality": "性格设定",
|
|
289
|
+
"memory.settings.personaPersonalityPlaceholder": "例如:轻松幽默,讲难题时用类比帮用户快速理解",
|
|
290
|
+
"memory.settings.personaReplyStyle": "回复要求",
|
|
291
|
+
"memory.settings.personaReplyStylePlaceholder": "例如:先给结论再解释;用人在用的词,不用机制名",
|
|
292
|
+
"memory.settings.personaSave": "保存人格",
|
|
293
|
+
"memory.settings.personaSaved": "人格已保存",
|
|
294
|
+
"memory.settings.personaSaveFailed": "保存失败",
|
|
295
|
+
"memory.settings.personaEnabledToggle": "启用这张卡",
|
|
296
|
+
"memory.settings.personaEnabledOn": "已启用——身份命中的会话会注入这张卡",
|
|
297
|
+
"memory.settings.personaEnabledOff": "未启用——这个身份的会话将使用默认卡",
|
|
298
|
+
"memory.settings.personaDefaultCard": "默认卡(兜底)",
|
|
299
|
+
"memory.settings.personaAdd": "添加卡",
|
|
300
|
+
"memory.settings.personaAddPlaceholder": "输入 agent 身份名(如 nyx),按它寻址注入",
|
|
301
|
+
"memory.settings.personaEditingDefault": "正在编辑:默认卡——没有命中任何身份卡的会话用它",
|
|
302
|
+
"memory.settings.personaEditingFor": "正在编辑身份卡:",
|
|
303
|
+
"memory.settings.personaDeleteCard": "删除这张卡",
|
|
304
|
+
"memory.settings.personaView": "Agent 眼中的你(注入生效文本)",
|
|
305
|
+
"memory.settings.personaViewHint": "以下是每一轮实际注入的用户画像与规则,只读。",
|
|
306
|
+
"memory.settings.personaViewEmpty": "画像与规则均为空——Agent 每轮只会看到记忆库。",
|
|
307
|
+
"memory.features.personaEnabled": "人格定义(beta)",
|
|
308
|
+
"memory.features.personaEnabled.hint": "把「设置 → 个性化」里的人格卡注入到每轮对话。默认关(beta);总闸与人格卡内的开关同时打开才生效。",
|
|
283
309
|
"memory.settings.rules": "规则",
|
|
284
310
|
"memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
|
|
285
311
|
"memory.settings.ruleAdd": "添加规则",
|
|
@@ -302,12 +328,14 @@ window.__ModuleLoader__.load({
|
|
|
302
328
|
"memory.explorer.tabStatus": "状态",
|
|
303
329
|
"memory.entities.count": "{n} 个实体",
|
|
304
330
|
"memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
|
|
331
|
+
// 搜索框有字但一条没筛出来时用它;与 entities.none(库里真没实体)分开,
|
|
332
|
+
// 否则「暂无实体」会把打错一个字误导成「你还没开抽取」。
|
|
333
|
+
"memory.entities.noMatch": "没有匹配的实体,换个词试试",
|
|
305
334
|
"memory.entities.none": "暂无实体:开启实体抽取后,记忆里的人物 / 项目 / 概念会自动沉淀到这里",
|
|
306
335
|
"memory.entities.attrs": "属性",
|
|
307
336
|
"memory.entities.relations": "关系",
|
|
308
337
|
"memory.entities.mentions": "{n} 次提及",
|
|
309
338
|
"memory.entities.lastSeen": "最近出现",
|
|
310
|
-
"memory.entities.graph": "关系图谱",
|
|
311
339
|
"memory.entity.person": "人物",
|
|
312
340
|
"memory.entity.project": "项目",
|
|
313
341
|
"memory.entity.concept": "概念",
|
|
@@ -332,8 +360,6 @@ window.__ModuleLoader__.load({
|
|
|
332
360
|
"memory.explorer.tags": "标签",
|
|
333
361
|
"memory.explorer.importance": "重要性",
|
|
334
362
|
"memory.explorer.heat": "热度",
|
|
335
|
-
"memory.explorer.sourceFilter": "来源",
|
|
336
|
-
"memory.explorer.sort": "排序",
|
|
337
363
|
"memory.explorer.heatSort": "热度优先",
|
|
338
364
|
"memory.explorer.heatSortHint": "对已加载条目按热度降序(页内排序,非全局序;时间树保持时间序)",
|
|
339
365
|
"memory.explorer.filterDeposited": "沉淀",
|
|
@@ -354,7 +380,6 @@ window.__ModuleLoader__.load({
|
|
|
354
380
|
"memory.graph.depth": "跳数",
|
|
355
381
|
"memory.graph.empty": "图谱待积累:随对话记忆的沉淀,实体与关系会自动进入图谱",
|
|
356
382
|
"memory.graph.notFound": "未找到该实体",
|
|
357
|
-
"memory.graph.attrs": "属性",
|
|
358
383
|
"memory.graph.related": "关联记忆",
|
|
359
384
|
"memory.graph.relation": "关系",
|
|
360
385
|
"memory.graph.sourceMemory": "查看来源记忆",
|
|
@@ -376,8 +401,8 @@ window.__ModuleLoader__.load({
|
|
|
376
401
|
"memory.settings.vectorReindexing": "索引中…",
|
|
377
402
|
"memory.settings.vectorReindexDone": "已索引 {n} 条",
|
|
378
403
|
"memory.settings.apiTokenTitle": "API Token(可选)",
|
|
379
|
-
"memory.settings.apiTokenHint": "
|
|
380
|
-
"memory.settings.apiTokenPlaceholder": "
|
|
404
|
+
"memory.settings.apiTokenHint": "这里只把 Token 存在当前浏览器(localStorage),供面板请求携带;服务端的锁是插件配置里的 apiToken,两边填同一个值才真正上锁。只填这里不改服务端 = 没上锁。",
|
|
405
|
+
"memory.settings.apiTokenPlaceholder": "与插件配置的 apiToken 保持一致",
|
|
381
406
|
"memory.settings.apiTokenSave": "保存 Token",
|
|
382
407
|
"memory.settings.apiTokenSaved": "Token 已保存",
|
|
383
408
|
"memory.settings.feedback.title": "意见与反馈",
|
|
@@ -389,8 +414,16 @@ window.__ModuleLoader__.load({
|
|
|
389
414
|
"memory.settings.feedback.browse": "浏览已知问题",
|
|
390
415
|
"memory.settings.feedback.browse.sub": "先搜搜,省一份重复 issue",
|
|
391
416
|
"memory.settings.feedback.hint": "反馈前先搜搜是否已有相同问题,能省一份重复的 issue~",
|
|
392
|
-
"memory.settings.
|
|
417
|
+
"memory.settings.update.check": "检测更新",
|
|
418
|
+
"memory.settings.update.latest": "已是最新版本 ✓",
|
|
419
|
+
"memory.settings.update.ahead": "当前版本比 registry 还新(本地开发版)",
|
|
420
|
+
"memory.settings.update.error": "检测失败:稍后再试",
|
|
421
|
+
// 检测更新点出的第三种结果(发现新版本);横幅形态已删,它现在只是
|
|
422
|
+
// 按需那一行的文案,占位 {v}/{c} 由 checkResultText 填。
|
|
423
|
+
"memory.settings.version.outdated": "有新版本 {v}(当前运行 {c})",
|
|
393
424
|
"memory.settings.version.outdatedHint": "若安装时指定过版本号,常规升级不会跨大/小版本(pnpm 钉子)——请重新安装或在升级命令加 --latest;npm 已发布而市场暂未收录属正常(约 1 天延迟)。",
|
|
425
|
+
"memory.settings.star": "给项目点个 Star",
|
|
426
|
+
"memory.settings.star.sub": "它对你是好用的,就鼓励一下作者吧 (●'◡'●)",
|
|
394
427
|
"memory.settings.mode.title": "运行模式",
|
|
395
428
|
"memory.settings.mode.desc": "轻量模式只保留核心能力:记忆读写、自动注入、热记忆;巩固、实体抽取、语义搜索等全部关闭,适合低配机器或只想「记住偏好」的场景。标准模式开启全部功能。",
|
|
396
429
|
"memory.settings.mode.light": "轻量",
|
|
@@ -425,30 +458,51 @@ window.__ModuleLoader__.load({
|
|
|
425
458
|
"memory.tab.constraint": "工程约束",
|
|
426
459
|
"memory.tab.document": "文档",
|
|
427
460
|
"memory.features.title": "功能开关",
|
|
428
|
-
"memory.features.desc": "
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
461
|
+
"memory.features.desc": "控制插件在后台做哪些事。先看「常用」里的总开关就够了,其余项收在「全部设置」里。改动即时保存,重启 DSH 后生效。",
|
|
462
|
+
// Issue #427:分组原先按「实现」分(基础/检索与增强/巩固与睡眠/多会话隔离
|
|
463
|
+
// 是架构图的分区),而人真正会问的是四个问题:它会记住什么、它什么时候
|
|
464
|
+
// 写进去、它怎么用回来、不同会话会不会串。组名改成人话,术语留在 hint。
|
|
465
|
+
// group.basic 是首屏层:新用户不读文档就该答得出「会不会自动记」;
|
|
466
|
+
// 「别的会话能不能看到」由同在首屏的 scope 组回答。
|
|
467
|
+
"memory.features.group.basic": "常用(先动这几个)",
|
|
468
|
+
"memory.features.group.core": "它会记住什么",
|
|
469
|
+
// group.mo 是 v0.9.0 新增的独立分组(座位闸门,不是行为开关);渲染
|
|
470
|
+
// 走 t(`memory.features.${g.key}`),缺这行会把键名直接写上屏。
|
|
471
|
+
"memory.features.group.mo": "它的工作流(M.O.)",
|
|
472
|
+
"memory.features.group.enhance": "它什么时候写进去",
|
|
473
|
+
"memory.features.group.dream": "它怎么用回来",
|
|
474
|
+
"memory.features.group.scope": "不同会话之间",
|
|
433
475
|
"memory.features.group.advanced": "高级",
|
|
434
|
-
|
|
476
|
+
// 「全部设置」= 第一层折叠(issue #427 建议 1),常显只留 basic + scope
|
|
477
|
+
// 两组;「高级」是它内部的第二层(进阶 7 项)。
|
|
478
|
+
"memory.features.allToggle": "全部设置",
|
|
479
|
+
"memory.features.advancedToggle": "更少用的 7 个开关",
|
|
435
480
|
"memory.features.restartHint": "重启 DSH 后生效",
|
|
436
481
|
"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.
|
|
482
|
+
"memory.features.saveFailed": "保存失败",
|
|
483
|
+
// Issue #427 建议 2:条目名给「打开会怎样、关掉会怎样」,术语放末尾。
|
|
484
|
+
// 括号里的英文是设置项键名,末尾的「(BM25)」才是术语。
|
|
485
|
+
"memory.features.autoInject": "自动带上旧记忆(每轮都用)",
|
|
486
|
+
"memory.features.autoInject.hint": "开:每轮自动挑相关记忆塞给 Agent,它不用问就能想起;关:Agent 得自己调用查记忆的工具",
|
|
487
|
+
"memory.features.injectGuidanceEnabled": "顺带教 Agent 怎么用记忆",
|
|
488
|
+
"memory.features.injectGuidanceEnabled.hint": "开:在记忆工具的说明里写清什么时候该查、什么时候该写;关:只给工具不给用法",
|
|
489
|
+
"memory.features.continuityRescueEnabled": "长对话被压缩前先存一份快照",
|
|
490
|
+
"memory.features.continuityRescueEnabled.hint": "开:压缩前先存「正在做什么 / 下一步 / 未决问题」,关键上下文活过压缩;关:压缩可能把它压掉",
|
|
491
|
+
"memory.features.parentOff": "上一项关闭时不生效",
|
|
492
|
+
// 「默认 / 已改」徽标:overrides 里没有这个键 = 值来自 config schema 的
|
|
493
|
+
// 默认值,一条命令就能退回。徽标让这件事不必去读配置文件才成立。
|
|
494
|
+
"memory.features.tagDefault": "默认",
|
|
495
|
+
"memory.features.tagDefault.hint": "这一项用的是出厂默认值,没有被你改过",
|
|
496
|
+
"memory.features.tagChanged": "已改",
|
|
497
|
+
"memory.features.tagChanged.hint": "你改过这一项,它不再用默认值",
|
|
498
|
+
"memory.features.autoSummarize": "自动把对话记下来(自动注入之外)",
|
|
499
|
+
"memory.features.autoSummarize.hint": "开:对话结束自动提炼成长期记忆条目(autoSummarize);关:只有你手动存或 Agent 主动存才有记忆",
|
|
500
|
+
"memory.features.hotMemoryEnabled": "顺带带上最近几轮原话",
|
|
501
|
+
"memory.features.hotMemoryEnabled.hint": "开:注入时把最近几轮的对话原文一并带上,Agent 眼前就是刚才聊的内容;关:只带提炼后的长期记忆。这几轮原文不会写进长期记忆(热记忆,默认开)",
|
|
502
|
+
"memory.features.injectTimePrefix": "会话开头告诉它今天几号",
|
|
503
|
+
"memory.features.injectTimePrefix.hint": "开:每个新会话的第一轮注入一次日期与星期,「今天」「上周」这类说法更稳;关:注入里不带时间(时间前缀,默认关)",
|
|
504
|
+
"memory.features.entityExtractionEnabled": "顺手抽出人名、项目和概念",
|
|
505
|
+
"memory.features.entityExtractionEnabled.hint": "开:每条新记忆里抽出人物/项目/概念,关系图谱自己长起来,也能按「谁」找记忆;关:只存整条记忆,不建关系(实体抽取,默认关)",
|
|
452
506
|
"memory.features.entityExtractionProvider": "实体抽取 Provider",
|
|
453
507
|
"memory.features.entityExtractionModel": "实体抽取模型",
|
|
454
508
|
"memory.features.entityExtractionReasoning": "实体抽取思考强度",
|
|
@@ -464,68 +518,143 @@ window.__ModuleLoader__.load({
|
|
|
464
518
|
"memory.features.summarizeReasoningEffort.high": "高",
|
|
465
519
|
"memory.features.summarizeReasoningEffort.hint": "思考型模型建议选「低」或「关闭思考」,避免推理占满输出导致总结失败",
|
|
466
520
|
"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.
|
|
521
|
+
"memory.features.codingRetrospect": "写代码的过程也记下来",
|
|
522
|
+
"memory.features.codingRetrospect.hint": "开:整段编码过程(含报错与工具调用)一并回顾,沉淀踩坑、约束和被否决的方案;关:只提炼对话内容(编码记忆蒸馏,默认关)",
|
|
523
|
+
"memory.features.rerankEnabled": "搜出来的结果再精排一遍",
|
|
524
|
+
"memory.features.rerankEnabled.hint": "开:本地重排模型给召回结果重新打分,更准但更慢;关:直接用检索时的排序(结果重排,默认关)",
|
|
525
|
+
"memory.features.heatEnabled": "越久没用越靠后(像会忘)",
|
|
526
|
+
"memory.features.heatEnabled.hint": "开:像遗忘曲线一样给记忆降温,长期没用过的往后排、不占注入名额;关:一直按相关度排(热度衰减,默认关)",
|
|
527
|
+
"memory.features.documentMemoryEnabled": "长文档只存一张摘要卡",
|
|
528
|
+
"memory.features.documentMemoryEnabled.hint": "开:Agent 写的长文档在记忆里只放「摘要卡」,正文仍由 Agent 保管、按需读取,注入只带摘要;关:整篇都往上下文里带(document 型记忆,默认关)",
|
|
529
|
+
// --- v0.9.0 M.O.(Modus Operandi · 手法,beta)---
|
|
530
|
+
// M.O. = Modus Operandi,拉丁语「一个人一贯的做事方式」;也是 org 词根
|
|
531
|
+
// modus(modusensus)。界面主显示 M.O.,设置项 hint 第一句给全名。
|
|
532
|
+
"memory.actions.beta": "beta",
|
|
533
|
+
"memory.actions.notice": "提炼结果是候选,可能有错、过时、会被推翻。",
|
|
534
|
+
"memory.actions.title": "M.O. · 手法",
|
|
535
|
+
"memory.actions.open": "打开 M.O.",
|
|
536
|
+
"memory.actions.viewFull": "完整视图",
|
|
537
|
+
"memory.actions.openPane": "在右栏打开完整视图",
|
|
538
|
+
"memory.actions.announce.open": "M.O. 面板已打开",
|
|
539
|
+
"memory.actions.announce.close": "M.O. 面板已关闭",
|
|
540
|
+
"memory.actions.close": "关闭",
|
|
541
|
+
"memory.actions.candidates": "候选 {n} 条",
|
|
542
|
+
"memory.actions.runs": "最近运行",
|
|
543
|
+
"memory.actions.runOf": "运行 {n}",
|
|
544
|
+
"memory.actions.run.ok": "成功",
|
|
545
|
+
"memory.actions.run.failed": "失败",
|
|
546
|
+
"memory.actions.run.refuted": "被推翻",
|
|
547
|
+
"memory.actions.state.candidate": "候选",
|
|
548
|
+
"memory.actions.state.trusted": "可信",
|
|
549
|
+
"memory.actions.state.stale": "过时",
|
|
550
|
+
"memory.actions.state.refuted": "被推翻",
|
|
551
|
+
"memory.actions.gate.has": "有闸门证据",
|
|
552
|
+
"memory.actions.gate.none": "还没闸门",
|
|
553
|
+
"memory.actions.gate.tag": "闸",
|
|
554
|
+
"memory.actions.who.main": "主",
|
|
555
|
+
"memory.actions.who.human": "人",
|
|
556
|
+
"memory.actions.who.ci": "CI",
|
|
557
|
+
"memory.actions.ov.done": "完成判据",
|
|
558
|
+
"memory.actions.ov.trigger": "何时触发",
|
|
559
|
+
"memory.actions.band.count": "{n} 步",
|
|
560
|
+
"memory.actions.legend.states": "状态",
|
|
561
|
+
"memory.actions.detail.situation": "情形",
|
|
562
|
+
"memory.actions.detail.action": "动作",
|
|
563
|
+
"memory.actions.detail.why": "为什么",
|
|
564
|
+
"memory.actions.detail.cf": "反事实",
|
|
565
|
+
"memory.actions.detail.noCf": "该节点还没有反事实记录",
|
|
566
|
+
"memory.actions.detail.source": "来源记忆",
|
|
567
|
+
"memory.actions.credit": "复用",
|
|
568
|
+
"memory.actions.creditValue": "{n} 次 · 上次 {d} 天前",
|
|
569
|
+
"memory.actions.dep": "依赖",
|
|
570
|
+
"memory.actions.zoom.value": "缩放 {n}%",
|
|
571
|
+
"memory.actions.zoom.in": "放大",
|
|
572
|
+
"memory.actions.zoom.out": "缩小",
|
|
573
|
+
"memory.actions.zoom.fit": "适应宽度",
|
|
574
|
+
"memory.actions.zoom.hint": "Ctrl/⌘ + 滚轮也能缩放",
|
|
575
|
+
// 缩放百分比指示({n} 由画布工具条填);基线 4568614 起就缺这两行,
|
|
576
|
+
// 工具条上会直接显示键名。
|
|
577
|
+
"memory.actions.list.heading": "这个项目里的工作流",
|
|
578
|
+
"memory.actions.list.count": "工作流 {n} 条",
|
|
579
|
+
"memory.actions.list.meta": "{steps} 步 · {gates} 道闸门 · 反事实 {cf}/{steps}",
|
|
580
|
+
"memory.actions.scope.project": "项目 {name}",
|
|
581
|
+
"memory.actions.scope.global": "全局",
|
|
582
|
+
"memory.actions.scope.none": "项目未知",
|
|
583
|
+
"memory.actions.broken": "有文件读不出来:",
|
|
584
|
+
"memory.actions.empty.title": "这个项目里还没有工作流",
|
|
585
|
+
"memory.actions.empty.body": "工作流的本体住在项目里的 .mneme/pipelines/ 下——跟着仓库走,能被提交、能被 review、能被 CI 读。做法被复用第二次才算节点类型,第一次只是实例;同一路径被走通两次之后,它才会从会话里被提炼出来、落到那个目录里。",
|
|
586
|
+
"memory.actions.noRuns": "这一条还没有运行记录,所以每个节点都还是候选。",
|
|
587
|
+
"memory.actions.settle.title": "本次运行",
|
|
588
|
+
"memory.actions.settle.line": "「{title}」",
|
|
589
|
+
"memory.actions.settle.used": "本次复用 {n} 条做法(勾选为被用上)",
|
|
590
|
+
"memory.actions.settle.refutedWarn": "本次路径里有已被推翻的节点,结果仅供参考",
|
|
591
|
+
"memory.features.actionPipelineEnabled": "M.O.(beta)",
|
|
592
|
+
"memory.features.actionPipelineEnabled.hint": "⚠️ 开发中:目前只有「看」这一层能用,自己沉淀和触发都还没做,建议暂时保持关闭。Modus Operandi(手法):把真实会话里反复走通的「做法」连成一张只读图谱——会话头图标、右栏完整视图、每次运行的结算卡。重启 DSH 后生效",
|
|
593
|
+
"memory.features.resilientModelDownload": "模型下载断了能续上",
|
|
594
|
+
"memory.features.resilientModelDownload.hint": "开:下载中断后从断点续传并自动重试;关:中断就整份重来(断点续传,默认开)",
|
|
595
|
+
"memory.features.searchSemanticDedup": "搜到意思相同的只留一条",
|
|
596
|
+
"memory.features.searchSemanticDedup.hint": "开:检索结果里意思相近的条目合并成一条,少占名额;关:同义条目一起出现(语义去重,默认关)",
|
|
597
|
+
"memory.features.autoDream": "后台自动整理记忆(合并 · 去重)",
|
|
598
|
+
"memory.features.autoDream.hint": "开:后台自动合并、沉淀碎片记忆(autoDream);关:记忆只增不减,越用越散",
|
|
599
|
+
"memory.features.sleepModeEnabled": "你不在的时候它整理记忆",
|
|
600
|
+
"memory.features.sleepModeEnabled.hint": "开:空闲时段做分层压缩与整理,白天少打扰;关:只有你手动触发的整理(睡眠模式,默认关)",
|
|
601
|
+
// #427 建议 5:hybridInject / selectiveInjectEnabled / adaptiveThresholdEnabled
|
|
602
|
+
// 是同一件事「带什么回来」的三档,相邻摆放并在 hint 里点明,不再摊成
|
|
603
|
+
// 三个不相干的并列开关。
|
|
604
|
+
"memory.features.hybridInject": "关键词 + 意思 两路一起找",
|
|
605
|
+
"memory.features.hybridInject.hint": "开:两路检索各取一批再合并,召回更全;关:只用一路。「带什么回来」共三档(混合注入 / 选择性注入 / 自适应阈值),可只开其中一档(混合注入,默认开)",
|
|
606
|
+
"memory.features.selectiveInjectEnabled": "只带真正相关的记忆",
|
|
607
|
+
"memory.features.selectiveInjectEnabled.hint": "开:按相关性筛一遍再注入,上下文更干净;关:带出去的更多也更杂。三档之一(选择性注入,默认开)",
|
|
608
|
+
"memory.features.adaptiveThresholdEnabled": "按检索质量自动调松紧",
|
|
609
|
+
"memory.features.adaptiveThresholdEnabled.hint": "开:召回质量差时放宽门槛、好时收紧;关:门槛固定。三档之一(自适应阈值,默认开)",
|
|
610
|
+
"memory.features.reflectionUpdateEnabled": "整理时允许改已有记忆",
|
|
611
|
+
"memory.features.reflectionUpdateEnabled.hint": "开:巩固可以改写、合并已有条目,不是只增不改;关:旧记忆永不被自动改动(反思更新,默认开)",
|
|
491
612
|
"memory.features.distill": "蒸馏与限流",
|
|
492
613
|
"memory.features.distillRateLimitIntervalMs": "蒸馏放行间隔 (ms)",
|
|
493
614
|
"memory.features.distillRateLimitRetries": "429 重试次数",
|
|
494
615
|
"memory.features.distillRateLimitBaseDelayMs": "退避起始延迟 (ms)",
|
|
495
616
|
"memory.features.distillMaxChars": "转录截断上限(字符)",
|
|
617
|
+
// #399 的两个留存键。此前只有后端白名单、面板够不着,而
|
|
618
|
+
// docs/CONFIGURATION.md 写的是「面板可启停」——补 UI 让文档成真。
|
|
619
|
+
// hint 必须写清两件事:这是**原始对话**落盘(隐私面),以及天数只管
|
|
620
|
+
// 启动期裁剪(reclaim 走另一条 --older-than,不读这个键)。
|
|
621
|
+
"memory.features.distillRecords.enabled": "留存蒸馏原文",
|
|
622
|
+
"memory.features.distillRecords.enabled.hint": "蒸馏时把窗口内的公开对话原文一并落盘,并记下每条记忆的来源区间;私密推理与工具结果不落。关 = 一行不写、连收集都不做",
|
|
623
|
+
"memory.features.distillRecords.retentionDays": "原文保留天数",
|
|
624
|
+
"memory.features.distillRecords.retentionDays.hint": "1–3650 天;只管启动期裁剪。想让 reclaim 也按这个天数回收,需显式传 --older-than 同一数值",
|
|
496
625
|
"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": "
|
|
626
|
+
"memory.features.memoryQualityFilter.enabled": "没价值的记忆自动收起来",
|
|
627
|
+
"memory.features.memoryQualityFilter.enabled.hint": "开:低价值条目自动归档、注入时按质量降权;关:什么都留着,噪声也一起带(记忆质量过滤,默认开)",
|
|
628
|
+
"memory.features.llmAudit.enabled": "记下后台用了多少模型调用",
|
|
629
|
+
"memory.features.llmAudit.enabled.hint": "开:巩固/总结的后台调用与 token 消耗都有账可查;关:后台开销无从核对(调用审计,默认开)",
|
|
501
630
|
// #254:写入准入两层。检测关时整条路径不碰;检测开、拦截关 = 仅告警 +
|
|
502
631
|
// 留审计;两个都开才真拦。文案要写清「只留审计」与「真拦下」的区别,
|
|
503
632
|
// 否则操作者会以为打开检测就已经在拦。
|
|
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": "
|
|
633
|
+
"memory.features.writeAdmission.enabled": "写入前先判一下是不是废话(只记不拦)",
|
|
634
|
+
"memory.features.writeAdmission.enabled.hint": "开:空白/噪声写入判为命中并留一条审计记录,但仍存进去;关:完全不判。要真拦,见下一项(写入准入判定,默认关)",
|
|
635
|
+
"memory.features.writeAdmission.enforce": "真把没用的写入挡下来",
|
|
636
|
+
"memory.features.writeAdmission.enforce.hint": "开:命中直接拒绝写入;关:只留审计、照存不误。需先打开上面那项判定(写入准入拦截,默认关)",
|
|
637
|
+
"memory.features.preInjectGate.enabled": "注入前先筛掉「只是我的看法」的记忆(只记不拦)",
|
|
638
|
+
"memory.features.preInjectGate.enabled.hint": "开:注入前判出意见/立场类记忆并留审计,先观察分布;关:照常注入(注入前判定,默认关)",
|
|
639
|
+
"memory.features.preInjectGate.enforce": "注入时真滤掉判定不合适的",
|
|
640
|
+
"memory.features.preInjectGate.enforce.hint": "开:命中的候选在注入时丢掉;关:只留审计、照常注入。需先打开上面那项判定(注入前判定拦截,默认关)",
|
|
512
641
|
// #164 A2:密钥/PII 判据自己的闸。文案要写清它管的是哪一批判据(与上面的
|
|
513
642
|
// 空白/噪声分开)、它跑在「写入准入判定」里面(那一项关着就不跑),以及
|
|
514
643
|
// 「开着也不拦」——拦是上面那项的事。
|
|
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": "
|
|
644
|
+
"memory.features.sensitiveScanEnabled": "写入前扫一遍密钥和个人信息",
|
|
645
|
+
"memory.features.sensitiveScanEnabled.hint": "开:在写入准入判定里面扫密钥/个人信息,命中只留审计;要真拦,再打开「写入准入拦截」。判定关着时它不跑(密钥/PII 扫描,默认关)",
|
|
646
|
+
"memory.features.bm25SearchEnabled": "也用关键词方式找记忆",
|
|
647
|
+
"memory.features.bm25SearchEnabled.hint": "开:多一个字少一个字也搜得到,专名、报错码、路径更易命中,与「按意思找」互补;关:只按语义相近度(BM25 关键词检索,默认开)",
|
|
648
|
+
"memory.features.conflictFreezeEnabled": "互相矛盾的记忆先冻结",
|
|
649
|
+
"memory.features.conflictFreezeEnabled.hint": "开:整理发现矛盾时先冻结待你确认,不让两条同时生效;关:两边都照用(冲突冻结,默认关)",
|
|
650
|
+
"memory.features.scopeEnabled": "记住每条记忆来自哪个会话",
|
|
651
|
+
"memory.features.scopeEnabled.hint": "开:每条新记忆标上来自哪个 Agent / 工作区,别的会话能不能看到由它说了算;关:新记忆不带来源,所有会话共用一批记忆(作用域标注,默认关)",
|
|
652
|
+
"memory.features.strictScope": "别人设为私有的记忆我看不见",
|
|
653
|
+
"memory.features.strictScope.hint": "开:其他会话明确标私有的记忆对你彻底不可见;关:仍可见、只是排到后面。要彻底隔离请用条目的「敏感度」字段。需先打开上面那项标注(严格隔离,默认关)",
|
|
654
|
+
"memory.features.trustEpistemicWeighting": "按来源可信度调排序",
|
|
655
|
+
"memory.features.trustEpistemicWeighting.hint": "开:可信度高的来源排前面;关:一视同仁(可信度加权,实验性,默认关)",
|
|
656
|
+
"memory.features.reflectionFailureTracking": "整理失败时留下案例",
|
|
657
|
+
"memory.features.reflectionFailureTracking.hint": "开:把失败原因与案例记下来供诊断改进;关:失败不留记录(失败追踪,默认开)",
|
|
529
658
|
"memory.features.embedRoute": "语义检索路线",
|
|
530
659
|
"memory.features.embedProvider": "Embedding 提供方",
|
|
531
660
|
"memory.features.embedProvider.openai": "OpenAI 兼容接口",
|
|
@@ -553,6 +682,7 @@ window.__ModuleLoader__.load({
|
|
|
553
682
|
"memory.features.modelTestOk": "连通正常",
|
|
554
683
|
"memory.features.modelTestFail": "测试失败",
|
|
555
684
|
"memory.features.modelTestHint": "真实发起一次最小巩固调用,验证 Provider/模型连通与 effort 支持",
|
|
685
|
+
"memory.features.modelTestDowngraded": "档位不被该模型接受,已去掉档位连通(与真实巩固一致)",
|
|
556
686
|
"memory.features.routeStaleMark": "(不在可用列表)",
|
|
557
687
|
"memory.features.routeStaleHint": "该值不在当前可用列表(常见于切换过 Provider):请重新选择,或点「测试连通性」当场验证",
|
|
558
688
|
"memory.explorer.viewCards": "卡片",
|
|
@@ -823,6 +953,31 @@ window.__ModuleLoader__.load({
|
|
|
823
953
|
"memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
|
|
824
954
|
"memory.settings.profileSave": "Save Profile",
|
|
825
955
|
"memory.settings.profileSaved": "Profile saved",
|
|
956
|
+
"memory.settings.persona": "Persona (beta)",
|
|
957
|
+
"memory.settings.personaHint": "Give this agent a character card: name, personality, reply style. Injected every turn when the toggle below and the \"Persona\" master switch are both on.",
|
|
958
|
+
"memory.settings.personaName": "Name",
|
|
959
|
+
"memory.settings.personaNamePlaceholder": "e.g. Nyx",
|
|
960
|
+
"memory.settings.personaPersonality": "Personality",
|
|
961
|
+
"memory.settings.personaPersonalityPlaceholder": "e.g. relaxed and humorous; use analogies for hard topics",
|
|
962
|
+
"memory.settings.personaReplyStyle": "Reply style",
|
|
963
|
+
"memory.settings.personaReplyStylePlaceholder": "e.g. conclusion first; plain words over jargon",
|
|
964
|
+
"memory.settings.personaSave": "Save Persona",
|
|
965
|
+
"memory.settings.personaSaved": "Persona saved",
|
|
966
|
+
"memory.settings.personaSaveFailed": "Save failed",
|
|
967
|
+
"memory.settings.personaEnabledToggle": "Enable this card",
|
|
968
|
+
"memory.settings.personaEnabledOn": "On — sessions matching this identity get this card",
|
|
969
|
+
"memory.settings.personaEnabledOff": "Off — sessions with this identity fall back to the default card",
|
|
970
|
+
"memory.settings.personaDefaultCard": "Default (fallback)",
|
|
971
|
+
"memory.settings.personaAdd": "Add card",
|
|
972
|
+
"memory.settings.personaAddPlaceholder": "Agent identity (e.g. nyx) used to address injection",
|
|
973
|
+
"memory.settings.personaEditingDefault": "Editing: default card — used by sessions matching no identity card",
|
|
974
|
+
"memory.settings.personaEditingFor": "Editing identity card:",
|
|
975
|
+
"memory.settings.personaDeleteCard": "Delete this card",
|
|
976
|
+
"memory.settings.personaView": "You, as the agent sees you (injected text)",
|
|
977
|
+
"memory.settings.personaViewHint": "The profile and rules actually injected every turn. Read-only.",
|
|
978
|
+
"memory.settings.personaViewEmpty": "Profile and rules are both empty — the agent only sees the memory store.",
|
|
979
|
+
"memory.features.personaEnabled": "Persona (beta)",
|
|
980
|
+
"memory.features.personaEnabled.hint": "Inject the persona card (Settings → Personalization) into every turn. Off by default (beta); both this switch and the card's own toggle must be on.",
|
|
826
981
|
"memory.settings.rules": "Rules",
|
|
827
982
|
"memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
|
|
828
983
|
"memory.settings.ruleAdd": "Add Rule",
|
|
@@ -845,12 +1000,12 @@ window.__ModuleLoader__.load({
|
|
|
845
1000
|
"memory.explorer.tabStatus": "Status",
|
|
846
1001
|
"memory.entities.count": "{n} entities",
|
|
847
1002
|
"memory.entities.pick": "Select an entity to see its attributes and relations",
|
|
1003
|
+
"memory.entities.noMatch": "No entities match that search — try another word",
|
|
848
1004
|
"memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
|
|
849
1005
|
"memory.entities.attrs": "Attributes",
|
|
850
1006
|
"memory.entities.relations": "Relations",
|
|
851
1007
|
"memory.entities.mentions": "{n} mentions",
|
|
852
1008
|
"memory.entities.lastSeen": "Last seen",
|
|
853
|
-
"memory.entities.graph": "Relation graph",
|
|
854
1009
|
"memory.entity.person": "People",
|
|
855
1010
|
"memory.entity.project": "Projects",
|
|
856
1011
|
"memory.entity.concept": "Concepts",
|
|
@@ -875,8 +1030,6 @@ window.__ModuleLoader__.load({
|
|
|
875
1030
|
"memory.explorer.tags": "Tags",
|
|
876
1031
|
"memory.explorer.importance": "Importance",
|
|
877
1032
|
"memory.explorer.heat": "Heat",
|
|
878
|
-
"memory.explorer.sourceFilter": "Source",
|
|
879
|
-
"memory.explorer.sort": "Sort",
|
|
880
1033
|
"memory.explorer.heatSort": "By heat",
|
|
881
1034
|
"memory.explorer.heatSortHint": "Sort loaded items by heat, in-page (not global; the month tree keeps chronological order)",
|
|
882
1035
|
"memory.explorer.filterDeposited": "Deposited",
|
|
@@ -897,7 +1050,6 @@ window.__ModuleLoader__.load({
|
|
|
897
1050
|
"memory.graph.depth": "hops",
|
|
898
1051
|
"memory.graph.empty": "The graph is waiting for data: entities and relations accumulate as memories are extracted",
|
|
899
1052
|
"memory.graph.notFound": "Entity not found",
|
|
900
|
-
"memory.graph.attrs": "Attributes",
|
|
901
1053
|
"memory.graph.related": "Related memories",
|
|
902
1054
|
"memory.graph.relation": "Relation",
|
|
903
1055
|
"memory.graph.sourceMemory": "View source memory",
|
|
@@ -919,8 +1071,8 @@ window.__ModuleLoader__.load({
|
|
|
919
1071
|
"memory.settings.vectorReindexing": "Indexing…",
|
|
920
1072
|
"memory.settings.vectorReindexDone": "Indexed {n} items",
|
|
921
1073
|
"memory.settings.apiTokenTitle": "API Token (optional)",
|
|
922
|
-
"memory.settings.apiTokenHint": "
|
|
923
|
-
"memory.settings.apiTokenPlaceholder": "
|
|
1074
|
+
"memory.settings.apiTokenHint": "This field only stores the token in this browser (localStorage) for the panel requests. The server-side lock is the plugin\u0027s apiToken config — set both to the same value; this field alone locks nothing.",
|
|
1075
|
+
"memory.settings.apiTokenPlaceholder": "Must match the plugin\u0027s apiToken",
|
|
924
1076
|
"memory.settings.apiTokenSave": "Save Token",
|
|
925
1077
|
"memory.settings.apiTokenSaved": "Token saved",
|
|
926
1078
|
"memory.settings.feedback.title": "Feedback",
|
|
@@ -932,8 +1084,14 @@ window.__ModuleLoader__.load({
|
|
|
932
1084
|
"memory.settings.feedback.browse": "Browse known issues",
|
|
933
1085
|
"memory.settings.feedback.browse.sub": "Check if it's already reported",
|
|
934
1086
|
"memory.settings.feedback.hint": "Search for an existing issue first — it saves a duplicate.",
|
|
935
|
-
"memory.settings.
|
|
1087
|
+
"memory.settings.update.check": "Check for updates",
|
|
1088
|
+
"memory.settings.update.latest": "Up to date ✓",
|
|
1089
|
+
"memory.settings.update.ahead": "Newer than the registry (local build)",
|
|
1090
|
+
"memory.settings.update.error": "Check failed: try again later",
|
|
1091
|
+
"memory.settings.version.outdated": "New version {v} available (running {c})",
|
|
936
1092
|
"memory.settings.version.outdatedHint": "If the plugin was installed with a pinned version, regular upgrades never cross minor/major lines — reinstall or pass --latest. A fresh npm release may take about a day to appear in the market.",
|
|
1093
|
+
"memory.settings.star": "Star the project",
|
|
1094
|
+
"memory.settings.star.sub": "If it serves you well, cheer the author on (●'◡'●)",
|
|
937
1095
|
"memory.settings.mode.title": "Runtime mode",
|
|
938
1096
|
"memory.settings.mode.desc": "Light mode keeps the core loop only: memory read/write, auto-injection and hot memory — consolidation, entity extraction and semantic search are all off. For modest machines or when you just want preferences remembered. Standard mode enables everything.",
|
|
939
1097
|
"memory.settings.mode.light": "Light",
|
|
@@ -968,30 +1126,44 @@ window.__ModuleLoader__.load({
|
|
|
968
1126
|
"memory.tab.constraint": "Constraints",
|
|
969
1127
|
"memory.tab.document": "Documents",
|
|
970
1128
|
"memory.features.title": "Features",
|
|
971
|
-
"memory.features.desc": "Choose what the plugin does in the background. Changes are saved immediately and take effect after restarting DSH.",
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1129
|
+
"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.",
|
|
1130
|
+
// Issue #427: groups used to be implementation slices (the architecture
|
|
1131
|
+
// diagram's partitions). What a person actually asks is four questions —
|
|
1132
|
+
// what gets remembered, when it gets written, how it comes back, and
|
|
1133
|
+
// whether sessions bleed into each other. Terminology stays in the hints.
|
|
1134
|
+
// group.basic is the first-screen tier; the scope group sits there too so
|
|
1135
|
+
// "can other sessions see this?" is answerable without opening anything.
|
|
1136
|
+
"memory.features.group.basic": "Common (start here)",
|
|
1137
|
+
"memory.features.group.core": "What gets remembered",
|
|
1138
|
+
"memory.features.group.mo": "Its workflow (M.O.)",
|
|
1139
|
+
"memory.features.group.enhance": "When it gets written",
|
|
1140
|
+
"memory.features.group.dream": "How it comes back",
|
|
1141
|
+
"memory.features.group.scope": "Between sessions",
|
|
976
1142
|
"memory.features.group.advanced": "Advanced",
|
|
977
|
-
"memory.features.
|
|
1143
|
+
"memory.features.allToggle": "All settings",
|
|
1144
|
+
"memory.features.advancedToggle": "7 rarer switches",
|
|
978
1145
|
"memory.features.restartHint": "Takes effect after restarting DSH",
|
|
979
1146
|
"memory.features.loadFailed": "Failed to load",
|
|
980
|
-
"memory.features.
|
|
981
|
-
"memory.features.autoInject
|
|
982
|
-
"memory.features.
|
|
983
|
-
"memory.features.injectGuidanceEnabled
|
|
984
|
-
"memory.features.
|
|
985
|
-
"memory.features.continuityRescueEnabled
|
|
986
|
-
"memory.features.
|
|
987
|
-
"memory.features.
|
|
988
|
-
"memory.features.
|
|
989
|
-
"memory.features.
|
|
990
|
-
"memory.features.
|
|
991
|
-
"memory.features.
|
|
992
|
-
"memory.features.
|
|
993
|
-
"memory.features.
|
|
994
|
-
"memory.features.
|
|
1147
|
+
"memory.features.saveFailed": "Failed to save",
|
|
1148
|
+
"memory.features.autoInject": "Carry old memories into every turn",
|
|
1149
|
+
"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",
|
|
1150
|
+
"memory.features.injectGuidanceEnabled": "Also teach the agent how to use memory",
|
|
1151
|
+
"memory.features.injectGuidanceEnabled.hint": "On: the memory tools' description says when to search and when to write. Off: the tools ship without guidance",
|
|
1152
|
+
"memory.features.continuityRescueEnabled": "Snapshot before a long chat gets compacted",
|
|
1153
|
+
"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",
|
|
1154
|
+
"memory.features.parentOff": "Inactive while the switch above is off",
|
|
1155
|
+
"memory.features.tagDefault": "default",
|
|
1156
|
+
"memory.features.tagDefault.hint": "This one is at its factory default — you have not changed it",
|
|
1157
|
+
"memory.features.tagChanged": "changed",
|
|
1158
|
+
"memory.features.tagChanged.hint": "You changed this one; it no longer uses the default",
|
|
1159
|
+
"memory.features.autoSummarize": "Turn conversations into memory automatically",
|
|
1160
|
+
"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",
|
|
1161
|
+
"memory.features.hotMemoryEnabled": "Carry the last few turns verbatim",
|
|
1162
|
+
"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)",
|
|
1163
|
+
"memory.features.injectTimePrefix": "Tell it today's date at session start",
|
|
1164
|
+
"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)",
|
|
1165
|
+
"memory.features.entityExtractionEnabled": "Also pull out people, projects and concepts",
|
|
1166
|
+
"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)",
|
|
995
1167
|
"memory.features.entityExtractionProvider": "Entity extraction provider",
|
|
996
1168
|
"memory.features.entityExtractionModel": "Entity extraction model",
|
|
997
1169
|
"memory.features.entityExtractionReasoning": "Entity extraction reasoning",
|
|
@@ -1007,68 +1179,135 @@ window.__ModuleLoader__.load({
|
|
|
1007
1179
|
"memory.features.summarizeReasoningEffort.high": "High",
|
|
1008
1180
|
"memory.features.summarizeReasoningEffort.hint": "For thinking models, prefer Low or No reasoning so thinking cannot fill the output budget and break summaries",
|
|
1009
1181
|
"memory.features.entityExtractionModelHint": "Provider / model empty = follow the main conversation model; reasoning \"Follow default\" = provider default",
|
|
1010
|
-
"memory.features.codingRetrospect": "
|
|
1011
|
-
"memory.features.codingRetrospect.hint": "
|
|
1012
|
-
"memory.features.rerankEnabled": "
|
|
1013
|
-
"memory.features.rerankEnabled.hint": "
|
|
1014
|
-
"memory.features.heatEnabled": "
|
|
1015
|
-
"memory.features.heatEnabled.hint": "
|
|
1016
|
-
"memory.features.documentMemoryEnabled": "
|
|
1017
|
-
"memory.features.documentMemoryEnabled.hint": "
|
|
1018
|
-
|
|
1019
|
-
"memory.
|
|
1020
|
-
"memory.
|
|
1021
|
-
"memory.
|
|
1022
|
-
"memory.
|
|
1023
|
-
"memory.
|
|
1024
|
-
"memory.
|
|
1025
|
-
"memory.
|
|
1026
|
-
"memory.
|
|
1027
|
-
"memory.
|
|
1028
|
-
"memory.
|
|
1029
|
-
"memory.
|
|
1030
|
-
"memory.
|
|
1031
|
-
"memory.
|
|
1032
|
-
"memory.
|
|
1033
|
-
"memory.
|
|
1182
|
+
"memory.features.codingRetrospect": "Remember how the code was written too",
|
|
1183
|
+
"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)",
|
|
1184
|
+
"memory.features.rerankEnabled": "Rerank search results once more",
|
|
1185
|
+
"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)",
|
|
1186
|
+
"memory.features.heatEnabled": "The longer unused, the lower it ranks",
|
|
1187
|
+
"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)",
|
|
1188
|
+
"memory.features.documentMemoryEnabled": "Store long documents as a summary card",
|
|
1189
|
+
"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)",
|
|
1190
|
+
// --- v0.9.0 M.O. / Modus Operandi (beta) ---
|
|
1191
|
+
"memory.actions.beta": "Beta",
|
|
1192
|
+
"memory.actions.notice": "Extracted steps are candidates: they may be wrong, outdated, or refuted.",
|
|
1193
|
+
"memory.actions.title": "Modus Operandi",
|
|
1194
|
+
"memory.actions.open": "Open Modus Operandi",
|
|
1195
|
+
"memory.actions.viewFull": "Full view",
|
|
1196
|
+
"memory.actions.openPane": "Open full view in sidebar",
|
|
1197
|
+
"memory.actions.announce.open": "Modus Operandi panel opened",
|
|
1198
|
+
"memory.actions.announce.close": "Modus Operandi panel closed",
|
|
1199
|
+
"memory.actions.close": "Close",
|
|
1200
|
+
"memory.actions.candidates": "{n} candidates",
|
|
1201
|
+
"memory.actions.runs": "Recent runs",
|
|
1202
|
+
"memory.actions.runOf": "Run {n}",
|
|
1203
|
+
"memory.actions.run.ok": "ok",
|
|
1204
|
+
"memory.actions.run.failed": "failed",
|
|
1205
|
+
"memory.actions.run.refuted": "refuted",
|
|
1206
|
+
"memory.actions.state.candidate": "candidate",
|
|
1207
|
+
"memory.actions.state.trusted": "trusted",
|
|
1208
|
+
"memory.actions.state.stale": "stale",
|
|
1209
|
+
"memory.actions.state.refuted": "refuted",
|
|
1210
|
+
"memory.actions.gate.has": "has counterfactual",
|
|
1211
|
+
"memory.actions.gate.none": "no gate yet",
|
|
1212
|
+
"memory.actions.gate.tag": "GATE",
|
|
1213
|
+
"memory.actions.who.main": "main",
|
|
1214
|
+
"memory.actions.who.human": "human",
|
|
1215
|
+
"memory.actions.who.ci": "CI",
|
|
1216
|
+
"memory.actions.ov.done": "Done when",
|
|
1217
|
+
"memory.actions.ov.trigger": "Triggered by",
|
|
1218
|
+
"memory.actions.band.count": "{n} steps",
|
|
1219
|
+
"memory.actions.legend.states": "States",
|
|
1220
|
+
"memory.actions.detail.situation": "Situation",
|
|
1221
|
+
"memory.actions.detail.action": "Action",
|
|
1222
|
+
"memory.actions.detail.why": "Why",
|
|
1223
|
+
"memory.actions.detail.cf": "Counterfactual",
|
|
1224
|
+
"memory.actions.detail.noCf": "No counterfactual recorded yet",
|
|
1225
|
+
"memory.actions.detail.source": "Source memories",
|
|
1226
|
+
"memory.actions.credit": "Reuse",
|
|
1227
|
+
"memory.actions.creditValue": "{n} uses · last {d}d ago",
|
|
1228
|
+
"memory.actions.dep": "depends on",
|
|
1229
|
+
"memory.actions.zoom.value": "Zoom {n}%",
|
|
1230
|
+
"memory.actions.zoom.in": "Zoom in",
|
|
1231
|
+
"memory.actions.zoom.out": "Zoom out",
|
|
1232
|
+
"memory.actions.zoom.fit": "Fit width",
|
|
1233
|
+
"memory.actions.zoom.hint": "Ctrl/⌘ + wheel also zooms",
|
|
1234
|
+
"memory.actions.list.heading": "Workflows in this project",
|
|
1235
|
+
"memory.actions.list.count": "{n} workflow(s)",
|
|
1236
|
+
"memory.actions.list.meta": "{steps} steps · {gates} gates · counterfactual {cf}/{steps}",
|
|
1237
|
+
"memory.actions.scope.project": "project {name}",
|
|
1238
|
+
"memory.actions.scope.global": "global",
|
|
1239
|
+
"memory.actions.scope.none": "project unknown",
|
|
1240
|
+
"memory.actions.broken": "Some files could not be read:",
|
|
1241
|
+
"memory.actions.empty.title": "No workflows in this project yet",
|
|
1242
|
+
"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.",
|
|
1243
|
+
"memory.actions.noRuns": "No runs recorded for this workflow yet, so every step is still a candidate.",
|
|
1244
|
+
"memory.actions.settle.title": "This run",
|
|
1245
|
+
"memory.actions.settle.line": "\"{title}\"",
|
|
1246
|
+
"memory.actions.settle.used": "Reused {n} steps this run (checked = actually used)",
|
|
1247
|
+
"memory.actions.settle.refutedWarn": "This path contains refuted steps; take the result with caution",
|
|
1248
|
+
"memory.features.actionPipelineEnabled": "M.O. (beta)",
|
|
1249
|
+
"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",
|
|
1250
|
+
"memory.features.resilientModelDownload": "Model downloads resume after interruption",
|
|
1251
|
+
"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)",
|
|
1252
|
+
"memory.features.searchSemanticDedup": "Keep one entry when search hits near-duplicates",
|
|
1253
|
+
"memory.features.searchSemanticDedup.hint": "On: near-duplicate entries merge into one, saving injection slots. Off: duplicates all show up (semantic dedup, off by default)",
|
|
1254
|
+
"memory.features.autoDream": "Tidy memories in the background (merge · dedupe)",
|
|
1255
|
+
"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",
|
|
1256
|
+
"memory.features.sleepModeEnabled": "It tidies memories while you are away",
|
|
1257
|
+
"memory.features.sleepModeEnabled.hint": "On: layered compaction and tidying happen during idle periods. Off: only consolidation you trigger manually (sleep mode, off by default)",
|
|
1258
|
+
// #427 suggestion 5: hybridInject / selectiveInjectEnabled /
|
|
1259
|
+
// adaptiveThresholdEnabled are three dials of one question — what comes
|
|
1260
|
+
// back. Adjacent placement plus a hint that names the other two.
|
|
1261
|
+
"memory.features.hybridInject": "Search by keywords and meaning together",
|
|
1262
|
+
"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)",
|
|
1263
|
+
"memory.features.selectiveInjectEnabled": "Only carry memories that truly matter",
|
|
1264
|
+
"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)",
|
|
1265
|
+
"memory.features.adaptiveThresholdEnabled": "Tighten or loosen by retrieval quality",
|
|
1266
|
+
"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)",
|
|
1267
|
+
"memory.features.reflectionUpdateEnabled": "Let tidying revise existing memories",
|
|
1268
|
+
"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)",
|
|
1034
1269
|
"memory.features.distill": "Distillation & rate limiting",
|
|
1035
1270
|
"memory.features.distillRateLimitIntervalMs": "Distill spacing (ms)",
|
|
1036
1271
|
"memory.features.distillRateLimitRetries": "429 retries",
|
|
1037
1272
|
"memory.features.distillRateLimitBaseDelayMs": "Backoff base delay (ms)",
|
|
1038
1273
|
"memory.features.distillMaxChars": "Transcript cap (chars)",
|
|
1274
|
+
"memory.features.distillRecords.enabled": "Keep distilled transcripts",
|
|
1275
|
+
"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",
|
|
1276
|
+
"memory.features.distillRecords.retentionDays": "Transcript retention (days)",
|
|
1277
|
+
"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",
|
|
1039
1278
|
"memory.features.codingBoostFactor": "Coding boost factor",
|
|
1040
|
-
"memory.features.memoryQualityFilter.enabled": "
|
|
1041
|
-
"memory.features.memoryQualityFilter.enabled.hint": "
|
|
1042
|
-
"memory.features.llmAudit.enabled": "
|
|
1043
|
-
"memory.features.llmAudit.enabled.hint": "
|
|
1279
|
+
"memory.features.memoryQualityFilter.enabled": "Tuck low-value memories away automatically",
|
|
1280
|
+
"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)",
|
|
1281
|
+
"memory.features.llmAudit.enabled": "Keep tabs on background model usage",
|
|
1282
|
+
"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)",
|
|
1044
1283
|
// #254 — two layers: enabled runs the check (audit only), enforce actually
|
|
1045
1284
|
// blocks. The hints must keep that difference explicit.
|
|
1046
|
-
"memory.features.writeAdmission.enabled": "
|
|
1047
|
-
"memory.features.writeAdmission.enabled.hint": "
|
|
1048
|
-
"memory.features.writeAdmission.enforce": "
|
|
1049
|
-
"memory.features.writeAdmission.enforce.hint": "
|
|
1050
|
-
"memory.features.preInjectGate.enabled": "
|
|
1051
|
-
"memory.features.preInjectGate.enabled.hint": "
|
|
1052
|
-
"memory.features.preInjectGate.enforce": "
|
|
1053
|
-
"memory.features.preInjectGate.enforce.hint": "
|
|
1285
|
+
"memory.features.writeAdmission.enabled": "Judge blank / noise writes before storing (audit only)",
|
|
1286
|
+
"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)",
|
|
1287
|
+
"memory.features.writeAdmission.enforce": "Really reject useless writes",
|
|
1288
|
+
"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)",
|
|
1289
|
+
"memory.features.preInjectGate.enabled": "Screen out \"just my opinion\" memories before injection (audit only)",
|
|
1290
|
+
"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)",
|
|
1291
|
+
"memory.features.preInjectGate.enforce": "Really drop flagged candidates at injection",
|
|
1292
|
+
"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)",
|
|
1054
1293
|
// #164 A2: its own gate for the secret / PII rules (separate from the
|
|
1055
1294
|
// blank/noise pair above), and it runs inside the write admission checks
|
|
1056
1295
|
// above, so with those off it never runs. Hits are audited only;
|
|
1057
1296
|
// enforcement stays with the key above.
|
|
1058
|
-
"memory.features.sensitiveScanEnabled": "
|
|
1059
|
-
"memory.features.sensitiveScanEnabled.hint": "
|
|
1060
|
-
"memory.features.bm25SearchEnabled": "
|
|
1061
|
-
"memory.features.bm25SearchEnabled.hint": "
|
|
1062
|
-
"memory.features.conflictFreezeEnabled": "
|
|
1063
|
-
"memory.features.conflictFreezeEnabled.hint": "
|
|
1064
|
-
"memory.features.scopeEnabled": "
|
|
1065
|
-
"memory.features.scopeEnabled.hint": "
|
|
1066
|
-
"memory.features.strictScope": "
|
|
1067
|
-
"memory.features.strictScope.hint": "
|
|
1068
|
-
"memory.features.trustEpistemicWeighting": "
|
|
1069
|
-
"memory.features.trustEpistemicWeighting.hint": "
|
|
1070
|
-
"memory.features.reflectionFailureTracking": "
|
|
1071
|
-
"memory.features.reflectionFailureTracking.hint": "
|
|
1297
|
+
"memory.features.sensitiveScanEnabled": "Scan writes for credentials and personal data",
|
|
1298
|
+
"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)",
|
|
1299
|
+
"memory.features.bm25SearchEnabled": "Also search by exact keywords",
|
|
1300
|
+
"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)",
|
|
1301
|
+
"memory.features.conflictFreezeEnabled": "Freeze contradictory memories first",
|
|
1302
|
+
"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)",
|
|
1303
|
+
"memory.features.scopeEnabled": "Record which session each memory came from",
|
|
1304
|
+
"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)",
|
|
1305
|
+
"memory.features.strictScope": "Memories others marked private stay invisible to me",
|
|
1306
|
+
"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)",
|
|
1307
|
+
"memory.features.trustEpistemicWeighting": "Rank by source credibility",
|
|
1308
|
+
"memory.features.trustEpistemicWeighting.hint": "On: more credible sources rank higher. Off: everything counts the same (credibility weighting, experimental, off by default)",
|
|
1309
|
+
"memory.features.reflectionFailureTracking": "Keep failed tidying cases on record",
|
|
1310
|
+
"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)",
|
|
1072
1311
|
"memory.features.embedRoute": "Semantic search route",
|
|
1073
1312
|
"memory.features.embedProvider": "Embedding provider",
|
|
1074
1313
|
"memory.features.embedProvider.openai": "OpenAI-compatible API",
|
|
@@ -1096,6 +1335,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1335
|
"memory.features.modelTestOk": "Connected",
|
|
1097
1336
|
"memory.features.modelTestFail": "Test failed",
|
|
1098
1337
|
"memory.features.modelTestHint": "Fires one minimal consolidation call to verify provider/model connectivity and effort support",
|
|
1338
|
+
"memory.features.modelTestDowngraded": "Effort not accepted by this model; connected without it (same as a real consolidation)",
|
|
1099
1339
|
"memory.features.routeStaleMark": " (not in list)",
|
|
1100
1340
|
"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",
|
|
1101
1341
|
"memory.explorer.viewCards": "Cards",
|
|
@@ -1467,17 +1707,26 @@ window.__ModuleLoader__.load({
|
|
|
1467
1707
|
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
1468
1708
|
// --- three-column browse layout: hairline separators, no outer box ---
|
|
1469
1709
|
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
1470
|
-
".mneme-xside{flex:none;width:
|
|
1471
|
-
|
|
1710
|
+
".mneme-xside{flex:none;width:264px;min-width:0;min-height:0;overflow-y:auto;overflow-x:hidden;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
|
|
1711
|
+
// 左栏下段=重要性+来源+热度:紧跟检索段之后(复验修订:不再钉底),
|
|
1712
|
+
// 顶部细线保留分段感;栏宽 264px 让 4 枚星级 chip 单行排布。
|
|
1713
|
+
".mneme-xside-imp{flex:none;margin-top:10px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:8px}",
|
|
1714
|
+
// 内容区列容器 + 顶部工具条:类型 chips 横排 + 计数/刷新靠右;窄容器靠 wrap 换行。
|
|
1715
|
+
".mneme-xcontent{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
1716
|
+
".mneme-xbrowsebar{flex:none;display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
1717
|
+
// 窄容器(better-sidebar 工作台 ~500px):左栏收窄保内容区可用宽度。
|
|
1718
|
+
"@container (max-width: 640px){.mneme-xside{width:216px}}",
|
|
1719
|
+
// 类型筛选原为第二侧栏的竖排列表(三栏布局遗留,#面板两栏改版):
|
|
1720
|
+
// 两栏化后收进内容区工具条,改为自适应横排 chips,彩点与计数保留。
|
|
1721
|
+
".mneme-xtype{display:inline-flex;justify-content:flex-start;align-items:center;gap:7px;padding:4px 9px;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}",
|
|
1472
1722
|
".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
1473
1723
|
".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
1474
1724
|
".mneme-xsearch{width:100%}",
|
|
1475
1725
|
".mneme-xselect{flex:1;min-width:0}",
|
|
1476
1726
|
".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--mneme-fg-dim);padding:2px 8px 8px}",
|
|
1477
|
-
".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}",
|
|
1478
1727
|
".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1479
1728
|
".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
1480
|
-
".mneme-xcount2{flex:none;
|
|
1729
|
+
".mneme-xcount2{flex:none;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
1481
1730
|
".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}",
|
|
1482
1731
|
".mneme-xmonth:first-child{margin-top:0}",
|
|
1483
1732
|
".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
@@ -1519,20 +1768,26 @@ window.__ModuleLoader__.load({
|
|
|
1519
1768
|
// 虚线态靠 dash 图案区分,不再叠 opacity:50% 的描边再乘 .7 后浅色画布上
|
|
1520
1769
|
// 只剩 2.28,低于图形要求的 3:1(图案已经承担了「没有 memory_id」这个语义)。
|
|
1521
1770
|
".mneme-gedge-dashed{stroke-dasharray:5 4}",
|
|
1522
|
-
|
|
1523
|
-
|
|
1771
|
+
// 图谱与图内侧卡的共同父容器:侧卡改浮层(#面板实体页去冗余)后需要
|
|
1772
|
+
// relative 锚点,否则 absolute 会逃逸到 .mneme-ent 根。
|
|
1773
|
+
".mneme-entgraphwrap{position:relative;min-width:0}",
|
|
1774
|
+
// 侧卡从「SVG 下方文档流堆叠」(把图挤成邮票)改为图内左下浮层:
|
|
1775
|
+
// 反应面板与点击焦点同位,SVG 不再被推挤;背景用不透明 bg-layer-1,
|
|
1776
|
+
// 浮层下透出的图形不参与文字对比度计算。
|
|
1777
|
+
".mneme-graphside{position:absolute;left:12px;bottom:12px;width:min(300px,60%);max-height:60%;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;z-index:2}",
|
|
1524
1778
|
".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}",
|
|
1525
1779
|
".mneme-gs-meta{font-size:13px;color:var(--mneme-fg-dim);margin-bottom:6px}",
|
|
1526
|
-
".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
|
|
1527
|
-
".mneme-gs-attrkey{flex:none;color:var(--mneme-fg-dim);font-size:13px}",
|
|
1528
|
-
".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
|
|
1529
1780
|
".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}",
|
|
1530
1781
|
".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1531
1782
|
// --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
|
|
1532
1783
|
".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
1533
|
-
".mneme-entrail{flex:none;width:224px;min-height:0;overflow
|
|
1534
|
-
|
|
1535
|
-
|
|
1784
|
+
".mneme-entrail{flex:none;width:224px;min-height:0;overflow:hidden;padding:12px 0 0;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column}",
|
|
1785
|
+
// 复验修订:搜索与计数固定在顶部不随列表滚动,列表独立滚动;
|
|
1786
|
+
// 类型分组头 sticky——长列表滚动时分组归属始终可见。
|
|
1787
|
+
".mneme-entrailhead{flex:none;padding:0 12px 10px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
1788
|
+
".mneme-entrailbody{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:4px 12px 12px;display:flex;flex-direction:column;gap:4px}",
|
|
1789
|
+
".mneme-enttype{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:8px 8px 2px;border: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}",
|
|
1790
|
+
".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;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}",
|
|
1536
1791
|
".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1537
1792
|
".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
|
|
1538
1793
|
".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
@@ -1668,6 +1923,11 @@ window.__ModuleLoader__.load({
|
|
|
1668
1923
|
".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)}",
|
|
1669
1924
|
".mneme-badge--archived{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1670
1925
|
".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)}",
|
|
1926
|
+
// 「默认 / 已改」徽标(#427):default 复用 --archived 的语义色组合
|
|
1927
|
+
// (fg-dim on interactive-bg-hover),changed 复用 --scope 的 accent 组
|
|
1928
|
+
// 合——两组配对都已过 contrast.test.js 的重算,不再发明新颜色。
|
|
1929
|
+
".mneme-badge--default{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
1930
|
+
".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)}",
|
|
1671
1931
|
// 冲突集中处理队列(v0.8.0):状态页的待确认冲突列表
|
|
1672
1932
|
".mneme-conflictq{display:flex;flex-direction:column;gap:10px}",
|
|
1673
1933
|
".mneme-conflictq-head{display:flex;align-items:center;gap:8px}",
|
|
@@ -1832,8 +2092,14 @@ window.__ModuleLoader__.load({
|
|
|
1832
2092
|
".mneme-barcol:hover .mneme-bar{opacity:.8}",
|
|
1833
2093
|
".mneme-barlabels{display:flex;gap:8px;padding:6px 2px 0}",
|
|
1834
2094
|
".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}",
|
|
1835
|
-
|
|
1836
|
-
|
|
2095
|
+
// GitHub 贡献图式紧凑网格(复验第三轮「间距太大」):11px 格 + 2px 缝;
|
|
2096
|
+
// heatrows 是左侧星期标签列(一/三/五),heatmonths 是顶部月份标签行
|
|
2097
|
+
// (列主序网格下月份行须与格子列对齐,所以同用 auto-flow:column)。
|
|
2098
|
+
".mneme-heatgrid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,11px);gap:2px;margin-top:12px}",
|
|
2099
|
+
".mneme-heatcell{width:11px;height:11px;border-radius:2px;background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2100
|
+
".mneme-heatwrap{display:flex;gap:4px}",
|
|
2101
|
+
".mneme-heatrows{display:grid;grid-template-rows:repeat(7,11px);gap:2px;margin-top:12px;font-size:9.5px;line-height:11px;color:var(--mneme-fg-dim);text-align:right}",
|
|
2102
|
+
".mneme-heatmonths{display:grid;grid-auto-flow:column;grid-auto-columns:11px;gap:2px;margin-top:0;font-size:9.5px;line-height:14px;color:var(--mneme-fg-dim)}",
|
|
1837
2103
|
// 类名编号=视觉档位(l1 最浅…l4 最深),变量编号=business 占比降序,故交叉引用。
|
|
1838
2104
|
".mneme-heatcell.l1{background:var(--mneme-heat-4)}",
|
|
1839
2105
|
".mneme-heatcell.l2{background:var(--mneme-heat-3)}",
|
|
@@ -1898,7 +2164,159 @@ window.__ModuleLoader__.load({
|
|
|
1898
2164
|
".mneme-maintwarnhead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-danger)}",
|
|
1899
2165
|
".mneme-maintresult{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
1900
2166
|
".mneme-maintresulthead{font-size:13px;line-height:19px;font-weight:600;color:var(--mneme-ok)}",
|
|
1901
|
-
".mneme-mainterr{margin-top:10px;font-size:12px;line-height:17px;color:var(--mneme-danger)}"
|
|
2167
|
+
".mneme-mainterr{margin-top:10px;font-size:12px;line-height:17px;color:var(--mneme-danger)}",
|
|
2168
|
+
// --- v0.9.0 M.O.(beta)---------------------------------------------
|
|
2169
|
+
// 语义色层第二份:浮层面板(portal 到 body)、右栏 pane、退化 sheet、
|
|
2170
|
+
// 对话行结算卡都不在原四个渲染根之下。contrast.test.js 对原四根选择器
|
|
2171
|
+
// 做精确匹配锁,所以这里另起一条规则、取值逐字相同,且必须排在其后
|
|
2172
|
+
// (测试按首次出现抓取 --mneme-* 定义)。引用的宿主 token 全部在
|
|
2173
|
+
// test/contrast.test.js 的 HOST_TOKENS 快照里,不许新增名字。
|
|
2174
|
+
".mneme-pipe,.mneme-pipeview,.mneme-pipe-settle,.mneme-pipe-root{" +
|
|
2175
|
+
"--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
|
|
2176
|
+
"--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
2177
|
+
"--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
2178
|
+
"--mneme-danger:color-mix(in srgb,var(--dsw-alias-state-error-primary) 80%,var(--dsw-alias-label-primary));" +
|
|
2179
|
+
"--mneme-warn:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 65%,var(--dsw-alias-label-primary));" +
|
|
2180
|
+
"--mneme-ok:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 65%,var(--dsw-alias-label-primary))" +
|
|
2181
|
+
"}",
|
|
2182
|
+
// 会话头 trigger:与 Agent Teams 同规格(28px 行高、12px 字号),
|
|
2183
|
+
// 页头窄容器下文字标签隐藏、降级纯图标。
|
|
2184
|
+
".mneme-pipe-root{position:relative;display:inline-flex}",
|
|
2185
|
+
".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}",
|
|
2186
|
+
".mneme-pipe-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2187
|
+
".mneme-pipe-trigger:focus-visible{color:var(--dsw-alias-label-primary);outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2188
|
+
".mneme-pipe-triggerLabel{white-space:nowrap}",
|
|
2189
|
+
"@container (width<=480px){.mneme-pipe-triggerLabel{display:none}}",
|
|
2190
|
+
".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}",
|
|
2191
|
+
// 浮层面板:几何抄 Agent Teams(min(500px,100vw-32px) / 12px 圆角 /
|
|
2192
|
+
// fixed 定位由 useAnchoredPosition 给出)。阴影用中性 rgba:宿主的
|
|
2193
|
+
// elevation token 不在面板 token 快照白名单里,不能引用。
|
|
2194
|
+
".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)}",
|
|
2195
|
+
".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)}",
|
|
2196
|
+
".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}",
|
|
2197
|
+
".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}",
|
|
2198
|
+
// 内测免责:界面常驻一行,「候选」是可见状态词不是 tooltip。
|
|
2199
|
+
".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)}",
|
|
2200
|
+
".mneme-pipe-scope{flex:none;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}",
|
|
2201
|
+
// 运行档位:一次运行一个芯片;失败/被推翻的运行照常列出(留痕)。
|
|
2202
|
+
".mneme-pipe-runs{flex:none;display:flex;gap:6px;flex-wrap:wrap;padding:6px 14px 2px}",
|
|
2203
|
+
".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}",
|
|
2204
|
+
".mneme-pipe-run:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2205
|
+
".mneme-pipe-run:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2206
|
+
".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)}",
|
|
2207
|
+
".mneme-pipe-runDot{flex:none;width:8px;height:8px;border-radius:4px}",
|
|
2208
|
+
".mneme-pipe-runDot--ok{background:var(--mneme-ok)}",
|
|
2209
|
+
".mneme-pipe-runDot--failed{background:var(--mneme-warn)}",
|
|
2210
|
+
".mneme-pipe-runDot--refuted{background:var(--mneme-danger)}",
|
|
2211
|
+
// 图:宽态 SVG 分层 DAG(trace viewer 读法),窄态整块换成竖排列表。
|
|
2212
|
+
// 高度下限:flex:1 的基准是 0,只要某一层祖先没有确定高度(浮层与居中 sheet 都是 max-height
|
|
2213
|
+
// 封顶),分到的就是 0 像素——画布「在 DOM 里、但看不见」。下限不修复成因,只保证最坏情况下
|
|
2214
|
+
// 还能看见图;这跟上面那条撞车是两件事,别把任何一条当成另一条的修复。
|
|
2215
|
+
".mneme-pipe-graph{flex:1;min-height:220px;overflow:auto;padding:4px 8px 2px}",
|
|
2216
|
+
// 缩放条与缩放容器。画布本身是 width:100% 的 SVG,所以「缩放」= 改外层容器的百分比宽度:
|
|
2217
|
+
// 100% 就是适应栏宽,放大则容器比栏宽,滚动条自己出来——平移不必另写一套。
|
|
2218
|
+
".mneme-pipe-zoombar{flex:none;display:flex;align-items:center;gap:6px;padding:5px 12px 0;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2219
|
+
".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}",
|
|
2220
|
+
".mneme-pipe-zoombtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2221
|
+
".mneme-pipe-zoombtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2222
|
+
".mneme-pipe-zoomval{min-width:40px;text-align:right;font-variant-numeric:tabular-nums}",
|
|
2223
|
+
".mneme-pipe-zoomwrap{min-width:0}",
|
|
2224
|
+
".mneme-pipe-svg{display:block;width:100%;height:auto}",
|
|
2225
|
+
".mneme-pipe-edge{stroke:var(--mneme-graphic-dim);fill:none;stroke-width:1.5}",
|
|
2226
|
+
".mneme-pipe-edgeOn{stroke:var(--mneme-accent);fill:none;stroke-width:2.5}",
|
|
2227
|
+
".mneme-pipe-edgeFail{stroke:var(--mneme-danger);fill:none;stroke-width:2;stroke-dasharray:5 3}",
|
|
2228
|
+
// 节点形状按 kind 区分(step 矩形 / gate 菱形 / branch 圆角 / artifact 胶囊),
|
|
2229
|
+
// 状态走描边+填充:refuted 是全篇核心信号,双份危险色最扎眼。
|
|
2230
|
+
".mneme-pipe-node{fill:var(--dsw-alias-bg-layer-2);stroke:var(--mneme-graphic-dim);stroke-width:1.2}",
|
|
2231
|
+
".mneme-pipe-node--trusted{stroke:var(--mneme-ok);stroke-width:1.6}",
|
|
2232
|
+
".mneme-pipe-node--candidate{stroke:var(--mneme-graphic-dim)}",
|
|
2233
|
+
".mneme-pipe-node--stale{stroke:var(--dsw-alias-border-l2);stroke-dasharray:3 2;fill:var(--dsw-alias-bg-base)}",
|
|
2234
|
+
".mneme-pipe-node--refuted{stroke:var(--mneme-danger);stroke-width:2;fill:color-mix(in srgb,var(--mneme-danger) 10%,transparent)}",
|
|
2235
|
+
".mneme-pipe-node--onpath{stroke:var(--dsw-alias-state-business-primary);stroke-width:2}",
|
|
2236
|
+
".mneme-pipe-nodeT{fill:var(--dsw-alias-label-primary);font-size:11px;font-weight:600}",
|
|
2237
|
+
".mneme-pipe-nodeS{fill:var(--mneme-fg-dim);font-size:9.5px}",
|
|
2238
|
+
".mneme-pipe-nodeK{fill:var(--mneme-fg-dim);font-size:9px}",
|
|
2239
|
+
// 图例:四个状态 + 闸门有无(counterfactual null = 空心圈「还没闸门」)。
|
|
2240
|
+
".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)}",
|
|
2241
|
+
".mneme-pipe-pill{flex:none;border-radius:6px;padding:0 6px;font-size:11px;line-height:18px;font-weight:600;border:1px solid transparent}",
|
|
2242
|
+
".mneme-pipe-pill--candidate{color:var(--mneme-fg-dim);border-color:var(--dsw-alias-border-l2)}",
|
|
2243
|
+
".mneme-pipe-pill--trusted{color:var(--mneme-ok);border-color:color-mix(in srgb,var(--mneme-ok) 45%,transparent)}",
|
|
2244
|
+
".mneme-pipe-pill--stale{color:var(--mneme-fg-dim);border-color:var(--dsw-alias-border-l2);border-style:dashed}",
|
|
2245
|
+
".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)}",
|
|
2246
|
+
".mneme-pipe-gate{flex:none;width:10px;height:10px;border-radius:5px;border:1.5px solid var(--mneme-graphic-dim)}",
|
|
2247
|
+
".mneme-pipe-gate--has{border-color:var(--mneme-ok);background:var(--mneme-ok)}",
|
|
2248
|
+
// 阶段带(2026-10-16):段底用宿主 hover 层而不是自造色——对比度锁不认识
|
|
2249
|
+
// 「SVG 文字底下还有一层 rect」,所以段底必须淡到不参与文字对比度。
|
|
2250
|
+
".mneme-pipe-bandbg{fill:var(--dsw-alias-interactive-bg-hover)}",
|
|
2251
|
+
".mneme-pipe-bandline{stroke:var(--dsw-alias-border-l2);stroke-width:1}",
|
|
2252
|
+
".mneme-pipe-bandT{fill:var(--dsw-alias-label-primary);font-size:12px;font-weight:600}",
|
|
2253
|
+
// 闸门节点与风险条:颜色只做冗余,信息本体在文本(闸 / R0-R3)里——
|
|
2254
|
+
// 不许靠单一色相区分(AGENTS.md 自查清单第 9 条)。
|
|
2255
|
+
".mneme-pipe-node--gate{stroke:var(--mneme-warn);stroke-width:2}",
|
|
2256
|
+
".mneme-pipe-gatetag{fill:var(--mneme-warn);font-size:9px;font-weight:600}",
|
|
2257
|
+
".mneme-pipe-risk--r0{stroke:var(--mneme-graphic-dim)}",
|
|
2258
|
+
".mneme-pipe-risk--r1{stroke:var(--mneme-accent)}",
|
|
2259
|
+
".mneme-pipe-risk--r2{stroke:var(--mneme-warn)}",
|
|
2260
|
+
".mneme-pipe-risk--r3{stroke:var(--mneme-danger)}",
|
|
2261
|
+
// 流程概览带:读投影里 pipeline.done 与 pipeline.trigger,回答「这条流程
|
|
2262
|
+
// 在干什么、何时起跑」。正文用 label-secondary(与节点详情屏同档)。
|
|
2263
|
+
".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}",
|
|
2264
|
+
// 节点详情(宽态点选展开 / 窄态 details 手风琴共用)。
|
|
2265
|
+
".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}",
|
|
2266
|
+
".mneme-pipe-detailTitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:2px}",
|
|
2267
|
+
".mneme-pipe-dt{font-weight:600;color:var(--mneme-fg-dim)}",
|
|
2268
|
+
".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)}",
|
|
2269
|
+
".mneme-pipe-cf--none{background:0 0;border:1px dashed var(--dsw-alias-border-l2);color:var(--mneme-fg-dim)}",
|
|
2270
|
+
// 窄态(better-sidebar ~500px / 浮层内):节点竖排列表化,边收成
|
|
2271
|
+
// 左侧轨道线 + 「依赖」芯片——换布局,不是缩放。
|
|
2272
|
+
".mneme-pipe-list{flex:1;min-height:180px;overflow:auto;padding:4px 12px 8px}",
|
|
2273
|
+
".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}",
|
|
2274
|
+
".mneme-pipe-item::before{content:\"\";position:absolute;left:13px;top:-10px;height:10px;border-left:2px solid var(--mneme-graphic-dim)}",
|
|
2275
|
+
".mneme-pipe-item:first-child::before{display:none}",
|
|
2276
|
+
".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)}",
|
|
2277
|
+
".mneme-pipe-itemDot--trusted{border-color:var(--mneme-ok)}",
|
|
2278
|
+
".mneme-pipe-itemDot--stale{border-style:dashed;border-color:var(--dsw-alias-border-l2)}",
|
|
2279
|
+
".mneme-pipe-itemDot--refuted{border-color:var(--mneme-danger);background:var(--mneme-danger)}",
|
|
2280
|
+
".mneme-pipe-itemDot--onpath{border-color:var(--dsw-alias-state-business-primary)}",
|
|
2281
|
+
".mneme-pipe-itemName{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary)}",
|
|
2282
|
+
".mneme-pipe-itemName--refuted{color:var(--mneme-danger)}",
|
|
2283
|
+
".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)}",
|
|
2284
|
+
".mneme-pipe-dep{font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2285
|
+
".mneme-pipe-item summary{cursor:pointer;list-style:none}",
|
|
2286
|
+
".mneme-pipe-item summary::-webkit-details-marker{display:none}",
|
|
2287
|
+
".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)}",
|
|
2288
|
+
// 对话行结算卡(turnTail):死数据,跟这次运行绑死。
|
|
2289
|
+
".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}",
|
|
2290
|
+
".mneme-pipe-settleHead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px}",
|
|
2291
|
+
".mneme-pipe-settleTitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;gap:6px}",
|
|
2292
|
+
".mneme-pipe-steps{display:flex;gap:4px;flex-wrap:wrap;align-items:center;margin:2px 0}",
|
|
2293
|
+
".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}",
|
|
2294
|
+
".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)}",
|
|
2295
|
+
".mneme-pipe-stepArrow{color:var(--mneme-fg-dim);font-size:11px}",
|
|
2296
|
+
// 老宿主退化:居中 sheet(形态沿用记忆库 overlay 的背板 + 居中面板)。
|
|
2297
|
+
".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)}",
|
|
2298
|
+
".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)}",
|
|
2299
|
+
".mneme-pipe-sheetbody{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}",
|
|
2300
|
+
// 空态/加载/错误共用文案块。
|
|
2301
|
+
".mneme-pipe-empty{flex:none;padding:18px 14px;text-align:center;font-size:13px;line-height:20px;color:var(--mneme-fg-dim)}",
|
|
2302
|
+
".mneme-pipe-emptyTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
|
|
2303
|
+
// 工作流清单:**选择器**,不是「一屏」。它在上面占一条、图在下面占其余——
|
|
2304
|
+
// 第一版把它做成吃掉整屏、点条目才换屏,结果画布藏在一次点击后面且界面上没有任何
|
|
2305
|
+
// 提示,反馈连着两次说「看不到画布和节点」。上限用固定值不用百分比:百分比在
|
|
2306
|
+
// 高度不确定的祖先下会被当成 none,又变回吃掉整屏。
|
|
2307
|
+
".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)}",
|
|
2308
|
+
".mneme-pipe-wfhead{flex:none;display:flex;align-items:baseline;justify-content:space-between;gap:8px;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2309
|
+
".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)}",
|
|
2310
|
+
".mneme-pipe-wf:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2311
|
+
".mneme-pipe-wf:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2312
|
+
// 当前展开的那条:只有颜色不同,图仍由下面的画布独占——选择器不该抢主语的位置。
|
|
2313
|
+
".mneme-pipe-wf.mneme-on{border-color:var(--dsw-alias-state-business-primary)}",
|
|
2314
|
+
".mneme-pipe-wfTop{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
|
|
2315
|
+
".mneme-pipe-wfName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:0}",
|
|
2316
|
+
".mneme-pipe-wfScope{flex:none;font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2317
|
+
".mneme-pipe-wfMeta{display:block;margin-top:3px;font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2318
|
+
".mneme-pipe-wfId{display:block;margin-top:2px;font-size:11px;color:var(--mneme-fg-dim)}",
|
|
2319
|
+
".mneme-pipe-broken{flex:none;font-size:12px;line-height:17px;color:var(--mneme-warn);padding:6px 14px 0}",
|
|
1902
2320
|
].join("\n");
|
|
1903
2321
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
|
|
1904
2322
|
const tag = document.createElement("style");
|
|
@@ -2238,31 +2656,39 @@ window.__ModuleLoader__.load({
|
|
|
2238
2656
|
const graphStatus = status === "ready" && data && data.nodes.length <= 1 ? "empty" : status;
|
|
2239
2657
|
|
|
2240
2658
|
const rail = h("div", { className: "mneme-entrail" },
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
),
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2659
|
+
// 头部固定段:表头(本页名「实体」——原误用全局页签名「记忆库」,而
|
|
2660
|
+
// memory.view.label 仍被顶部 nav aria-label 使用,字典不动只换这里)
|
|
2661
|
+
// + 搜索 + 计数,不随列表滚动。
|
|
2662
|
+
h("div", { className: "mneme-entrailhead" },
|
|
2663
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.tabEntities")),
|
|
2664
|
+
h("input", {
|
|
2665
|
+
className: "mneme-search mneme-xsearch",
|
|
2666
|
+
placeholder: t("memory.graph.placeholder"),
|
|
2667
|
+
value: railQuery,
|
|
2668
|
+
onChange: (e) => setRailQuery(e.target.value)
|
|
2669
|
+
}),
|
|
2670
|
+
entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
|
|
2671
|
+
t("memory.entities.count").replace("{n}", String(entities.length)))
|
|
2672
|
+
),
|
|
2673
|
+
h("div", { className: "mneme-entrailbody" },
|
|
2674
|
+
entities === null && h("div", { className: "mneme-xempty" }, "…"),
|
|
2675
|
+
entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
|
|
2676
|
+
entities !== null && entities.length > 0 && visibleEntities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.noMatch")),
|
|
2677
|
+
groups.map((g) => h("div", { key: g.key },
|
|
2678
|
+
h("div", { className: "mneme-enttype" },
|
|
2679
|
+
h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
|
|
2680
|
+
entityTypeLabel(t, g.key)
|
|
2681
|
+
),
|
|
2682
|
+
g.items.map((e) => h("button", {
|
|
2683
|
+
key: e.id,
|
|
2684
|
+
className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
|
|
2685
|
+
onClick: () => setEntityName(e.name)
|
|
2686
|
+
},
|
|
2687
|
+
h("span", { className: "mneme-entname" }, e.name),
|
|
2688
|
+
h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
|
|
2689
|
+
))
|
|
2264
2690
|
))
|
|
2265
|
-
)
|
|
2691
|
+
)
|
|
2266
2692
|
);
|
|
2267
2693
|
|
|
2268
2694
|
// side card for a clicked (non-root) node or edge inside the graph layer
|
|
@@ -2278,14 +2704,7 @@ window.__ModuleLoader__.load({
|
|
|
2278
2704
|
selected.node.name !== entityName && h("button", {
|
|
2279
2705
|
className: "mneme-footbtn",
|
|
2280
2706
|
onClick: () => setEntityName(selected.node.name)
|
|
2281
|
-
}, t("memory.graph.viewInGraph"))
|
|
2282
|
-
attrs.length > 0 && h("div", null,
|
|
2283
|
-
h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
|
|
2284
|
-
attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
|
|
2285
|
-
h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
|
|
2286
|
-
h("span", { className: "mneme-gs-attrval" }, a.value)
|
|
2287
|
-
))
|
|
2288
|
-
)
|
|
2707
|
+
}, t("memory.graph.viewInGraph"))
|
|
2289
2708
|
)
|
|
2290
2709
|
: selected?.kind === "edge"
|
|
2291
2710
|
? h("div", { className: "mneme-graphside" },
|
|
@@ -2319,46 +2738,104 @@ window.__ModuleLoader__.load({
|
|
|
2319
2738
|
h("span", { title: formatDate(entity.last_seen) },
|
|
2320
2739
|
`${t("memory.entities.lastSeen")}: ${formatRelativeTime(entity.last_seen, t)}`)
|
|
2321
2740
|
),
|
|
2322
|
-
// ①
|
|
2741
|
+
// ① 关系图谱(前置):图形在属性/列表之前,是实体页的第一眼;
|
|
2742
|
+
// 深度/复位 chips 从层头移进图谱工具行(动作靠近作用对象),操作
|
|
2743
|
+
// 提示收进行尾小字不再独占一行;关系列表(跳来源记忆的唯一入口)
|
|
2744
|
+
// 保留为图谱的文本镜像、与图谱同层共享层头,消除信息双轨的分段感。
|
|
2323
2745
|
h("div", { className: "mneme-layer" },
|
|
2324
2746
|
h("div", { className: "mneme-layerhead" },
|
|
2325
|
-
t("memory.entities.
|
|
2326
|
-
h("span", { className: "mneme-layercount" }, String(
|
|
2747
|
+
t("memory.entities.relations"),
|
|
2748
|
+
h("span", { className: "mneme-layercount" }, String(rootEdges.length))
|
|
2327
2749
|
),
|
|
2328
|
-
|
|
2329
|
-
? h("div", { className: "mneme-
|
|
2330
|
-
: h(
|
|
2331
|
-
|
|
2332
|
-
h("
|
|
2333
|
-
|
|
2334
|
-
|
|
2750
|
+
rootEdges.length === 0 && graphStatus === "empty"
|
|
2751
|
+
? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
|
|
2752
|
+
: h(react.Fragment, null,
|
|
2753
|
+
graphStatus !== "empty" && h("div", { className: "mneme-graphbar" },
|
|
2754
|
+
h("button", {
|
|
2755
|
+
className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
|
|
2756
|
+
title: t("memory.graph.depth"),
|
|
2757
|
+
onClick: () => setDepth(depth === 1 ? 2 : 1)
|
|
2758
|
+
}, `${depth} ${t("memory.graph.depth")}`),
|
|
2759
|
+
h("button", {
|
|
2760
|
+
className: "mneme-chip",
|
|
2761
|
+
title: t("memory.graph.resetView"),
|
|
2762
|
+
onClick: resetView
|
|
2763
|
+
}, t("memory.graph.resetView")),
|
|
2764
|
+
h("span", { style: { marginLeft: "auto", fontSize: 12, color: "var(--mneme-fg-dim)" } }, t("memory.graph.hint"))
|
|
2765
|
+
),
|
|
2766
|
+
graphStatus !== "empty" && h("div", { className: "mneme-entgraphwrap" },
|
|
2767
|
+
h("svg", {
|
|
2768
|
+
ref: svgRef,
|
|
2769
|
+
className: "mneme-graphsvg",
|
|
2770
|
+
viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
|
|
2771
|
+
style: { height: 440 },
|
|
2772
|
+
role: "img",
|
|
2773
|
+
"aria-label": t("memory.graph.summary")
|
|
2774
|
+
.replace("{entity}", (data.root && (data.root.label || data.root.name)) || "")
|
|
2775
|
+
.replace("{nodes}", String(data.nodes.length))
|
|
2776
|
+
.replace("{edges}", String(data.edges.length)),
|
|
2777
|
+
onMouseDown: onSurfaceMouseDown
|
|
2778
|
+
},
|
|
2779
|
+
h("g", {
|
|
2780
|
+
transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
|
|
2781
|
+
},
|
|
2782
|
+
data.edges.map((e) => {
|
|
2783
|
+
const a = nodeById.get(e.from), b = nodeById.get(e.to);
|
|
2784
|
+
if (!a || !b) return null;
|
|
2785
|
+
return h("line", {
|
|
2786
|
+
key: e.id,
|
|
2787
|
+
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
|
2788
|
+
className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
|
|
2789
|
+
onClick: () => onEdgeClick(e)
|
|
2790
|
+
});
|
|
2791
|
+
}),
|
|
2792
|
+
nodes.map((n) => h("g", {
|
|
2793
|
+
key: n.id,
|
|
2794
|
+
className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
|
|
2795
|
+
transform: `translate(${n.x},${n.y})`,
|
|
2796
|
+
onMouseDown: (e) => onNodeMouseDown(e, n),
|
|
2797
|
+
onClick: () => onNodeClick(n),
|
|
2798
|
+
"data-node": n.name
|
|
2799
|
+
},
|
|
2800
|
+
h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
|
|
2801
|
+
h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
|
|
2802
|
+
))
|
|
2803
|
+
)
|
|
2804
|
+
),
|
|
2805
|
+
side
|
|
2806
|
+
),
|
|
2807
|
+
rootEdges.length > 0 && rootEdges.map((e) => {
|
|
2808
|
+
const out = e.from === data.root.id;
|
|
2809
|
+
const other = nodeById.get(out ? e.to : e.from);
|
|
2810
|
+
const otherName = other?.name ?? (out ? e.to : e.from);
|
|
2811
|
+
return h("button", {
|
|
2812
|
+
key: e.id,
|
|
2813
|
+
className: "mneme-relrow",
|
|
2814
|
+
title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
|
|
2815
|
+
onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
|
|
2816
|
+
},
|
|
2817
|
+
h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
|
|
2818
|
+
h("span", { className: "mneme-reltype" }, e.relation_type),
|
|
2819
|
+
h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
|
|
2820
|
+
);
|
|
2821
|
+
})
|
|
2335
2822
|
)
|
|
2336
2823
|
),
|
|
2337
|
-
// ②
|
|
2338
|
-
|
|
2824
|
+
// ② 属性 — current snapshot (valid_until IS NULL);空层整层隐藏
|
|
2825
|
+
// (原先渲染一行「暂无内容」占住高度,是零信息的堆积)
|
|
2826
|
+
attrs.length > 0 && h("div", { className: "mneme-layer" },
|
|
2339
2827
|
h("div", { className: "mneme-layerhead" },
|
|
2340
|
-
t("memory.entities.
|
|
2341
|
-
h("span", { className: "mneme-layercount" }, String(
|
|
2828
|
+
t("memory.entities.attrs"),
|
|
2829
|
+
h("span", { className: "mneme-layercount" }, String(attrs.length))
|
|
2342
2830
|
),
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
return h("button", {
|
|
2350
|
-
key: e.id,
|
|
2351
|
-
className: "mneme-relrow",
|
|
2352
|
-
title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
|
|
2353
|
-
onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
|
|
2354
|
-
},
|
|
2355
|
-
h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
|
|
2356
|
-
h("span", { className: "mneme-reltype" }, e.relation_type),
|
|
2357
|
-
h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
|
|
2358
|
-
);
|
|
2359
|
-
})
|
|
2831
|
+
h("div", { className: "mneme-attrgrid" },
|
|
2832
|
+
attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
|
|
2833
|
+
h("div", { className: "mneme-attrkey" }, a.key),
|
|
2834
|
+
h("div", { className: "mneme-attrval" }, a.value)
|
|
2835
|
+
))
|
|
2836
|
+
)
|
|
2360
2837
|
),
|
|
2361
|
-
// ③ 关联记忆 — entity: prefix recall
|
|
2838
|
+
// ③ 关联记忆 — entity: prefix recall(entity: 之外的唯一记忆回链入口)
|
|
2362
2839
|
related.length > 0 && h("div", { className: "mneme-layer" },
|
|
2363
2840
|
h("div", { className: "mneme-layerhead" },
|
|
2364
2841
|
t("memory.graph.related"),
|
|
@@ -2371,68 +2848,6 @@ window.__ModuleLoader__.load({
|
|
|
2371
2848
|
title: t("memory.card.open"),
|
|
2372
2849
|
onClick: () => onJumpMemory && onJumpMemory(m)
|
|
2373
2850
|
}, m.title || m.content?.slice(0, 60)))
|
|
2374
|
-
),
|
|
2375
|
-
// ④ 关系图谱 — the ego graph
|
|
2376
|
-
h("div", { className: "mneme-layer" },
|
|
2377
|
-
h("div", { className: "mneme-layerhead" },
|
|
2378
|
-
t("memory.entities.graph"),
|
|
2379
|
-
h("span", { className: "mneme-layercount" },
|
|
2380
|
-
h("button", {
|
|
2381
|
-
className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
|
|
2382
|
-
title: t("memory.graph.depth"),
|
|
2383
|
-
onClick: () => setDepth(depth === 1 ? 2 : 1)
|
|
2384
|
-
}, `${depth} ${t("memory.graph.depth")}`)),
|
|
2385
|
-
h("span", { className: "mneme-layercount" },
|
|
2386
|
-
h("button", {
|
|
2387
|
-
className: "mneme-chip",
|
|
2388
|
-
title: t("memory.graph.resetView"),
|
|
2389
|
-
onClick: resetView
|
|
2390
|
-
}, t("memory.graph.resetView")))
|
|
2391
|
-
),
|
|
2392
|
-
graphStatus === "empty"
|
|
2393
|
-
? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
|
|
2394
|
-
: h(react.Fragment, null,
|
|
2395
|
-
h("svg", {
|
|
2396
|
-
ref: svgRef,
|
|
2397
|
-
className: "mneme-graphsvg",
|
|
2398
|
-
viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
|
|
2399
|
-
style: { height: 320 },
|
|
2400
|
-
role: "img",
|
|
2401
|
-
"aria-label": t("memory.graph.summary")
|
|
2402
|
-
.replace("{entity}", (data.root && (data.root.label || data.root.name)) || "")
|
|
2403
|
-
.replace("{nodes}", String(data.nodes.length))
|
|
2404
|
-
.replace("{edges}", String(data.edges.length)),
|
|
2405
|
-
onMouseDown: onSurfaceMouseDown
|
|
2406
|
-
},
|
|
2407
|
-
h("g", {
|
|
2408
|
-
transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
|
|
2409
|
-
},
|
|
2410
|
-
data.edges.map((e) => {
|
|
2411
|
-
const a = nodeById.get(e.from), b = nodeById.get(e.to);
|
|
2412
|
-
if (!a || !b) return null;
|
|
2413
|
-
return h("line", {
|
|
2414
|
-
key: e.id,
|
|
2415
|
-
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
|
2416
|
-
className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
|
|
2417
|
-
onClick: () => onEdgeClick(e)
|
|
2418
|
-
});
|
|
2419
|
-
}),
|
|
2420
|
-
nodes.map((n) => h("g", {
|
|
2421
|
-
key: n.id,
|
|
2422
|
-
className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
|
|
2423
|
-
transform: `translate(${n.x},${n.y})`,
|
|
2424
|
-
onMouseDown: (e) => onNodeMouseDown(e, n),
|
|
2425
|
-
onClick: () => onNodeClick(n),
|
|
2426
|
-
"data-node": n.name
|
|
2427
|
-
},
|
|
2428
|
-
h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
|
|
2429
|
-
h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
|
|
2430
|
-
))
|
|
2431
|
-
)
|
|
2432
|
-
),
|
|
2433
|
-
h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
|
|
2434
|
-
),
|
|
2435
|
-
side
|
|
2436
2851
|
)
|
|
2437
2852
|
)
|
|
2438
2853
|
)
|
|
@@ -2454,10 +2869,32 @@ window.__ModuleLoader__.load({
|
|
|
2454
2869
|
// 后端 src/config.js 的 INJECT_CHILD_FLAGS(运行时闸门),两侧漂移由
|
|
2455
2870
|
// test/inject-parent-gate.test.js 钉住。
|
|
2456
2871
|
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
|
|
2872
|
+
// Issue #427 建议 1:折叠线挪到「新手之外」。group.basic 是首屏层,只列新
|
|
2873
|
+
// 用户敢动的总开关;group.scope 也在首屏——它回答新用户的第二个必答题
|
|
2874
|
+
// 「它记的东西会不会被别的会话看到」。其余组整组收进「全部设置」折叠
|
|
2875
|
+
// (showAll)。成员从原组里移走而不是渲染时过滤:同一项永远只渲染一次。
|
|
2876
|
+
// 首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + M.O. 1 开关 +
|
|
2877
|
+
// scope 2 开关 + 「全部设置」按钮 = 9。复验修订:产品主人点名 M.O.
|
|
2878
|
+
// 「别藏在里面」,独立成组但仍折在「全部设置」后等于换个地方藏;
|
|
2879
|
+
// ≤8 是本仓库 #427 自设的软预算,owner 点名豁免一项,锁随注释同步改。
|
|
2880
|
+
// RuntimeProvisionBlock 配好即消失,常态首屏更少。
|
|
2881
|
+
const FEATURE_FIRST_SCREEN = ["group.basic", "group.mo", "group.scope"];
|
|
2457
2882
|
const FEATURE_GROUPS = [
|
|
2458
|
-
{ key: "group.
|
|
2459
|
-
{ key: "group.
|
|
2460
|
-
|
|
2883
|
+
{ key: "group.basic", items: ["autoInject", "autoSummarize", "autoDream"] },
|
|
2884
|
+
{ key: "group.core", items: ["hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce", "preInjectGate.enabled", "preInjectGate.enforce", "sensitiveScanEnabled",
|
|
2885
|
+
// #399 的留存总闸(见下方 distillSub:天数行是它的子块)。放在组末,
|
|
2886
|
+
// 子块才能紧跟在它后面展开——items.map 之后的子块是按组尾渲染的。
|
|
2887
|
+
"distillRecords.enabled"] },
|
|
2888
|
+
// v0.9.0 M.O.(beta)复验修订:独立分组行——它不是行为开关而是座位闸门
|
|
2889
|
+
// (关=一个座位都不注册,见 apply 内的 M.O. 块)。独立成组只解决「混在
|
|
2890
|
+
// enhance 末位」;可见层由 FEATURE_FIRST_SCREEN 决定(见其注释),
|
|
2891
|
+
// 本轮已把组名提进首屏——组成员资格不决定可见性。
|
|
2892
|
+
{ key: "group.mo", items: ["actionPipelineEnabled"] },
|
|
2893
|
+
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled",
|
|
2894
|
+
// 人格定义页(beta):注入总闸。分组随 enhance(行为开关,非座位闸门);
|
|
2895
|
+
// 人格内容在设置页「个性化」区的人格卡。
|
|
2896
|
+
"personaEnabled"] },
|
|
2897
|
+
{ key: "group.dream", items: ["sleepModeEnabled"] },
|
|
2461
2898
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
2462
2899
|
{ key: "group.scope", items: ["scopeEnabled", "strictScope"] }
|
|
2463
2900
|
];
|
|
@@ -2468,6 +2905,12 @@ window.__ModuleLoader__.load({
|
|
|
2468
2905
|
// Issue #239 第 4 项:错峰时段串(巩固侧与蒸馏侧)。此前只有后端白名单、
|
|
2469
2906
|
// 面板调不到——两个错峰键一个能调一个不能比都不给更让人困惑。
|
|
2470
2907
|
"dreamPeakHours", "summarizePeakHours"];
|
|
2908
|
+
// 整数键(blur/Enter 提交,非法值不提交、退回生效值):
|
|
2909
|
+
// issue #399 的 distillRecords.retentionDays。此前它与 enabled 一样只有
|
|
2910
|
+
// 后端白名单、面板够不着——而 CONFIGURATION.md 明写「面板可启停」。
|
|
2911
|
+
// 其余整数键(429 调速器参数、distillMaxChars、codingBoostFactor)是有意
|
|
2912
|
+
// 不上 UI 的调优噪音,见上方注释,别顺手搬进来。
|
|
2913
|
+
const FEATURE_NUMBERS = ["distillRecords.retentionDays"];
|
|
2471
2914
|
const EMBED_PROVIDERS = ["openai", "local", "ollama"];
|
|
2472
2915
|
// 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
|
|
2473
2916
|
const ENTITY_REASONING = ["none", "low", "medium", "high"];
|
|
@@ -2475,10 +2918,14 @@ window.__ModuleLoader__.load({
|
|
|
2475
2918
|
// 蒸馏防推理烧预算)。后端枚举含 off,面板必须能选到,否则操作者用不上。
|
|
2476
2919
|
const SUMMARIZE_REASONING = ["off", "none", "low", "medium", "high"];
|
|
2477
2920
|
|
|
2478
|
-
function FeatureRow({ name, hint, on, disabled, onToggle, sub }) {
|
|
2921
|
+
function FeatureRow({ name, hint, on, disabled, onToggle, sub, tag }) {
|
|
2479
2922
|
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
2480
2923
|
h("div", { className: "mneme-featmain" },
|
|
2481
|
-
h("div", { className: "mneme-featname" }, name
|
|
2924
|
+
h("div", { className: "mneme-featname" }, name,
|
|
2925
|
+
// 「默认 / 已改」徽标(#427 建议 3):不用点开配置文件就知道哪
|
|
2926
|
+
// 些是出厂值、哪些被你改过;title 给完整解释(键盘/读屏可达)。
|
|
2927
|
+
tag && h("span", { className: "mneme-badge " + tag.kind, title: tag.title }, tag.text)
|
|
2928
|
+
),
|
|
2482
2929
|
h("div", { className: "mneme-feathint" }, hint)
|
|
2483
2930
|
),
|
|
2484
2931
|
h("button", {
|
|
@@ -2496,10 +2943,18 @@ window.__ModuleLoader__.load({
|
|
|
2496
2943
|
function FeaturesCard({ t }) {
|
|
2497
2944
|
const [state, setState] = useState(null); // { overrides, effective }
|
|
2498
2945
|
const [error, setError] = useState("");
|
|
2946
|
+
// 保存失败的提示要单独的座位:上面那个 error 只在首屏加载失败(state 还是
|
|
2947
|
+
// null)时渲染,state 一到手它就永不出现——于是每一次 put 失败都静默,
|
|
2948
|
+
// 草稿留在输入框里,人以为改成功了。
|
|
2949
|
+
const [saveError, setSaveError] = useState("");
|
|
2499
2950
|
const [busy, setBusy] = useState(false);
|
|
2500
2951
|
const [savedTick, setSavedTick] = useState(false);
|
|
2501
2952
|
const [showAdv, setShowAdv] = useState(false);
|
|
2953
|
+
// Issue #427 建议 1:「全部设置」是第一层折叠(默认收起),「高级」是它
|
|
2954
|
+
// 内部的第二层。两个状态独立:收起全部设置时高级的展开与否被记住。
|
|
2955
|
+
const [showAll, setShowAll] = useState(false);
|
|
2502
2956
|
const [strs, setStrs] = useState({}); // 字符串输入的本地草稿:key -> string
|
|
2957
|
+
const [nums, setNums] = useState({}); // 数值输入的本地草稿:key -> string(提交时才转整数)
|
|
2503
2958
|
// 巩固/睡眠模型路由下拉的数据源:GET /llm-providers 探测(云端 v0.7.26+
|
|
2504
2959
|
// 的插件侧端点)。null = 端点不可用(404/失败)→ 回退纯文本输入,不挡旧后端。
|
|
2505
2960
|
const [routes, setRoutes] = useState(null);
|
|
@@ -2519,6 +2974,9 @@ window.__ModuleLoader__.load({
|
|
|
2519
2974
|
const draft = {};
|
|
2520
2975
|
for (const key of FEATURE_STRINGS) draft[key] = String(e[key] ?? "");
|
|
2521
2976
|
setStrs(draft);
|
|
2977
|
+
const numDraft = {};
|
|
2978
|
+
for (const key of FEATURE_NUMBERS) numDraft[key] = String(e[key] ?? "");
|
|
2979
|
+
setNums(numDraft);
|
|
2522
2980
|
})
|
|
2523
2981
|
.catch(() => { if (!cancelled) setError(t("memory.features.loadFailed")); });
|
|
2524
2982
|
return () => { cancelled = true; };
|
|
@@ -2536,6 +2994,7 @@ window.__ModuleLoader__.load({
|
|
|
2536
2994
|
|
|
2537
2995
|
const eff = (state && state.effective) || {};
|
|
2538
2996
|
|
|
2997
|
+
// 返回值 = 这次写入有没有落库。调用方要据此决定草稿的去留。
|
|
2539
2998
|
const put = async (patch) => {
|
|
2540
2999
|
setBusy(true);
|
|
2541
3000
|
try {
|
|
@@ -2544,13 +3003,25 @@ window.__ModuleLoader__.load({
|
|
|
2544
3003
|
headers: { "Content-Type": "application/json" },
|
|
2545
3004
|
body: JSON.stringify(patch)
|
|
2546
3005
|
});
|
|
2547
|
-
if (!res.ok)
|
|
3006
|
+
if (!res.ok) {
|
|
3007
|
+
// 服务端的校验失败把键名与范围写进 error(api.js 里那条 400 的注释
|
|
3008
|
+
// 就是这么设计的),读出来给人看。拿不到 body 才退回状态码——
|
|
3009
|
+
// 总之不能只剩一句「失败」让人猜是哪个开关、错在哪。
|
|
3010
|
+
const detail = await res.json()
|
|
3011
|
+
.then((j) => (j && j.error) || "")
|
|
3012
|
+
.catch(() => "");
|
|
3013
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
3014
|
+
}
|
|
2548
3015
|
setState(await res.json());
|
|
3016
|
+
setSaveError("");
|
|
2549
3017
|
announce(t("memory.features.restartHint"));
|
|
2550
3018
|
setSavedTick(true);
|
|
2551
3019
|
setTimeout(() => setSavedTick(false), 1800);
|
|
2552
|
-
|
|
2553
|
-
|
|
3020
|
+
return true;
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
const why = (err && err.message) || String(err);
|
|
3023
|
+
setSaveError(`${t("memory.features.saveFailed")} — ${why}`);
|
|
3024
|
+
return false;
|
|
2554
3025
|
} finally {
|
|
2555
3026
|
setBusy(false);
|
|
2556
3027
|
}
|
|
@@ -2558,15 +3029,45 @@ window.__ModuleLoader__.load({
|
|
|
2558
3029
|
|
|
2559
3030
|
// 字符串项失焦/回车提交:trim 后与生效值相同则回填放弃,否则交给
|
|
2560
3031
|
// 后端校验(ollamaBaseUrl 的协议、embedProvider 的枚举在后端把关)。
|
|
3032
|
+
// 后端拒了就退回生效值并出声——留着被拒的文本,人以为改成功了。
|
|
2561
3033
|
const commitString = (key) => {
|
|
2562
3034
|
const raw = (strs[key] ?? "").trim();
|
|
2563
3035
|
if (raw === String(eff[key] ?? "")) {
|
|
2564
3036
|
setStrs((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
2565
3037
|
return;
|
|
2566
3038
|
}
|
|
2567
|
-
put({ [key]: raw })
|
|
3039
|
+
put({ [key]: raw }).then((ok) => {
|
|
3040
|
+
if (!ok) setStrs((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
3041
|
+
});
|
|
2568
3042
|
};
|
|
2569
3043
|
|
|
3044
|
+
// 整数项失焦/回车提交。与字符串项同一形状,多三道闸:本地先挡非整数,
|
|
3045
|
+
// 挡不住的交后端(retentionDays 的范围 1–3650 由白名单把关),后端也拒了
|
|
3046
|
+
// 就把草稿退回生效值。前两道退回时也不出声——那是本地的即时反馈;第三道
|
|
3047
|
+
// 必须出声(put 会把原因写进 saveError),否则「框里的数字」与「在用的
|
|
3048
|
+
// 保留天数」会分家,而人看不出来。
|
|
3049
|
+
const commitNumber = (key) => {
|
|
3050
|
+
const raw = (nums[key] ?? "").trim();
|
|
3051
|
+
if (raw === String(eff[key] ?? "")) return;
|
|
3052
|
+
if (!/^\d+$/.test(raw)) {
|
|
3053
|
+
setNums((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
put({ [key]: Number(raw) }).then((ok) => {
|
|
3057
|
+
if (!ok) setNums((c) => ({ ...c, [key]: String(eff[key] ?? "") }));
|
|
3058
|
+
});
|
|
3059
|
+
};
|
|
3060
|
+
|
|
3061
|
+
// 「默认 / 已改」的信号来源是 state.overrides:featureSnapshot 只把
|
|
3062
|
+
// settings.getFeatureFlags() 里真实落库的键放进 overrides,键不在其中
|
|
3063
|
+
// = 值仍来自 config schema 的默认值。面板自己判断,不加后端字段、
|
|
3064
|
+
// 不动 config.js(#427 的验收红线之一)。
|
|
3065
|
+
const ovr = (state && state.overrides) || {};
|
|
3066
|
+
const flagTag = (k) => ({
|
|
3067
|
+
kind: Object.prototype.hasOwnProperty.call(ovr, k) ? "mneme-badge--changed" : "mneme-badge--default",
|
|
3068
|
+
text: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged" : "memory.features.tagDefault"),
|
|
3069
|
+
title: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged.hint" : "memory.features.tagDefault.hint")
|
|
3070
|
+
});
|
|
2570
3071
|
const boolRow = (k, opts = {}) => h(FeatureRow, {
|
|
2571
3072
|
key: k,
|
|
2572
3073
|
name: t(`memory.features.${k}`),
|
|
@@ -2575,6 +3076,7 @@ window.__ModuleLoader__.load({
|
|
|
2575
3076
|
on: !!eff[k],
|
|
2576
3077
|
disabled: busy,
|
|
2577
3078
|
sub: !!opts.sub,
|
|
3079
|
+
tag: flagTag(k),
|
|
2578
3080
|
onToggle: () => put({ [k]: !eff[k] })
|
|
2579
3081
|
});
|
|
2580
3082
|
|
|
@@ -2592,9 +3094,11 @@ window.__ModuleLoader__.load({
|
|
|
2592
3094
|
// 连通性测试:POST /test-model,空 provider/model = 按巩固路由解析
|
|
2593
3095
|
// (agent 默认)。durationMs 优先后端值,缺失时客户端兜底计时;成功行
|
|
2594
3096
|
// 附模型的实际回复(reply,后端截 100 字符)——「真的答了 ok」而非只报通。
|
|
2595
|
-
// reasoningEffort 透传该路由配置的档位(issue #215
|
|
2596
|
-
//
|
|
2597
|
-
//
|
|
3097
|
+
// reasoningEffort 透传该路由配置的档位(issue #215),'none'/未配置省略
|
|
3098
|
+
// 字段,与后端 src/api.js 的同口径。**档位被拒不再是失败**:后端与真实
|
|
3099
|
+
// run 一样丢掉字段重试一次,回以 effortDropped:true,这里单独说明「连通
|
|
3100
|
+
// 了、但不是用你配的档位」——否则用户会照着红色失败去换 Provider,而
|
|
3101
|
+
// 后台其实一直在正常跑(这正是它此前给出误导性诊断的原因)。
|
|
2598
3102
|
const runModelTest = async (provider, model, setTestState, reasoningEffort) => {
|
|
2599
3103
|
setTestState({ running: true });
|
|
2600
3104
|
const started = Date.now();
|
|
@@ -2620,6 +3124,8 @@ window.__ModuleLoader__.load({
|
|
|
2620
3124
|
setTestState({
|
|
2621
3125
|
ok: true,
|
|
2622
3126
|
ms,
|
|
3127
|
+
// 只有后端明确置位才算降级(旧后端无此字段 → 不显示,不误报)。
|
|
3128
|
+
dropped: j.effortDropped === true,
|
|
2623
3129
|
detail: [j.modelId, j.reply ? "「" + j.reply + "」" : ""].filter(Boolean).join(" · ")
|
|
2624
3130
|
});
|
|
2625
3131
|
} catch (err) {
|
|
@@ -2687,6 +3193,7 @@ window.__ModuleLoader__.load({
|
|
|
2687
3193
|
(testState && !testState.running
|
|
2688
3194
|
? h("div", { className: "mneme-featsubhint" },
|
|
2689
3195
|
(testState.ok ? "✓ " + t("memory.features.modelTestOk") : "✗ " + t("memory.features.modelTestFail"))
|
|
3196
|
+
+ (testState.ok && testState.dropped ? " · " + t("memory.features.modelTestDowngraded") : "")
|
|
2690
3197
|
+ (typeof testState.ms === "number" ? " · " + (testState.ms / 1000).toFixed(1) + "s" : "")
|
|
2691
3198
|
+ (testState.detail ? " · " + testState.detail : ""))
|
|
2692
3199
|
: h("div", { className: "mneme-featsubhint" }, t("memory.features.modelTestHint"))))
|
|
@@ -2703,6 +3210,22 @@ window.__ModuleLoader__.load({
|
|
|
2703
3210
|
onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); }
|
|
2704
3211
|
}));
|
|
2705
3212
|
|
|
3213
|
+
// type 刻意用 text + inputMode 而不是 number:number 输入框在部分内容被
|
|
3214
|
+
// 判非法时 e.target.value 会变成空串,草稿与用户所见脱节;这里要自己拿
|
|
3215
|
+
// 到原文才能挡下非整数并退回生效值。
|
|
3216
|
+
const numRow = (key) => h("div", { className: "mneme-featnum", key },
|
|
3217
|
+
h("span", { className: "mneme-featnumlabel" }, t(`memory.features.${key}`)),
|
|
3218
|
+
h("input", {
|
|
3219
|
+
className: "mneme-numinput",
|
|
3220
|
+
type: "text",
|
|
3221
|
+
inputMode: "numeric",
|
|
3222
|
+
value: nums[key] ?? "",
|
|
3223
|
+
"aria-label": t(`memory.features.${key}`),
|
|
3224
|
+
onChange: (e) => setNums((c) => ({ ...c, [key]: e.target.value })),
|
|
3225
|
+
onBlur: () => commitNumber(key),
|
|
3226
|
+
onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); }
|
|
3227
|
+
}));
|
|
3228
|
+
|
|
2706
3229
|
// 语义检索路线:provider 下拉即时提交;local/ollama 选中时展开各自
|
|
2707
3230
|
// 的连接字段,openai 的连接信息由「向量搜索」卡片持有,只给指引。
|
|
2708
3231
|
const embedSub = h("div", { className: "mneme-featsub" },
|
|
@@ -2784,6 +3307,14 @@ window.__ModuleLoader__.load({
|
|
|
2784
3307
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.summarizePeakHours.hint"))
|
|
2785
3308
|
);
|
|
2786
3309
|
|
|
3310
|
+
// #399 的留存开关紧跟在蒸馏总闸下:天数行只在留存开着时展开(关着一行
|
|
3311
|
+
// 不写,改天数没有意义)。放在这而不是挂在 flagRow 上,是因为
|
|
3312
|
+
// FEATURE_CHILDREN 只认布尔子项,而这里要的是一个整数输入行。
|
|
3313
|
+
const distillSub = eff["distillRecords.enabled"] && h("div", { className: "mneme-featsub" },
|
|
3314
|
+
numRow("distillRecords.retentionDays"),
|
|
3315
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.distillRecords.retentionDays.hint"))
|
|
3316
|
+
);
|
|
3317
|
+
|
|
2787
3318
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
2788
3319
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
2789
3320
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -2794,29 +3325,58 @@ window.__ModuleLoader__.load({
|
|
|
2794
3325
|
savedTick && h("span", { className: "mneme-saved", style: { marginLeft: 8, fontWeight: 400 } }, t("memory.features.restartHint"))
|
|
2795
3326
|
),
|
|
2796
3327
|
h("div", { className: "mneme-set-desc" }, t("memory.features.desc")),
|
|
3328
|
+
// 见 saveError 的声明处:这条分支不能跟着加载失败那条走。
|
|
3329
|
+
saveError && h("div", { className: "mneme-mainterr" }, saveError),
|
|
2797
3330
|
h(RuntimeProvisionBlock, { t }),
|
|
2798
3331
|
state === null && !error
|
|
2799
3332
|
? h("div", { className: "mneme-set-hint" }, "…")
|
|
2800
|
-
:
|
|
2801
|
-
|
|
3333
|
+
: (() => {
|
|
3334
|
+
// Issue #427:分组按「实现」分改不动需求本身,改的是「折叠线划在
|
|
3335
|
+
// 哪里」。renderGroup 是唯一的组渲染路径(含组尾子块),首屏层与
|
|
3336
|
+
// 「全部设置」共用它,保证 g.items.map(flagRow) 只写一次、子项
|
|
3337
|
+
// 只可能经 flagRow 渲染(test/inject-parent-gate.test.js 的门)。
|
|
3338
|
+
const renderGroup = (g) => h(react.Fragment, { key: g.key },
|
|
2802
3339
|
h("div", { className: "mneme-featgroup" }, t(`memory.features.${g.key}`)),
|
|
2803
3340
|
g.items.map(flagRow),
|
|
2804
|
-
|
|
3341
|
+
// 子块按组尾追加。distillSub 紧跟在 core 组最后一行
|
|
3342
|
+
// (distillRecords.enabled)之后,天数行才贴着它的总闸。
|
|
3343
|
+
g.key === "group.core" && h(react.Fragment, null, distillSub, summarizeSub),
|
|
2805
3344
|
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
2806
3345
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
2807
|
-
)
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
},
|
|
2815
|
-
h(
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3346
|
+
);
|
|
3347
|
+
const firstScreen = FEATURE_GROUPS.filter((g) => FEATURE_FIRST_SCREEN.includes(g.key));
|
|
3348
|
+
const restGroups = FEATURE_GROUPS.filter((g) => !FEATURE_FIRST_SCREEN.includes(g.key));
|
|
3349
|
+
return h(react.Fragment, null,
|
|
3350
|
+
firstScreen.map(renderGroup),
|
|
3351
|
+
// 「全部设置」= 第一层折叠(默认收起,#427 建议 1):里面的
|
|
3352
|
+
// 30 余项对新手是噪音,对新用户可见的只有上面的总开关层。
|
|
3353
|
+
h("div", { className: "mneme-featgroup" },
|
|
3354
|
+
h("button", {
|
|
3355
|
+
type: "button",
|
|
3356
|
+
className: "mneme-footbtn",
|
|
3357
|
+
"aria-expanded": String(showAll),
|
|
3358
|
+
onClick: () => setShowAll(!showAll)
|
|
3359
|
+
},
|
|
3360
|
+
h(Icon, { name: showAll ? "chevronDown" : "chevronRight", size: 12 }),
|
|
3361
|
+
t("memory.features.allToggle"))
|
|
3362
|
+
),
|
|
3363
|
+
showAll && h(react.Fragment, null,
|
|
3364
|
+
restGroups.map(renderGroup),
|
|
3365
|
+
// 「高级」降为第二层,嵌在「全部设置」内部。
|
|
3366
|
+
h("div", { className: "mneme-featgroup" },
|
|
3367
|
+
h("button", {
|
|
3368
|
+
type: "button",
|
|
3369
|
+
className: "mneme-footbtn",
|
|
3370
|
+
"aria-expanded": String(showAdv),
|
|
3371
|
+
onClick: () => setShowAdv(!showAdv)
|
|
3372
|
+
},
|
|
3373
|
+
h(Icon, { name: showAdv ? "chevronDown" : "chevronRight", size: 12 }),
|
|
3374
|
+
t("memory.features.advancedToggle"))
|
|
3375
|
+
),
|
|
3376
|
+
showAdv && FEATURE_ADVANCED_BOOLS.map((k) => boolRow(k))
|
|
3377
|
+
)
|
|
3378
|
+
);
|
|
3379
|
+
})()
|
|
2820
3380
|
);
|
|
2821
3381
|
}
|
|
2822
3382
|
|
|
@@ -2848,6 +3408,14 @@ window.__ModuleLoader__.load({
|
|
|
2848
3408
|
const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
|
|
2849
3409
|
const [saved, setSaved] = react.useState(false);
|
|
2850
3410
|
const [cmdError, setCmdError] = react.useState("");
|
|
3411
|
+
// 人格卡(beta,v1.2 多卡):default 卡草稿 + byAgent 卡列表 + 当前编辑位;
|
|
3412
|
+
// provenance 是 v1.5 协作式人格的预留位,v1 只透传不编辑。
|
|
3413
|
+
const [personaDefault, setPersonaDefault] = react.useState({ name: "", personality: "", replyStyle: "", enabled: false });
|
|
3414
|
+
const [personaByAgent, setPersonaByAgent] = react.useState({});
|
|
3415
|
+
const [personaEditing, setPersonaEditing] = react.useState("default");
|
|
3416
|
+
const [personaNewName, setPersonaNewName] = react.useState("");
|
|
3417
|
+
const [personaSaved, setPersonaSaved] = react.useState(false);
|
|
3418
|
+
const [personaError, setPersonaError] = react.useState("");
|
|
2851
3419
|
const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
2852
3420
|
const [vectorSaved, setVectorSaved] = react.useState(false);
|
|
2853
3421
|
const [reindexing, setReindexing] = react.useState(false);
|
|
@@ -2858,9 +3426,39 @@ window.__ModuleLoader__.load({
|
|
|
2858
3426
|
const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
|
|
2859
3427
|
// 反馈入口的插件版本(GET /info)。失败保持 "unknown",链接仍可用。
|
|
2860
3428
|
const [pkgVersion, setPkgVersion] = react.useState("unknown");
|
|
2861
|
-
//
|
|
2862
|
-
//
|
|
3429
|
+
// 版本自检(复验修订:原为进入设置页自动请求 + 顶部横幅,现改为底部
|
|
3430
|
+
// 「支持与鼓励」卡里的「检测更新」按钮手动触发,结果内联显示——自动
|
|
3431
|
+
// 请求与横幅常驻都是打扰面)。status 共四种:outdated / latest / error / null(未检测)。
|
|
2863
3432
|
const [updateInfo, setUpdateInfo] = react.useState(null);
|
|
3433
|
+
const [updateChecking, setUpdateChecking] = react.useState(false);
|
|
3434
|
+
function checkForUpdate() {
|
|
3435
|
+
if (updateChecking) return;
|
|
3436
|
+
setUpdateChecking(true);
|
|
3437
|
+
apiFetch("/api/dsh-mneme/version-check")
|
|
3438
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
3439
|
+
.then((j) => {
|
|
3440
|
+
setUpdateInfo(j && typeof j.latest === "string" && typeof j.version === "string"
|
|
3441
|
+
? { status: j.status, latest: j.latest, current: j.version }
|
|
3442
|
+
: { status: "error" });
|
|
3443
|
+
setUpdateChecking(false);
|
|
3444
|
+
})
|
|
3445
|
+
.catch(() => { setUpdateInfo({ status: "error" }); setUpdateChecking(false); });
|
|
3446
|
+
}
|
|
3447
|
+
// 检测结果一行文案:按 status 取既有或新增的 i18n key。
|
|
3448
|
+
function checkResultText() {
|
|
3449
|
+
if (!updateInfo) return "";
|
|
3450
|
+
if (updateInfo.status === "outdated") {
|
|
3451
|
+
return t("memory.settings.version.outdated")
|
|
3452
|
+
.replace("{v}", updateInfo.latest)
|
|
3453
|
+
.replace("{c}", updateInfo.current);
|
|
3454
|
+
}
|
|
3455
|
+
// 后端 classify(src/version-check.js)返回的是 "up-to-date"(连字符)。
|
|
3456
|
+
// 前端状态名必须与它逐字对齐:此前误写 "latest"/"up_to_date" 两个后端
|
|
3457
|
+
// 从未返回过的名字,检测成功也掉进 error 兜底(复验实测踩中)。
|
|
3458
|
+
if (updateInfo.status === "up-to-date") return t("memory.settings.update.latest");
|
|
3459
|
+
if (updateInfo.status === "ahead") return t("memory.settings.update.ahead");
|
|
3460
|
+
return t("memory.settings.update.error");
|
|
3461
|
+
}
|
|
2864
3462
|
// 运行模式 — light vs standard; null = still loading. The card keeps
|
|
2865
3463
|
// its own busy/saved/error state so it never blocks the others.
|
|
2866
3464
|
const [mode, setMode] = react.useState(null);
|
|
@@ -2881,16 +3479,33 @@ window.__ModuleLoader__.load({
|
|
|
2881
3479
|
|
|
2882
3480
|
const load = react.useCallback(async () => {
|
|
2883
3481
|
try {
|
|
2884
|
-
const [p, r, c, v] = await Promise.all([
|
|
3482
|
+
const [p, r, c, v, ps] = await Promise.all([
|
|
2885
3483
|
apiFetch("/api/dsh-mneme/profile").then((res) => res.json()),
|
|
2886
3484
|
apiFetch("/api/dsh-mneme/rules").then((res) => res.json()),
|
|
2887
3485
|
apiFetch("/api/dsh-mneme/commands").then((res) => res.json()),
|
|
2888
|
-
apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json())
|
|
3486
|
+
apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json()),
|
|
3487
|
+
apiFetch("/api/dsh-mneme/persona").then((res) => res.json()).catch(() => null)
|
|
2889
3488
|
]);
|
|
2890
3489
|
setProfile(p.profile || "");
|
|
2891
3490
|
setRules(Array.isArray(r.rules) ? r.rules : []);
|
|
2892
3491
|
setCommands(Array.isArray(c.commands) ? c.commands : []);
|
|
2893
3492
|
setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
|
|
3493
|
+
// 人格卡:旧后端无 /persona(404)时保持默认空卡,不阻塞其它卡片。
|
|
3494
|
+
// v1.2 多卡形 {default, byAgent};旧单对象形读作 default(后端已兼容,
|
|
3495
|
+
// 这里双保险再判一次形状)。
|
|
3496
|
+
const pj = ps && ps.persona && typeof ps.persona === "object" ? ps.persona : {};
|
|
3497
|
+
const isMulti = "default" in pj || "byAgent" in pj;
|
|
3498
|
+
const dRaw = isMulti ? (pj.default ?? {}) : pj;
|
|
3499
|
+
const bRaw = isMulti && pj.byAgent && typeof pj.byAgent === "object" ? pj.byAgent : {};
|
|
3500
|
+
const norm = (c) => ({
|
|
3501
|
+
name: typeof c?.name === "string" ? c.name : "",
|
|
3502
|
+
personality: typeof c?.personality === "string" ? c.personality : "",
|
|
3503
|
+
replyStyle: typeof c?.replyStyle === "string" ? c.replyStyle : "",
|
|
3504
|
+
enabled: c?.enabled === true
|
|
3505
|
+
});
|
|
3506
|
+
setPersonaDefault(norm(dRaw));
|
|
3507
|
+
setPersonaByAgent(Object.fromEntries(Object.entries(bRaw).map(([k, c]) => [k, norm(c)])));
|
|
3508
|
+
setPersonaEditing("default");
|
|
2894
3509
|
} catch { /* ignore */ }
|
|
2895
3510
|
}, []);
|
|
2896
3511
|
|
|
@@ -2906,21 +3521,6 @@ window.__ModuleLoader__.load({
|
|
|
2906
3521
|
return () => { cancelled = true; };
|
|
2907
3522
|
}, []);
|
|
2908
3523
|
|
|
2909
|
-
// 版本自检:只读 /version-check(宿主围栏鉴权)。非 outdated 一律归
|
|
2910
|
-
// null——横幅不出现即是「无需处理」;接口失败也静默,不新增故障面。
|
|
2911
|
-
react.useEffect(() => {
|
|
2912
|
-
let cancelled = false;
|
|
2913
|
-
apiFetch("/api/dsh-mneme/version-check")
|
|
2914
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
2915
|
-
.then((j) => {
|
|
2916
|
-
if (!cancelled && j && j.status === "outdated" && typeof j.latest === "string" && typeof j.version === "string") {
|
|
2917
|
-
setUpdateInfo({ latest: j.latest, current: j.version });
|
|
2918
|
-
}
|
|
2919
|
-
})
|
|
2920
|
-
.catch(() => {});
|
|
2921
|
-
return () => { cancelled = true; };
|
|
2922
|
-
}, []);
|
|
2923
|
-
|
|
2924
3524
|
// 反馈链接预填:环境信息(插件版本 + 平台)+ 问题描述骨架。邮件主题带
|
|
2925
3525
|
// 版本/平台方便归档,正文同款模板;纯链接零后端成本。
|
|
2926
3526
|
const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
|
|
@@ -2981,6 +3581,65 @@ window.__ModuleLoader__.load({
|
|
|
2981
3581
|
} catch { /* ignore */ }
|
|
2982
3582
|
}
|
|
2983
3583
|
|
|
3584
|
+
// 人格卡保存(v1.2 多卡):全量多卡形 PUT({default, byAgent},后端全量
|
|
3585
|
+
// 替换语义)。保存失败必须冒提示——v1 的静默 catch 曾把 404 吞成「看起来
|
|
3586
|
+
// 保存了」(后端未重装配时 PUT 打 404),桉桉实测踩中,此处不再静默。
|
|
3587
|
+
async function savePersonaCards(nextDefault, nextByAgent) {
|
|
3588
|
+
try {
|
|
3589
|
+
const res = await apiFetch("/api/dsh-mneme/persona", {
|
|
3590
|
+
method: "PUT",
|
|
3591
|
+
headers: { "Content-Type": "application/json" },
|
|
3592
|
+
body: JSON.stringify({ persona: { default: nextDefault, byAgent: nextByAgent } })
|
|
3593
|
+
});
|
|
3594
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
3595
|
+
setPersonaSaved(true);
|
|
3596
|
+
setPersonaError("");
|
|
3597
|
+
announce(t("memory.settings.personaSaved"));
|
|
3598
|
+
setTimeout(() => setPersonaSaved(false), 1500);
|
|
3599
|
+
} catch (err) {
|
|
3600
|
+
setPersonaError((err && err.message) || "failed");
|
|
3601
|
+
setTimeout(() => setPersonaError(""), 4000);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
// 当前编辑位的卡:default 或 byAgent[editing];草稿即编辑位状态本身。
|
|
3605
|
+
const personaEditingCard = personaEditing === "default"
|
|
3606
|
+
? personaDefault
|
|
3607
|
+
: personaByAgent[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false };
|
|
3608
|
+
const updatePersonaCard = (key, value) => {
|
|
3609
|
+
if (personaEditing === "default") setPersonaDefault((c) => ({ ...c, [key]: value }));
|
|
3610
|
+
else setPersonaByAgent((c) => ({ ...c, [personaEditing]: { ...(c[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false }), [key]: value } }));
|
|
3611
|
+
};
|
|
3612
|
+
const saveEditingPersona = () => savePersonaCards(personaDefault, personaByAgent);
|
|
3613
|
+
const toggleEditingPersona = (checked) => {
|
|
3614
|
+
if (personaEditing === "default") {
|
|
3615
|
+
const next = { ...personaDefault, enabled: checked };
|
|
3616
|
+
setPersonaDefault(next);
|
|
3617
|
+
savePersonaCards(next, personaByAgent);
|
|
3618
|
+
} else {
|
|
3619
|
+
const next = { ...(personaByAgent[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false }), enabled: checked };
|
|
3620
|
+
setPersonaByAgent((c) => ({ ...c, [personaEditing]: next }));
|
|
3621
|
+
savePersonaCards(personaDefault, { ...personaByAgent, [personaEditing]: next });
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
const addPersonaCard = () => {
|
|
3625
|
+
const id = personaNewName.trim();
|
|
3626
|
+
if (!id || id === "default" || personaByAgent[id]) return;
|
|
3627
|
+
const fresh = { name: id, personality: "", replyStyle: "", enabled: false };
|
|
3628
|
+
const next = { ...personaByAgent, [id]: fresh };
|
|
3629
|
+
setPersonaByAgent(next);
|
|
3630
|
+
setPersonaNewName("");
|
|
3631
|
+
setPersonaEditing(id);
|
|
3632
|
+
savePersonaCards(personaDefault, next);
|
|
3633
|
+
};
|
|
3634
|
+
const removePersonaCard = (id) => {
|
|
3635
|
+
if (id === "default" || !personaByAgent[id]) return;
|
|
3636
|
+
const next = { ...personaByAgent };
|
|
3637
|
+
delete next[id];
|
|
3638
|
+
setPersonaByAgent(next);
|
|
3639
|
+
if (personaEditing === id) setPersonaEditing("default");
|
|
3640
|
+
savePersonaCards(personaDefault, next);
|
|
3641
|
+
};
|
|
3642
|
+
|
|
2984
3643
|
async function putRules(next) {
|
|
2985
3644
|
await apiFetch("/api/dsh-mneme/rules", {
|
|
2986
3645
|
method: "PUT",
|
|
@@ -3162,18 +3821,7 @@ window.__ModuleLoader__.load({
|
|
|
3162
3821
|
}
|
|
3163
3822
|
|
|
3164
3823
|
return h("div", null,
|
|
3165
|
-
//
|
|
3166
|
-
// 全部零渲染)。钉子警示:安装时指定过版本号的 profile 会被 pnpm
|
|
3167
|
-
// 挡住常规升级(#174 报障者的实际成因);市场收录新发布约有 1 天延迟。
|
|
3168
|
-
updateInfo && h("section", { className: "mneme-set-card" },
|
|
3169
|
-
h("div", { className: "mneme-set-title" },
|
|
3170
|
-
t("memory.settings.version.outdated")
|
|
3171
|
-
.replace("{v}", updateInfo.latest)
|
|
3172
|
-
.replace("{c}", updateInfo.current)
|
|
3173
|
-
),
|
|
3174
|
-
h("div", { className: "mneme-featsubhint" }, t("memory.settings.version.outdatedHint"))
|
|
3175
|
-
),
|
|
3176
|
-
// —— 个性化:画像 / 规则 / 自定义指令(Agent 怎么对你) ——
|
|
3824
|
+
// —— 个性化:画像 / 人格 / 规则 / 自定义指令(Agent 怎么对你) ——
|
|
3177
3825
|
h("div", { className: "mneme-set-grouphead" }, t("memory.settings.group.personal")),
|
|
3178
3826
|
// 用户画像 — who the agent is talking to
|
|
3179
3827
|
h("section", { className: "mneme-set-card" },
|
|
@@ -3190,6 +3838,103 @@ window.__ModuleLoader__.load({
|
|
|
3190
3838
|
saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
|
|
3191
3839
|
)
|
|
3192
3840
|
),
|
|
3841
|
+
// 人格卡(beta,v1.2 多卡)— who the agent is:多张角色卡按会话身份注入。
|
|
3842
|
+
// 卡内开关只是该卡 enabled 半边;总闸 personaEnabled 在功能开关组,双闸同真。
|
|
3843
|
+
h("section", { className: "mneme-set-card" },
|
|
3844
|
+
h("div", { className: "mneme-set-title" }, t("memory.settings.persona")),
|
|
3845
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.personaHint")),
|
|
3846
|
+
// 卡列表:default + 各 byAgent 卡,点选切换编辑位;default 标注兜底位
|
|
3847
|
+
h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 } },
|
|
3848
|
+
h("button", {
|
|
3849
|
+
type: "button",
|
|
3850
|
+
className: "mneme-btn" + (personaEditing === "default" ? " mneme-btn--active" : ""),
|
|
3851
|
+
onClick: () => setPersonaEditing("default")
|
|
3852
|
+
}, t("memory.settings.personaDefaultCard")),
|
|
3853
|
+
Object.keys(personaByAgent).map((id) =>
|
|
3854
|
+
h("span", { key: id, style: { display: "inline-flex", alignItems: "center", gap: 2 } },
|
|
3855
|
+
h("button", {
|
|
3856
|
+
type: "button",
|
|
3857
|
+
className: "mneme-btn" + (personaEditing === id ? " mneme-btn--active" : ""),
|
|
3858
|
+
onClick: () => setPersonaEditing(id)
|
|
3859
|
+
}, id),
|
|
3860
|
+
h("button", {
|
|
3861
|
+
type: "button",
|
|
3862
|
+
className: "mneme-set-del",
|
|
3863
|
+
title: t("memory.settings.personaDeleteCard"),
|
|
3864
|
+
"aria-label": t("memory.settings.personaDeleteCard") + " " + id,
|
|
3865
|
+
onClick: () => removePersonaCard(id)
|
|
3866
|
+
}, "×")
|
|
3867
|
+
))
|
|
3868
|
+
),
|
|
3869
|
+
// 添加卡:输入 agent 身份名(注入时按它寻址)
|
|
3870
|
+
h("div", { style: { display: "flex", gap: 8 } },
|
|
3871
|
+
h("input", {
|
|
3872
|
+
className: "mneme-set-input",
|
|
3873
|
+
style: { marginBottom: 0 },
|
|
3874
|
+
value: personaNewName,
|
|
3875
|
+
placeholder: t("memory.settings.personaAddPlaceholder"),
|
|
3876
|
+
onChange: (e) => setPersonaNewName(e.target.value),
|
|
3877
|
+
onKeyDown: (e) => { if (e.key === "Enter") addPersonaCard(); }
|
|
3878
|
+
}),
|
|
3879
|
+
h("button", { className: "mneme-btn", onClick: addPersonaCard }, t("memory.settings.personaAdd"))
|
|
3880
|
+
),
|
|
3881
|
+
// 编辑位标题:default 卡 vs 具体身份卡
|
|
3882
|
+
h("div", { className: "mneme-set-desc" },
|
|
3883
|
+
personaEditing === "default"
|
|
3884
|
+
? t("memory.settings.personaEditingDefault")
|
|
3885
|
+
: t("memory.settings.personaEditingFor") + " " + personaEditing),
|
|
3886
|
+
// 可见字段标签(v1.1 反馈:aria-label 看不见,框上要有字)
|
|
3887
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaEnabledToggle")),
|
|
3888
|
+
h("div", { className: "mneme-set-row" },
|
|
3889
|
+
h("input", {
|
|
3890
|
+
type: "checkbox",
|
|
3891
|
+
checked: personaEditingCard.enabled === true,
|
|
3892
|
+
"aria-label": t("memory.settings.personaEnabledToggle"),
|
|
3893
|
+
onChange: (e) => toggleEditingPersona(e.target.checked)
|
|
3894
|
+
}),
|
|
3895
|
+
h("span", { className: "mneme-set-ruletext" },
|
|
3896
|
+
personaEditingCard.enabled === true
|
|
3897
|
+
? t("memory.settings.personaEnabledOn")
|
|
3898
|
+
: t("memory.settings.personaEnabledOff"))
|
|
3899
|
+
),
|
|
3900
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaName")),
|
|
3901
|
+
h("input", {
|
|
3902
|
+
className: "mneme-set-input",
|
|
3903
|
+
value: personaEditingCard.name,
|
|
3904
|
+
placeholder: t("memory.settings.personaNamePlaceholder"),
|
|
3905
|
+
onChange: (e) => updatePersonaCard("name", e.target.value)
|
|
3906
|
+
}),
|
|
3907
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaPersonality")),
|
|
3908
|
+
h("textarea", {
|
|
3909
|
+
className: "mneme-set-input mneme-set-input--area",
|
|
3910
|
+
value: personaEditingCard.personality,
|
|
3911
|
+
placeholder: t("memory.settings.personaPersonalityPlaceholder"),
|
|
3912
|
+
onChange: (e) => updatePersonaCard("personality", e.target.value)
|
|
3913
|
+
}),
|
|
3914
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaReplyStyle")),
|
|
3915
|
+
h("textarea", {
|
|
3916
|
+
className: "mneme-set-input mneme-set-input--area",
|
|
3917
|
+
value: personaEditingCard.replyStyle,
|
|
3918
|
+
placeholder: t("memory.settings.personaReplyStylePlaceholder"),
|
|
3919
|
+
onChange: (e) => updatePersonaCard("replyStyle", e.target.value)
|
|
3920
|
+
}),
|
|
3921
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
3922
|
+
h("button", { className: "mneme-btn", onClick: saveEditingPersona }, t("memory.settings.personaSave")),
|
|
3923
|
+
personaSaved && h("span", { className: "mneme-saved" }, t("memory.settings.personaSaved")),
|
|
3924
|
+
personaError && h("span", { className: "mneme-saved", style: { color: "var(--dsw-danger, #d33)" } },
|
|
3925
|
+
t("memory.settings.personaSaveFailed") + " (" + personaError + ")")
|
|
3926
|
+
)
|
|
3927
|
+
),
|
|
3928
|
+
// Agent 眼中的画像(只读)— 注入生效文本:profile + rules 原样展示。
|
|
3929
|
+
// 纯前端拼接(GET 已在 load 里拿到 profile/rules),零后端新请求。
|
|
3930
|
+
h("section", { className: "mneme-set-card" },
|
|
3931
|
+
h("div", { className: "mneme-set-title" }, t("memory.settings.personaView")),
|
|
3932
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.personaViewHint")),
|
|
3933
|
+
(profile.trim() || rules.length)
|
|
3934
|
+
? h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } },
|
|
3935
|
+
[profile.trim() ? "- " + profile.trim() : "", ...rules.map((r) => "- " + r)].filter(Boolean).join("\n"))
|
|
3936
|
+
: h("div", { className: "mneme-xempty" }, t("memory.settings.personaViewEmpty"))
|
|
3937
|
+
),
|
|
3193
3938
|
// 规则 — numbered rows, hover reveals the delete affordance
|
|
3194
3939
|
h("section", { className: "mneme-set-card" },
|
|
3195
3940
|
h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
|
|
@@ -3417,7 +4162,7 @@ window.__ModuleLoader__.load({
|
|
|
3417
4162
|
),
|
|
3418
4163
|
apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
|
|
3419
4164
|
),
|
|
3420
|
-
// ——
|
|
4165
|
+
// —— 帮助:支持与鼓励(图标行 + 检测更新 + star,兼当「关于」收尾) ——
|
|
3421
4166
|
h("div", { className: "mneme-set-grouphead" }, t("memory.settings.group.help")),
|
|
3422
4167
|
// 反馈三入口:新建 issue 预填(版本+平台)/ 邮件 / 浏览已知问题。
|
|
3423
4168
|
// 纯前端链接零后端成本;插件版本来自 /info,平台取 userAgent。GitHub
|
|
@@ -3433,6 +4178,21 @@ window.__ModuleLoader__.load({
|
|
|
3433
4178
|
feedRow("search", t("memory.settings.feedback.browse"), t("memory.settings.feedback.browse.sub"),
|
|
3434
4179
|
{ href: "https://github.com/slow-stack/mneme/issues", target: "_blank", rel: "noopener noreferrer" })
|
|
3435
4180
|
),
|
|
4181
|
+
// 检测更新:手动触发(复验修订——原顶部自动横幅移除),结果内联;
|
|
4182
|
+
// outdated 时提示带 pnpm 钉子警示(#174 报障成因)。
|
|
4183
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
4184
|
+
h("button", { className: "mneme-btn", onClick: checkForUpdate, disabled: updateChecking },
|
|
4185
|
+
updateChecking ? "…" : t("memory.settings.update.check")),
|
|
4186
|
+
updateInfo && h("span", { className: "mneme-feedsub" }, checkResultText())
|
|
4187
|
+
),
|
|
4188
|
+
updateInfo?.status === "outdated" && h("div", { className: "mneme-set-hint", style: { marginTop: 8 } },
|
|
4189
|
+
t("memory.settings.version.outdatedHint")),
|
|
4190
|
+
// star 鼓励入口:链接到仓库主页而非 stargazers——前者右上角就有
|
|
4191
|
+
// Star 按钮,后者只是「看谁点过」的名单页(复验实测反馈)。
|
|
4192
|
+
h("div", { className: "mneme-feedlist", style: { marginTop: 12 } },
|
|
4193
|
+
feedRow("star", t("memory.settings.star"), t("memory.settings.star.sub"),
|
|
4194
|
+
{ href: "https://github.com/slow-stack/mneme", target: "_blank", rel: "noopener noreferrer" })
|
|
4195
|
+
),
|
|
3436
4196
|
h("div", { className: "mneme-set-foot" },
|
|
3437
4197
|
h("span", null, `dsh-mneme v${pkgVersion}`)
|
|
3438
4198
|
)
|
|
@@ -4118,7 +4878,18 @@ window.__ModuleLoader__.load({
|
|
|
4118
4878
|
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
4119
4879
|
return String(n);
|
|
4120
4880
|
};
|
|
4121
|
-
|
|
4881
|
+
// 热力分档:固定阈值(75万/150万/250万)在低用量库里整片落在 l1,档位失真
|
|
4882
|
+
// (复验第三轮「颜色不怎么明显」)。改按 56 天窗口内的非零日做三分位切四档
|
|
4883
|
+
// ——GitHub 贡献图同思路,档次随库的实际用量自适应拉开;全零/无数据退化
|
|
4884
|
+
// 为全 0 档(不留空档位)。边界是三个分位值(q 只返回 3 个):n>p75 → l4、
|
|
4885
|
+
// >p50 → l3、>p25 → l2、其余非零 → l1,四档都有 populated。
|
|
4886
|
+
const heatLevel = (n, q) => (n <= 0 ? 0 : n > q[2] ? 4 : n > q[1] ? 3 : n > q[0] ? 2 : 1);
|
|
4887
|
+
const heatQuartiles = (days) => {
|
|
4888
|
+
const pos = days.map((d) => d.total).filter((v) => v > 0).sort((a, b) => a - b);
|
|
4889
|
+
if (!pos.length) return [0, 0, 0];
|
|
4890
|
+
const pick = (p) => pos[Math.min(pos.length - 1, Math.floor(p * pos.length))];
|
|
4891
|
+
return [pick(0.25), pick(0.5), pick(0.75)];
|
|
4892
|
+
};
|
|
4122
4893
|
|
|
4123
4894
|
function TokenFlowSection({ t }) {
|
|
4124
4895
|
const [stats, setStats] = useState(null);
|
|
@@ -4228,6 +4999,33 @@ window.__ModuleLoader__.load({
|
|
|
4228
4999
|
heatCells.push({ key, total: byDayMap.get(key) ?? 0 });
|
|
4229
5000
|
}
|
|
4230
5001
|
}
|
|
5002
|
+
// 分档阈值取自本窗口真实分布(见 heatQuartiles 注释),空窗口退化全 0 档。
|
|
5003
|
+
const heatQ = heatQuartiles(heatCells);
|
|
5004
|
+
// 月份标签:GitHub 同款——按周列贴,列首日期属于哪个月,那列就标那个
|
|
5005
|
+
// 月;连续同月的列合并成一段(gridColumn: from / span)。月不在列首
|
|
5006
|
+
// 开始时其首列仍标上一月(列首归属优先),这是贡献图的通行近似。
|
|
5007
|
+
const monthLabels = (() => {
|
|
5008
|
+
if (!heatCells.length) return [];
|
|
5009
|
+
const spans = [];
|
|
5010
|
+
heatCells.forEach((c, i) => {
|
|
5011
|
+
if (i % 7 !== 0) return; // 只看列首
|
|
5012
|
+
const ym = c.key.slice(0, 7);
|
|
5013
|
+
const col = Math.floor(i / 7) + 1;
|
|
5014
|
+
const label = `${Number(ym.slice(5, 7))}月`;
|
|
5015
|
+
const lastSpan = spans[spans.length - 1];
|
|
5016
|
+
if (lastSpan && lastSpan.key === ym) { lastSpan.last = col; return; }
|
|
5017
|
+
spans.push({ key: ym, label, from: col, last: col });
|
|
5018
|
+
});
|
|
5019
|
+
return spans.map((m) => ({ ...m, span: m.last - m.from + 1 }));
|
|
5020
|
+
})();
|
|
5021
|
+
// 行标签:行 r 的星期 = (首列星期 + r) % 7(getUTCDay 周日=0)。一/三/五
|
|
5022
|
+
// 三行有字(GitHub 同款),其余留空。
|
|
5023
|
+
const heatRowLabels = (() => {
|
|
5024
|
+
const startDow = heatCells.length ? new Date(`${heatCells[0].key}T00:00:00Z`).getUTCDay() : 0;
|
|
5025
|
+
const rows = new Array(7).fill("");
|
|
5026
|
+
for (const d of [1, 3, 5]) rows[(d - startDow + 7) % 7] = ["一", "三", "五"][[1, 3, 5].indexOf(d)];
|
|
5027
|
+
return rows;
|
|
5028
|
+
})();
|
|
4231
5029
|
const kpi = (title, num, cap) =>
|
|
4232
5030
|
h(StatusCard, { t, title, loading, error: statsError, num, cap });
|
|
4233
5031
|
const segClass = (s) =>
|
|
@@ -4321,12 +5119,22 @@ window.__ModuleLoader__.load({
|
|
|
4321
5119
|
days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
|
|
4322
5120
|
),
|
|
4323
5121
|
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
5122
|
+
// 月份行与格子列同用列主序网格对齐:每月占的列数 = 该月在这个
|
|
5123
|
+
// 56 天窗口里的列数(按列的起始日期算),标签只在该列起点画一次。
|
|
5124
|
+
h("div", { className: "mneme-heatmonths" },
|
|
5125
|
+
monthLabels.map((m) => h("span", { key: m.key, style: { gridColumn: `${m.from} / span ${m.span}` } }, m.label))
|
|
5126
|
+
),
|
|
5127
|
+
h("div", { className: "mneme-heatwrap" },
|
|
5128
|
+
h("div", { className: "mneme-heatrows", "aria-hidden": "true" },
|
|
5129
|
+
heatRowLabels.map((d, i) => h("span", { key: i }, d))
|
|
5130
|
+
),
|
|
5131
|
+
h("div", { className: "mneme-heatgrid" },
|
|
5132
|
+
heatCells.map((c) => h("div", {
|
|
5133
|
+
key: c.key,
|
|
5134
|
+
className: heatLevel(c.total, heatQ) ? `mneme-heatcell l${heatLevel(c.total, heatQ)}` : "mneme-heatcell",
|
|
5135
|
+
title: `${c.key} · ${c.total.toLocaleString()}`
|
|
5136
|
+
}))
|
|
5137
|
+
)
|
|
4330
5138
|
),
|
|
4331
5139
|
h("div", { className: "mneme-heatlegend" },
|
|
4332
5140
|
h("span", null, t("memory.status.token.heatLess")),
|
|
@@ -5805,6 +6613,9 @@ window.__ModuleLoader__.load({
|
|
|
5805
6613
|
),
|
|
5806
6614
|
),
|
|
5807
6615
|
view === "memory" && h("div", { className: "mneme-xmain" },
|
|
6616
|
+
// 两栏布局(桉桉规格):左栏上段=语义检索(怎么搜),下段=重要性/
|
|
6617
|
+
// 来源/热度(留什么);原第二侧栏的类型列表与计数刷新下沉到内容区
|
|
6618
|
+
// 工具条(.mneme-xbrowsebar),五个功能入口一个不丢。
|
|
5808
6619
|
h("div", { className: "mneme-xside" },
|
|
5809
6620
|
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
|
|
5810
6621
|
h("div", { className: "mneme-xsearchwrap" },
|
|
@@ -5845,39 +6656,28 @@ window.__ModuleLoader__.load({
|
|
|
5845
6656
|
["all", "7d", "30d", "90d"].map((k) =>
|
|
5846
6657
|
h("option", { key: k, value: k }, t(`memory.explorer.date.${k}`))))
|
|
5847
6658
|
),
|
|
5848
|
-
h("div", { className: "mneme-
|
|
5849
|
-
h("
|
|
5850
|
-
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
|
|
5851
|
-
h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
|
|
5852
|
-
)
|
|
5853
|
-
),
|
|
5854
|
-
h("div", { className: "mneme-xside mneme-xside--filter" },
|
|
5855
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
|
|
5856
|
-
h("div", { className: "mneme-xrow" },
|
|
5857
|
-
[0, 3, 4, 5].map((v) =>
|
|
5858
|
-
h("button", {
|
|
5859
|
-
key: v,
|
|
5860
|
-
className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
|
|
5861
|
-
onClick: () => setMinImp(v)
|
|
5862
|
-
}, v === 0 ? t("memory.tab.all") : h(react.Fragment, null, h(StarGlyph, { size: 11 }), ` ${v}+`)))
|
|
5863
|
-
),
|
|
5864
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sourceFilter")),
|
|
5865
|
-
h("div", { className: "mneme-xrow" },
|
|
5866
|
-
h("button", {
|
|
5867
|
-
className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
|
|
5868
|
-
title: t("memory.explorer.filterDepositedHint"),
|
|
5869
|
-
onClick: () => setDepositedOnly(!depositedOnly)
|
|
5870
|
-
}, t("memory.explorer.filterDeposited")),
|
|
5871
|
-
h("button", {
|
|
5872
|
-
className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
|
|
5873
|
-
title: t("memory.explorer.filterArchivedHint"),
|
|
5874
|
-
onClick: () => setArchivedOnly(!archivedOnly)
|
|
5875
|
-
}, t("memory.explorer.filterArchived"))
|
|
5876
|
-
),
|
|
5877
|
-
heatAvailable && h(react.Fragment, null,
|
|
5878
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sort")),
|
|
6659
|
+
h("div", { className: "mneme-xside-imp" },
|
|
6660
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
|
|
5879
6661
|
h("div", { className: "mneme-xrow" },
|
|
6662
|
+
[0, 3, 4, 5].map((v) =>
|
|
6663
|
+
h("button", {
|
|
6664
|
+
key: v,
|
|
6665
|
+
className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
|
|
6666
|
+
onClick: () => setMinImp(v)
|
|
6667
|
+
}, v === 0 ? t("memory.tab.all") : h(react.Fragment, null, h(StarGlyph, { size: 11 }), ` ${v}+`)))
|
|
6668
|
+
),
|
|
6669
|
+
h("div", { className: "mneme-xrow" },
|
|
6670
|
+
h("button", {
|
|
6671
|
+
className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
|
|
6672
|
+
title: t("memory.explorer.filterDepositedHint"),
|
|
6673
|
+
onClick: () => setDepositedOnly(!depositedOnly)
|
|
6674
|
+
}, t("memory.explorer.filterDeposited")),
|
|
5880
6675
|
h("button", {
|
|
6676
|
+
className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
|
|
6677
|
+
title: t("memory.explorer.filterArchivedHint"),
|
|
6678
|
+
onClick: () => setArchivedOnly(!archivedOnly)
|
|
6679
|
+
}, t("memory.explorer.filterArchived")),
|
|
6680
|
+
heatAvailable && h("button", {
|
|
5881
6681
|
className: heatSort ? "mneme-chip mneme-active" : "mneme-chip",
|
|
5882
6682
|
title: t("memory.explorer.heatSortHint"),
|
|
5883
6683
|
onClick: () => {
|
|
@@ -5886,23 +6686,28 @@ window.__ModuleLoader__.load({
|
|
|
5886
6686
|
}
|
|
5887
6687
|
}, t("memory.explorer.heatSort"))
|
|
5888
6688
|
)
|
|
5889
|
-
)
|
|
5890
|
-
h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
|
|
5891
|
-
h("button", {
|
|
5892
|
-
className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
5893
|
-
onClick: () => setType("all")
|
|
5894
|
-
}, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
|
|
5895
|
-
knownTypes.concat(extraTypes).map((key) =>
|
|
5896
|
-
h("button", {
|
|
5897
|
-
key,
|
|
5898
|
-
className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
5899
|
-
onClick: () => setType(key)
|
|
5900
|
-
},
|
|
5901
|
-
h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
|
|
5902
|
-
h("span", null, typeLabel(t, key)),
|
|
5903
|
-
h("span", { className: "mneme-xcount2" }, String(counts[key]))
|
|
5904
|
-
))
|
|
6689
|
+
)
|
|
5905
6690
|
),
|
|
6691
|
+
h("div", { className: "mneme-xcontent" },
|
|
6692
|
+
h("div", { className: "mneme-xbrowsebar" },
|
|
6693
|
+
h("button", {
|
|
6694
|
+
className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
6695
|
+
onClick: () => setType("all")
|
|
6696
|
+
}, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
|
|
6697
|
+
knownTypes.concat(extraTypes).map((key) =>
|
|
6698
|
+
h("button", {
|
|
6699
|
+
key,
|
|
6700
|
+
className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
|
|
6701
|
+
onClick: () => setType(key)
|
|
6702
|
+
},
|
|
6703
|
+
h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
|
|
6704
|
+
h("span", null, typeLabel(t, key)),
|
|
6705
|
+
h("span", { className: "mneme-xcount2" }, String(counts[key]))
|
|
6706
|
+
)),
|
|
6707
|
+
h("span", { className: "mneme-xcount", style: { marginLeft: "auto" } }, t("memory.explorer.count").replace("{n}", String(visible.length))),
|
|
6708
|
+
h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
|
|
6709
|
+
h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
|
|
6710
|
+
),
|
|
5906
6711
|
viewMode === "cards"
|
|
5907
6712
|
? h("div", { className: "mneme-cards" },
|
|
5908
6713
|
loading
|
|
@@ -6007,8 +6812,11 @@ window.__ModuleLoader__.load({
|
|
|
6007
6812
|
)
|
|
6008
6813
|
),
|
|
6009
6814
|
),
|
|
6815
|
+
),
|
|
6010
6816
|
// 详情抽屉:卡片/时间线共用,选中即从右侧滑出(替代旧底部面板)。
|
|
6011
6817
|
// 两步删除确认在抽屉内完成;编辑/归档经 /update 落库并同步本地视图。
|
|
6818
|
+
// 保持为 .mneme-xmain 直接子元素:抽屉锚在 xmain 的 relative 层上,随
|
|
6819
|
+
// 两栏改版内容区变宽,抽屉锚点不变。
|
|
6012
6820
|
selected && h(MemoryDrawer, {
|
|
6013
6821
|
key: selected.id,
|
|
6014
6822
|
t,
|
|
@@ -6274,6 +7082,1045 @@ window.__ModuleLoader__.load({
|
|
|
6274
7082
|
), host);
|
|
6275
7083
|
}
|
|
6276
7084
|
|
|
7085
|
+
// ---------------------------------------------------------------------
|
|
7086
|
+
// v0.9.0 M.O.(beta,只读)
|
|
7087
|
+
// 定性:trace viewer 不是编辑画布——节点=步骤、边=依赖、一次运行=一条
|
|
7088
|
+
// 被高亮走过的路径。本版数据全部是 §4 mneme.pipeline/v1 形状的假数据,
|
|
7089
|
+
// 服务端管线落地前先验证形态与交互。
|
|
7090
|
+
// ---------------------------------------------------------------------
|
|
7091
|
+
const PIPELINE_ACTIONS_TAB_ID = "dsh-mneme:actions";
|
|
7092
|
+
const PIPELINE_ACTIONS_TAB_KIND = "mneme-actions";
|
|
7093
|
+
|
|
7094
|
+
// 节点状态四值,refuted 是全篇核心信号(最扎眼)。
|
|
7095
|
+
const PIPELINE_STATES = ["candidate", "trusted", "stale", "refuted"];
|
|
7096
|
+
|
|
7097
|
+
// 数据面 = **项目里**的工作流(服务端 /api/dsh-mneme/pipeline 投影成 mneme.pipeline/v1)。
|
|
7098
|
+
// 项目从哪来:只有客户端拿得到会话的工作目录,所以这里解析出 cwd、当 ?project= 带上;
|
|
7099
|
+
// 服务端没有「当前项目」这个概念,也不许替谁猜一个。
|
|
7100
|
+
// 为什么四个座位共用缓存、且缓存**按项目 + 按工作流**分桶:四个座位读的必须是同一份
|
|
7101
|
+
// ——「会话头说有 3 条、右栏说有 5 条」这种分家没人查得出来;同理,同一个进程里开着
|
|
7102
|
+
// 两个不同项目的会话时,不分桶就会把 A 项目的清单端给 B 项目看,而画面看不出错。
|
|
7103
|
+
const pipelineDocs = new Map();
|
|
7104
|
+
const pipelinePendingByKey = new Map();
|
|
7105
|
+
function pipelineKey(project, id) { return (project || "") + "\u0000" + (id || ""); }
|
|
7106
|
+
function loadPipeline(project, id) {
|
|
7107
|
+
const key = pipelineKey(project, id);
|
|
7108
|
+
if (!pipelinePendingByKey.has(key)) {
|
|
7109
|
+
const params = [];
|
|
7110
|
+
if (project) params.push("project=" + encodeURIComponent(project));
|
|
7111
|
+
if (id) params.push("id=" + encodeURIComponent(id));
|
|
7112
|
+
const url = "/api/dsh-mneme/pipeline" + (params.length ? "?" + params.join("&") : "");
|
|
7113
|
+
pipelinePendingByKey.set(key, apiFetch(url)
|
|
7114
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
7115
|
+
.catch(() => null)
|
|
7116
|
+
.then((doc) => { pipelineDocs.set(key, doc); return doc; }));
|
|
7117
|
+
}
|
|
7118
|
+
return pipelinePendingByKey.get(key);
|
|
7119
|
+
}
|
|
7120
|
+
// undefined = 还在读;null = 读不出来;对象 = 拿到了(其中 pipeline 为 null 表示
|
|
7121
|
+
// 这个项目里还没有工作流——那是空态,不是错误,别把两者画成同一张脸)。
|
|
7122
|
+
// id 为空 = 取清单(服务端会顺带给出第一条的完整投影,够结算卡用)。
|
|
7123
|
+
function usePipeline(project, id) {
|
|
7124
|
+
const key = pipelineKey(project, id);
|
|
7125
|
+
const [doc, setDoc] = useState(pipelineDocs.has(key) ? pipelineDocs.get(key) : undefined);
|
|
7126
|
+
useEffect(() => {
|
|
7127
|
+
let alive = true;
|
|
7128
|
+
loadPipeline(project, id).then((d) => { if (alive) setDoc(d); });
|
|
7129
|
+
return () => { alive = false; };
|
|
7130
|
+
}, [key]);
|
|
7131
|
+
return doc;
|
|
7132
|
+
}
|
|
7133
|
+
// 会话工作目录。两条来源依次试,各自兜自己的异常——一条抛了不能把另一条也带走;
|
|
7134
|
+
// 两处都试不到就**返回 null**(面板退到全局层,并如实标「项目未知」),绝不退回进程
|
|
7135
|
+
// 目录之类的东西——那会把「读不到」变成「读到别处」,而后者在界面上看不出来,是最难
|
|
7136
|
+
// 查的一类错。
|
|
7137
|
+
// 首选会话列表快照:byId[sessionId].cwd 是宿主给客户端的公开形状(dsh-context 用的
|
|
7138
|
+
// 就是这条);sessions.get() 是宿主侧写法,客户端不一定有,所以放第二位。
|
|
7139
|
+
function sessionProject(ctx, sessionId) {
|
|
7140
|
+
if (!sessionId || !ctx || typeof ctx.get !== "function") return null;
|
|
7141
|
+
const warn = (err) => console.warn("[dsh-mneme] session cwd lookup failed:", err);
|
|
7142
|
+
const pick = (row) => {
|
|
7143
|
+
if (!row) return null;
|
|
7144
|
+
const cwd = (row.header && row.header.cwd) || row.cwd;
|
|
7145
|
+
return typeof cwd === "string" && cwd ? cwd : null;
|
|
7146
|
+
};
|
|
7147
|
+
let sessions = null;
|
|
7148
|
+
try { sessions = ctx.get("sessions"); } catch (err) { warn(err); }
|
|
7149
|
+
if (sessions) {
|
|
7150
|
+
try {
|
|
7151
|
+
const snap = sessions.list && typeof sessions.list.getSnapshot === "function"
|
|
7152
|
+
? sessions.list.getSnapshot()
|
|
7153
|
+
: null;
|
|
7154
|
+
const hit = pick(snap && snap.byId ? snap.byId[sessionId] : null);
|
|
7155
|
+
if (hit) return hit;
|
|
7156
|
+
} catch (err) { warn(err); }
|
|
7157
|
+
try {
|
|
7158
|
+
if (typeof sessions.get === "function") {
|
|
7159
|
+
const hit = pick(sessions.get(sessionId));
|
|
7160
|
+
if (hit) return hit;
|
|
7161
|
+
}
|
|
7162
|
+
} catch (err) { warn(err); }
|
|
7163
|
+
}
|
|
7164
|
+
return null;
|
|
7165
|
+
}
|
|
7166
|
+
|
|
7167
|
+
// DAG 分层布局:Kahn 最长路分层(层数 = 到该节点最长边数),
|
|
7168
|
+
// 防环——正常分层算完后如果还有节点没落位(说明有环),
|
|
7169
|
+
// 按输入顺序塞进最后一层,绝不死循环。节点数少,几十行够用。
|
|
7170
|
+
function layoutDag(nodes, edges) {
|
|
7171
|
+
const byId = {};
|
|
7172
|
+
for (const n of nodes) byId[n.id] = n;
|
|
7173
|
+
const indeg = {};
|
|
7174
|
+
const out = {};
|
|
7175
|
+
for (const n of nodes) { indeg[n.id] = 0; out[n.id] = []; }
|
|
7176
|
+
for (const e of edges) {
|
|
7177
|
+
if (!byId[e.from] || !byId[e.to] || e.from === e.to) continue;
|
|
7178
|
+
out[e.from].push(e.to);
|
|
7179
|
+
indeg[e.to] += 1;
|
|
7180
|
+
}
|
|
7181
|
+
const layer = {};
|
|
7182
|
+
const queue = nodes.filter((n) => indeg[n.id] === 0).map((n) => n.id);
|
|
7183
|
+
const remaining = {};
|
|
7184
|
+
for (const n of nodes) remaining[n.id] = indeg[n.id];
|
|
7185
|
+
while (queue.length) {
|
|
7186
|
+
const id = queue.shift();
|
|
7187
|
+
for (const to of out[id]) {
|
|
7188
|
+
layer[to] = Math.max(layer[to] || 0, (layer[id] || 0) + 1);
|
|
7189
|
+
remaining[to] -= 1;
|
|
7190
|
+
if (remaining[to] === 0) queue.push(to);
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
const maxLayer = nodes.reduce((m, n) => Math.max(m, layer[n.id] || 0), 0);
|
|
7194
|
+
const leftovers = nodes.filter((n) => remaining[n.id] > 0);
|
|
7195
|
+
for (const n of leftovers) layer[n.id] = maxLayer + 1; // 环兜底
|
|
7196
|
+
const layers = [];
|
|
7197
|
+
for (const n of nodes) {
|
|
7198
|
+
const L = layer[n.id] || 0;
|
|
7199
|
+
(layers[L] = layers[L] || []).push(n.id);
|
|
7200
|
+
}
|
|
7201
|
+
return { layer, layers: layers.map((l) => l || []) };
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
// 阶段带布局(2026-10-16):layoutDag 只按 DAG 深度分层、不读 phase,把 5 段
|
|
7205
|
+
// 19 步排成 18 层的单列长链(固有 354×1902px),第一屏既看不出「分几段」也
|
|
7206
|
+
// 读不出「谁在做」。这里按 step.phase 分组(保持作者书写顺序——phase 是人手
|
|
7207
|
+
// 写的分类词,不能按字母重排)、段内左到右每行两列换行:画布固有宽度落回
|
|
7208
|
+
// 354px(右栏 400px 不缩放就能读),高度从 1902 腰斩到 ~836。
|
|
7209
|
+
// 纯函数、不碰 DOM:探针 .scratch/mo-ux-probe-layout.mjs 会把它抠出来真跑,
|
|
7210
|
+
// 改签名或引用外部变量都会让那条可重跑核算失明。
|
|
7211
|
+
function layoutPipelineBands(nodes, edges, C) {
|
|
7212
|
+
const NW = (C && C.NW) || 150;
|
|
7213
|
+
const NH = (C && C.NH) || 54;
|
|
7214
|
+
const GX = (C && C.GX) || 30;
|
|
7215
|
+
const PAD = (C && C.PAD) || 12;
|
|
7216
|
+
const COLS = 2, ROW_GAP = 14, BAND_HEAD = 26, BAND_GAP = 18;
|
|
7217
|
+
const order = [];
|
|
7218
|
+
const byPhase = {};
|
|
7219
|
+
for (const n of nodes) {
|
|
7220
|
+
const ph = n.phase || "";
|
|
7221
|
+
if (!byPhase[ph]) { byPhase[ph] = []; order.push(ph); }
|
|
7222
|
+
byPhase[ph].push(n.id);
|
|
7223
|
+
}
|
|
7224
|
+
const pos = {};
|
|
7225
|
+
const bands = [];
|
|
7226
|
+
const bandRects = [];
|
|
7227
|
+
const W = PAD * 2 + COLS * NW + (COLS - 1) * GX;
|
|
7228
|
+
let y = PAD;
|
|
7229
|
+
for (const ph of order) {
|
|
7230
|
+
const ids = byPhase[ph];
|
|
7231
|
+
const rows = Math.ceil(ids.length / COLS);
|
|
7232
|
+
const top = y;
|
|
7233
|
+
y += BAND_HEAD;
|
|
7234
|
+
ids.forEach((id, i) => {
|
|
7235
|
+
const r = Math.floor(i / COLS), c = i % COLS;
|
|
7236
|
+
pos[id] = { x: PAD + c * (NW + GX), y: y + r * (NH + ROW_GAP) };
|
|
7237
|
+
});
|
|
7238
|
+
y += rows * NH + (rows - 1) * ROW_GAP;
|
|
7239
|
+
bands.push({ name: ph, count: ids.length, top, bottom: y });
|
|
7240
|
+
bandRects.push({ name: ph, count: ids.length, x: PAD - 6, y: top, w: W - 2 * (PAD - 6), h: y - top });
|
|
7241
|
+
y += BAND_GAP;
|
|
7242
|
+
}
|
|
7243
|
+
const H = y - BAND_GAP + PAD;
|
|
7244
|
+
return { pos, W, H, bands, bandRects };
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
// 标题两行拆分:pipeClip 一刀截 11 字把「定版本号与发布范围」截成「…发布范…」。
|
|
7248
|
+
// 节点加高到 54px 后装得下两行——标本 19 个标题里 7 个超 11 字、最长 22 字,
|
|
7249
|
+
// 恰好两行放全;超长兜底仍留省略号。
|
|
7250
|
+
function pipeTitleLines(s, n) {
|
|
7251
|
+
const str = String(s || "");
|
|
7252
|
+
if (str.length <= n) return [str];
|
|
7253
|
+
const head = str.slice(0, n);
|
|
7254
|
+
const rest = str.slice(n);
|
|
7255
|
+
return [head, rest.length > n ? rest.slice(0, n - 1) + "…" : rest];
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
function pipeFmtWhen(iso) {
|
|
7259
|
+
if (!iso) return "";
|
|
7260
|
+
const d = new Date(iso);
|
|
7261
|
+
if (isNaN(d.getTime())) return "";
|
|
7262
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
7263
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
7264
|
+
}
|
|
7265
|
+
function pipeClip(s, n) {
|
|
7266
|
+
const str = String(s || "");
|
|
7267
|
+
return str.length > n ? str.slice(0, n - 1) + "…" : str;
|
|
7268
|
+
}
|
|
7269
|
+
function pipeDaysAgo(iso) {
|
|
7270
|
+
if (!iso) return null;
|
|
7271
|
+
const d = new Date(iso);
|
|
7272
|
+
if (isNaN(d.getTime())) return null;
|
|
7273
|
+
return Math.max(0, Math.floor((Date.now() - d.getTime()) / 86400000));
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
// 运行档位:一次运行一个芯片;失败/被推翻的运行照常列出(留痕)。
|
|
7277
|
+
function PipelineRunChips(props) {
|
|
7278
|
+
const { t, runs, selectedRunId, onSelectRun } = props;
|
|
7279
|
+
return h("div", { className: "mneme-pipe-runs", role: "group", "aria-label": t("memory.actions.runs") },
|
|
7280
|
+
h("span", { className: "mneme-pipe-scope" }, t("memory.actions.runs")),
|
|
7281
|
+
(runs || []).map((r, i) => h("button", {
|
|
7282
|
+
key: r.id || i,
|
|
7283
|
+
type: "button",
|
|
7284
|
+
className: "mneme-pipe-run" + (r.id === selectedRunId ? " mneme-on" : ""),
|
|
7285
|
+
onClick: () => onSelectRun && onSelectRun(r.id),
|
|
7286
|
+
title: t("memory.actions.run." + (r.outcome || "ok"))
|
|
7287
|
+
},
|
|
7288
|
+
h("span", { className: "mneme-pipe-runDot mneme-pipe-runDot--" + (r.outcome || "ok"), "aria-hidden": "true" }),
|
|
7289
|
+
t("memory.actions.runOf").replace("{n}", String(i + 1)) + " · " + pipeFmtWhen(r.at)
|
|
7290
|
+
))
|
|
7291
|
+
);
|
|
7292
|
+
}
|
|
7293
|
+
|
|
7294
|
+
// 宽态 DAG:按 phase 分阶段带的 SVG,一次运行 = 一条高亮路径(trace viewer 读法)。
|
|
7295
|
+
// 分组与几何来自 layoutPipelineBands(纯函数,探针真跑它),这里只管画。
|
|
7296
|
+
function PipelineGraphSvg(props) {
|
|
7297
|
+
const { t, data, run, selectedNodeId, onSelectNode } = props;
|
|
7298
|
+
const nodes = data.nodes || [];
|
|
7299
|
+
const edges = data.edges || [];
|
|
7300
|
+
const pathSet = {};
|
|
7301
|
+
const edgeState = {};
|
|
7302
|
+
if (run && Array.isArray(run.path)) {
|
|
7303
|
+
for (const id of run.path) pathSet[id] = true;
|
|
7304
|
+
for (let i = 0; i + 1 < run.path.length; i++) {
|
|
7305
|
+
const a = run.path[i];
|
|
7306
|
+
const b = run.path[i + 1];
|
|
7307
|
+
const isLast = i + 2 === run.path.length;
|
|
7308
|
+
edgeState[a + ">" + b] = run.outcome === "failed" && isLast ? "fail" : "on";
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
const usedSet = {};
|
|
7312
|
+
if (run && Array.isArray(run.usedNodeIds)) {
|
|
7313
|
+
for (const id of run.usedNodeIds) usedSet[id] = true;
|
|
7314
|
+
}
|
|
7315
|
+
const NW = 150, NH = 54, GX = 30, GY = 66, PAD = 12;
|
|
7316
|
+
const laid = layoutPipelineBands(nodes, edges, { NW, NH, GX, GY, PAD });
|
|
7317
|
+
const pos = laid.pos;
|
|
7318
|
+
const W = laid.W;
|
|
7319
|
+
const Hh = laid.H;
|
|
7320
|
+
const KIND_RX = { step: 8, gate: 4, branch: 16, artifact: 21 };
|
|
7321
|
+
return h("svg", {
|
|
7322
|
+
className: "mneme-pipe-svg",
|
|
7323
|
+
viewBox: `0 0 ${W} ${Hh}`,
|
|
7324
|
+
width: W,
|
|
7325
|
+
height: Hh,
|
|
7326
|
+
role: "img",
|
|
7327
|
+
"aria-label": t("memory.actions.title")
|
|
7328
|
+
},
|
|
7329
|
+
// 阶段带:段底 + 段名与该段步数。分段信息画在节点之下、边之下——
|
|
7330
|
+
// 它是背景不是主角,但必须先上屏,第一屏才读得出「分几段」。
|
|
7331
|
+
laid.bandRects.map((b, i) => h("g", { key: "band" + i },
|
|
7332
|
+
h("rect", {
|
|
7333
|
+
className: "mneme-pipe-bandbg",
|
|
7334
|
+
x: b.x, y: b.y, width: b.w, height: b.h, rx: 10
|
|
7335
|
+
}),
|
|
7336
|
+
i > 0 ? h("line", {
|
|
7337
|
+
className: "mneme-pipe-bandline",
|
|
7338
|
+
x1: PAD, x2: W - PAD, y1: b.y - 9, y2: b.y - 9
|
|
7339
|
+
}) : null,
|
|
7340
|
+
h("text", {
|
|
7341
|
+
className: "mneme-pipe-bandT",
|
|
7342
|
+
x: PAD + 4, y: b.y + 17
|
|
7343
|
+
}, (b.name || "") + " · " + t("memory.actions.band.count").replace("{n}", String(b.count)))
|
|
7344
|
+
)),
|
|
7345
|
+
edges.map((e) => {
|
|
7346
|
+
const a = pos[e.from];
|
|
7347
|
+
const b = pos[e.to];
|
|
7348
|
+
if (!a || !b) return null;
|
|
7349
|
+
const st = edgeState[e.from + ">" + e.to];
|
|
7350
|
+
const cls = st === "fail" ? "mneme-pipe-edgeFail" : st === "on" ? "mneme-pipe-edgeOn" : "mneme-pipe-edge";
|
|
7351
|
+
// 同行相邻(蛇形读法):右缘出、左缘进,短水平贝塞尔;
|
|
7352
|
+
// 跨行/跨段仍走「底出顶进」的竖向贝塞尔。同行旧画法会把线甩出段外
|
|
7353
|
+
// (真机截图验出),横连读起来就是「往下一段走」。
|
|
7354
|
+
let d;
|
|
7355
|
+
if (b.y === a.y && b.x > a.x) {
|
|
7356
|
+
const ym = a.y + NH / 2;
|
|
7357
|
+
d = `M ${a.x + NW} ${ym} C ${a.x + NW + GX / 2} ${ym}, ${b.x - GX / 2} ${ym}, ${b.x} ${ym}`;
|
|
7358
|
+
} else {
|
|
7359
|
+
const x1 = a.x + NW / 2, y1 = a.y + NH, x2 = b.x + NW / 2, y2 = b.y;
|
|
7360
|
+
d = `M ${x1} ${y1} C ${x1} ${y1 + 26}, ${x2} ${y2 - 26}, ${x2} ${y2}`;
|
|
7361
|
+
}
|
|
7362
|
+
return h("path", {
|
|
7363
|
+
key: e.id || (e.from + ">" + e.to),
|
|
7364
|
+
className: cls,
|
|
7365
|
+
d
|
|
7366
|
+
}, e.label ? h("title", null, e.label) : null);
|
|
7367
|
+
}),
|
|
7368
|
+
nodes.map((n) => {
|
|
7369
|
+
const p = pos[n.id];
|
|
7370
|
+
if (!p) return null;
|
|
7371
|
+
const rx = KIND_RX[n.kind] != null ? KIND_RX[n.kind] : 8;
|
|
7372
|
+
const on = !!(run && pathSet[n.id]);
|
|
7373
|
+
const gate = n.kind === "gate";
|
|
7374
|
+
const cls = "mneme-pipe-node mneme-pipe-node--" + (n.state || "candidate")
|
|
7375
|
+
+ (usedSet[n.id] ? " mneme-pipe-node--onpath" : "")
|
|
7376
|
+
+ (gate ? " mneme-pipe-node--gate" : "");
|
|
7377
|
+
const lines = pipeTitleLines(n.title, 10);
|
|
7378
|
+
// meta 行三段:状态(四态)· who(主/人/CI)· risk(R0-R3)。
|
|
7379
|
+
// who/risk 都是投影自带字段,此前一个都没上屏;risk 缺失按 R0 兜底。
|
|
7380
|
+
const meta = [
|
|
7381
|
+
t("memory.actions.state." + (n.state || "candidate")),
|
|
7382
|
+
n.who ? t("memory.actions.who." + n.who) : null,
|
|
7383
|
+
n.risk || "R0"
|
|
7384
|
+
].filter(Boolean).join(" · ");
|
|
7385
|
+
return h("g", {
|
|
7386
|
+
key: n.id,
|
|
7387
|
+
onClick: () => onSelectNode && onSelectNode(n.id),
|
|
7388
|
+
style: { cursor: "pointer" }
|
|
7389
|
+
},
|
|
7390
|
+
h("rect", {
|
|
7391
|
+
className: cls,
|
|
7392
|
+
x: p.x, y: p.y, width: NW, height: NH, rx
|
|
7393
|
+
}),
|
|
7394
|
+
// 风险色条:左缘一条竖线做冗余编码,信息本体在 meta 行的 R 文本里。
|
|
7395
|
+
h("line", {
|
|
7396
|
+
className: "mneme-pipe-risk--" + String(n.risk || "R0").toLowerCase(),
|
|
7397
|
+
x1: p.x + 3, y1: p.y + 8, x2: p.x + 3, y2: p.y + NH - 8,
|
|
7398
|
+
"stroke-width": 3, "stroke-linecap": "round"
|
|
7399
|
+
}),
|
|
7400
|
+
// 闸门角标:gate 步会「停下等批准」,与反事实圈同列放右上、圈在其上方——
|
|
7401
|
+
// 左上角压 risk 色条(真机截图验出的布局冲突)。
|
|
7402
|
+
gate ? h("text", {
|
|
7403
|
+
className: "mneme-pipe-gatetag",
|
|
7404
|
+
x: p.x + NW - 12, y: p.y + 24, textAnchor: "middle"
|
|
7405
|
+
}, t("memory.actions.gate.tag")) : null,
|
|
7406
|
+
// 反事实标记:空心圈 = 还没闸门(counterfactual 为 null),
|
|
7407
|
+
// 实心圈 = 有对照证据。做在右上角,一眼区分。
|
|
7408
|
+
h("circle", {
|
|
7409
|
+
cx: p.x + NW - 12, cy: p.y + 11, r: 4,
|
|
7410
|
+
fill: n.counterfactual ? "var(--mneme-ok)" : "none",
|
|
7411
|
+
stroke: n.counterfactual ? "var(--mneme-ok)" : "var(--mneme-graphic-dim)",
|
|
7412
|
+
"stroke-width": 1.5
|
|
7413
|
+
}, h("title", null, n.counterfactual ? t("memory.actions.gate.has") : t("memory.actions.gate.none"))),
|
|
7414
|
+
lines.length === 1
|
|
7415
|
+
? h("text", {
|
|
7416
|
+
className: "mneme-pipe-nodeT",
|
|
7417
|
+
x: p.x + NW / 2, y: p.y + 24, textAnchor: "middle"
|
|
7418
|
+
}, lines[0])
|
|
7419
|
+
: [
|
|
7420
|
+
h("text", {
|
|
7421
|
+
key: "t1", className: "mneme-pipe-nodeT",
|
|
7422
|
+
x: p.x + NW / 2, y: p.y + 18, textAnchor: "middle"
|
|
7423
|
+
}, lines[0]),
|
|
7424
|
+
h("text", {
|
|
7425
|
+
key: "t2", className: "mneme-pipe-nodeT",
|
|
7426
|
+
x: p.x + NW / 2, y: p.y + 31, textAnchor: "middle"
|
|
7427
|
+
}, lines[1])
|
|
7428
|
+
],
|
|
7429
|
+
h("text", {
|
|
7430
|
+
className: "mneme-pipe-nodeS",
|
|
7431
|
+
x: p.x + NW / 2, y: p.y + 46, textAnchor: "middle"
|
|
7432
|
+
}, meta),
|
|
7433
|
+
h("title", null, n.situation || n.title)
|
|
7434
|
+
);
|
|
7435
|
+
})
|
|
7436
|
+
);
|
|
7437
|
+
}
|
|
7438
|
+
|
|
7439
|
+
// 图例:四个状态一眼分清 + 闸门有无。「被推翻」最扎眼是全篇核心信号。
|
|
7440
|
+
function PipelineLegend(props) {
|
|
7441
|
+
const { t } = props;
|
|
7442
|
+
return h("div", { className: "mneme-pipe-legend" },
|
|
7443
|
+
t("memory.actions.legend.states"),
|
|
7444
|
+
PIPELINE_STATES.map((s) => h("span", { key: s, className: "mneme-pipe-pill mneme-pipe-pill--" + s }, t("memory.actions.state." + s))),
|
|
7445
|
+
h("span", { className: "mneme-pipe-gate", "aria-hidden": "true" }),
|
|
7446
|
+
t("memory.actions.gate.none"),
|
|
7447
|
+
h("span", { className: "mneme-pipe-gate mneme-pipe-gate--has", "aria-hidden": "true" }),
|
|
7448
|
+
t("memory.actions.gate.has")
|
|
7449
|
+
);
|
|
7450
|
+
}
|
|
7451
|
+
|
|
7452
|
+
// 节点详情:情形 / 动作 / 为什么 / 反事实(有无两种形态)/ 复用 / 来源。
|
|
7453
|
+
function PipelineNodeDetail(props) {
|
|
7454
|
+
const { t, node } = props;
|
|
7455
|
+
if (!node) return null;
|
|
7456
|
+
const credit = node.credit || {};
|
|
7457
|
+
const days = pipeDaysAgo(credit.lastUsedAt);
|
|
7458
|
+
const rows = [
|
|
7459
|
+
["memory.actions.detail.situation", node.situation],
|
|
7460
|
+
["memory.actions.detail.action", node.action],
|
|
7461
|
+
["memory.actions.detail.why", node.why]
|
|
7462
|
+
].filter(([, v]) => v);
|
|
7463
|
+
return h("div", { className: "mneme-pipe-detail", role: "region", "aria-label": node.title },
|
|
7464
|
+
h("div", { className: "mneme-pipe-detailTitle" },
|
|
7465
|
+
h("span", { className: "mneme-pipe-pill mneme-pipe-pill--" + (node.state || "candidate") }, t("memory.actions.state." + (node.state || "candidate"))),
|
|
7466
|
+
" " + node.title
|
|
7467
|
+
),
|
|
7468
|
+
rows.map(([k, v]) => h("div", { key: k },
|
|
7469
|
+
h("span", { className: "mneme-pipe-dt" }, t(k) + ":"), v
|
|
7470
|
+
)),
|
|
7471
|
+
node.counterfactual
|
|
7472
|
+
? h("div", { className: "mneme-pipe-cf" },
|
|
7473
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.cf") + ":"),
|
|
7474
|
+
node.counterfactual.contrast || "")
|
|
7475
|
+
: h("div", { className: "mneme-pipe-cf mneme-pipe-cf--none" }, t("memory.actions.detail.noCf")),
|
|
7476
|
+
h("div", null,
|
|
7477
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.credit") + ":"),
|
|
7478
|
+
t("memory.actions.creditValue")
|
|
7479
|
+
.replace("{n}", String(credit.uses != null ? credit.uses : 0))
|
|
7480
|
+
.replace("{d}", days == null ? "—" : String(days))
|
|
7481
|
+
),
|
|
7482
|
+
node.source && (node.source.memoryIds || []).length
|
|
7483
|
+
? h("div", null,
|
|
7484
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.source") + ":"),
|
|
7485
|
+
(node.source.memoryIds || []).join("、"))
|
|
7486
|
+
: null
|
|
7487
|
+
);
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7490
|
+
// 窄态(浮层 / better-sidebar ~500px):竖排列表——换布局,不是缩放。
|
|
7491
|
+
// 边收成左侧轨道线 + 「依赖」芯片;details 手风琴承载详情。
|
|
7492
|
+
function PipelineList(props) {
|
|
7493
|
+
const { t, data, run, selectedNodeId, onSelectNode } = props;
|
|
7494
|
+
const nodes = data.nodes || [];
|
|
7495
|
+
const edges = data.edges || [];
|
|
7496
|
+
const usedSet = {};
|
|
7497
|
+
if (run && Array.isArray(run.usedNodeIds)) for (const id of run.usedNodeIds) usedSet[id] = true;
|
|
7498
|
+
const onPath = {};
|
|
7499
|
+
if (run && Array.isArray(run.path)) for (const id of run.path) onPath[id] = true;
|
|
7500
|
+
const deps = {};
|
|
7501
|
+
for (const e of edges) (deps[e.to] = deps[e.to] || []).push(e.from);
|
|
7502
|
+
const { layers } = layoutDag(nodes, edges);
|
|
7503
|
+
const flat = [];
|
|
7504
|
+
layers.forEach((col) => col.forEach((id) => {
|
|
7505
|
+
const n = nodes.find((x) => x.id === id);
|
|
7506
|
+
if (n) flat.push(n);
|
|
7507
|
+
}));
|
|
7508
|
+
return h("div", { className: "mneme-pipe-list" },
|
|
7509
|
+
flat.map((n) => h("details", {
|
|
7510
|
+
key: n.id,
|
|
7511
|
+
className: "mneme-pipe-item",
|
|
7512
|
+
open: selectedNodeId === n.id || undefined,
|
|
7513
|
+
onToggle: (ev) => { if (ev.target && ev.target.open) onSelectNode && onSelectNode(n.id); }
|
|
7514
|
+
},
|
|
7515
|
+
h("summary", null,
|
|
7516
|
+
h("span", {
|
|
7517
|
+
className: "mneme-pipe-itemDot mneme-pipe-itemDot--" + (n.state || "candidate") +
|
|
7518
|
+
(onPath[n.id] ? " mneme-pipe-itemDot--onpath" : ""),
|
|
7519
|
+
"aria-hidden": "true"
|
|
7520
|
+
}),
|
|
7521
|
+
h("span", { className: "mneme-pipe-itemName" + (n.state === "refuted" ? " mneme-pipe-itemName--refuted" : "") }, n.title),
|
|
7522
|
+
h("span", { className: "mneme-pipe-itemMeta" },
|
|
7523
|
+
h("span", { className: "mneme-pipe-pill mneme-pipe-pill--" + (n.state || "candidate") }, t("memory.actions.state." + (n.state || "candidate"))),
|
|
7524
|
+
(deps[n.id] || []).length ? h("span", { className: "mneme-pipe-dep" }, t("memory.actions.dep") + " " + deps[n.id].join(" ")) : null,
|
|
7525
|
+
n.counterfactual ? null : h("span", { className: "mneme-pipe-dep" }, t("memory.actions.gate.none"))
|
|
7526
|
+
)
|
|
7527
|
+
),
|
|
7528
|
+
h("div", { className: "mneme-pipe-itemBody" },
|
|
7529
|
+
n.situation ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.situation") + ":"), n.situation) : null,
|
|
7530
|
+
n.action ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.action") + ":"), n.action) : null,
|
|
7531
|
+
n.why ? h("div", null, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.why") + ":"), n.why) : null,
|
|
7532
|
+
n.counterfactual
|
|
7533
|
+
? h("div", { className: "mneme-pipe-cf" }, h("span", { className: "mneme-pipe-dt" }, t("memory.actions.detail.cf") + ":"), n.counterfactual.contrast || "")
|
|
7534
|
+
: h("div", { className: "mneme-pipe-cf mneme-pipe-cf--none" }, t("memory.actions.detail.noCf"))
|
|
7535
|
+
)
|
|
7536
|
+
))
|
|
7537
|
+
);
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
// 条目清单(名字 + 规模 + 作用域):图上方的**选择器**。
|
|
7541
|
+
// 它解决的是「这是哪条工作流」——一进来就是一张图时这个问题没有落点,多条之间也没得选。
|
|
7542
|
+
// 但它不占整屏:图和它同屏,见下面的 PipelineBrowser。
|
|
7543
|
+
function PipelineWorkflowList(props) {
|
|
7544
|
+
const { t, data, activeId, onOpen } = props;
|
|
7545
|
+
const list = (data && data.available) || [];
|
|
7546
|
+
const scopeOf = (row) => (row.scope === "global"
|
|
7547
|
+
? t("memory.actions.scope.global")
|
|
7548
|
+
: t("memory.actions.scope.project").replace("{name}", row.scopeLabel || row.scope));
|
|
7549
|
+
return h("div", { className: "mneme-pipe-wflist" },
|
|
7550
|
+
h("div", { className: "mneme-pipe-wfhead" },
|
|
7551
|
+
h("span", null, t("memory.actions.list.heading")),
|
|
7552
|
+
h("span", null, t("memory.actions.list.count").replace("{n}", String(list.length)))
|
|
7553
|
+
),
|
|
7554
|
+
list.length
|
|
7555
|
+
? list.map((row) => h("button", {
|
|
7556
|
+
key: row.id,
|
|
7557
|
+
type: "button",
|
|
7558
|
+
className: "mneme-pipe-wf" + (row.id === activeId ? " mneme-on" : ""),
|
|
7559
|
+
"aria-current": row.id === activeId ? "true" : undefined,
|
|
7560
|
+
onClick: () => onOpen(row.id)
|
|
7561
|
+
},
|
|
7562
|
+
h("span", { className: "mneme-pipe-wfTop" },
|
|
7563
|
+
h("span", { className: "mneme-pipe-wfName" }, row.title || row.id),
|
|
7564
|
+
h("span", { className: "mneme-pipe-wfScope" }, scopeOf(row))
|
|
7565
|
+
),
|
|
7566
|
+
h("span", { className: "mneme-pipe-wfMeta" }, t("memory.actions.list.meta")
|
|
7567
|
+
.replace("{steps}", String(row.steps || 0))
|
|
7568
|
+
.replace("{gates}", String(row.gates || 0))
|
|
7569
|
+
.replace("{cf}", String(row.contrast || 0))),
|
|
7570
|
+
h("span", { className: "mneme-pipe-wfId" }, row.id)
|
|
7571
|
+
))
|
|
7572
|
+
// 「读得出来但一份都没有」与「读不出来」是两回事,别画成同一张脸:
|
|
7573
|
+
// 前者要告诉人文件该放哪,后者只有 loadPipeline 返回 null 时才是。
|
|
7574
|
+
: h("div", { className: "mneme-pipe-empty" },
|
|
7575
|
+
h("div", { className: "mneme-pipe-emptyTitle" }, t("memory.actions.empty.title")),
|
|
7576
|
+
t("memory.actions.empty.body")
|
|
7577
|
+
),
|
|
7578
|
+
data && data.unreadable && data.unreadable.length
|
|
7579
|
+
? h("div", { className: "mneme-pipe-broken" },
|
|
7580
|
+
t("memory.actions.broken") + " " + data.unreadable.map((u) => u.detail || u.dir).join(";"))
|
|
7581
|
+
: null
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
7584
|
+
|
|
7585
|
+
// 清单与图**同屏**:上面清单(选择器),下面画布(主语),默认展开第一条。
|
|
7586
|
+
// 形状换过两次,两次的教训都留在这里,免得再翻回去:
|
|
7587
|
+
// 一进来就是图 —— 没有名字、没有落点,用户要的是「像 Hermes 那样一串条目」;
|
|
7588
|
+
// 清单 ⟷ 图两屏来回切 —— 条目有了,但画布藏在一次点击后面、界面上没有任何提示,
|
|
7589
|
+
// 反馈连着两次说「看不到画布和节点」。
|
|
7590
|
+
// 两个 usePipeline 都是无条件调用:hook 不能条件调用,而「默认选第一条」这件事
|
|
7591
|
+
// 恰好可以放在两个读取之间,不必为此拆组件。
|
|
7592
|
+
function PipelineBrowser(props) {
|
|
7593
|
+
const { t, project } = props;
|
|
7594
|
+
const [picked, setPicked] = useState(null);
|
|
7595
|
+
const listDoc = usePipeline(project, null);
|
|
7596
|
+
const rows = (listDoc && listDoc.available) || [];
|
|
7597
|
+
// 没手选过就跟第一条;手选之后清单再刷新也不跳回去(否则一刷新就把人的选择抹掉)。
|
|
7598
|
+
const activeId = picked || (rows.length ? rows[0].id : null);
|
|
7599
|
+
const detailDoc = usePipeline(project, activeId);
|
|
7600
|
+
return h("div", {
|
|
7601
|
+
className: "mneme-pipe",
|
|
7602
|
+
style: { height: "100%", display: "flex", flexDirection: "column", minHeight: "0" }
|
|
7603
|
+
},
|
|
7604
|
+
h(PipelineWorkflowList, { t, data: listDoc, activeId, onOpen: setPicked }),
|
|
7605
|
+
h(PipelineView, { t, data: detailDoc })
|
|
7606
|
+
);
|
|
7607
|
+
}
|
|
7608
|
+
|
|
7609
|
+
// 画布缩放。为什么要有:重排前 19 步是一条 18 层竖链(固有 354×1902),被 width:100%
|
|
7610
|
+
// 拉到栏宽之后高度上万——只能上下滚;重排成阶段带后(固有 354×836)仍有
|
|
7611
|
+
// 「凑近看某一段」的需求。按钮管精确、滚轮管顺手。
|
|
7612
|
+
const PIPELINE_ZOOM_MIN = 0.3;
|
|
7613
|
+
const PIPELINE_ZOOM_MAX = 4;
|
|
7614
|
+
const clampZoom = (z) => Math.min(PIPELINE_ZOOM_MAX, Math.max(PIPELINE_ZOOM_MIN, z));
|
|
7615
|
+
|
|
7616
|
+
function PipelineZoomBar(props) {
|
|
7617
|
+
const { t, zoom, onZoom } = props;
|
|
7618
|
+
return h("div", { className: "mneme-pipe-zoombar" },
|
|
7619
|
+
h("button", {
|
|
7620
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7621
|
+
title: t("memory.actions.zoom.out"), "aria-label": t("memory.actions.zoom.out"),
|
|
7622
|
+
onClick: () => onZoom(zoom / 1.25)
|
|
7623
|
+
}, "−"),
|
|
7624
|
+
h("span", { className: "mneme-pipe-zoomval" },
|
|
7625
|
+
t("memory.actions.zoom.value").replace("{n}", String(Math.round(zoom * 100)))),
|
|
7626
|
+
h("button", {
|
|
7627
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7628
|
+
title: t("memory.actions.zoom.in"), "aria-label": t("memory.actions.zoom.in"),
|
|
7629
|
+
onClick: () => onZoom(zoom * 1.25)
|
|
7630
|
+
}, "+"),
|
|
7631
|
+
h("button", {
|
|
7632
|
+
type: "button", className: "mneme-pipe-zoombtn",
|
|
7633
|
+
title: t("memory.actions.zoom.hint"),
|
|
7634
|
+
onClick: () => onZoom(1)
|
|
7635
|
+
}, t("memory.actions.zoom.fit"))
|
|
7636
|
+
);
|
|
7637
|
+
}
|
|
7638
|
+
|
|
7639
|
+
// 主视图:宽态 SVG 图 ↔ 窄态列表按容器宽切换(640px 阈值,ResizeObserver)。
|
|
7640
|
+
// 根挂 .mneme-pipeview 拿语义色变量层。
|
|
7641
|
+
function PipelineView(props) {
|
|
7642
|
+
const { t, data } = props;
|
|
7643
|
+
const nodes = (data && data.nodes) || [];
|
|
7644
|
+
const runs = (data && data.runs) || [];
|
|
7645
|
+
const [selectedRunId, setSelectedRunId] = react.useState(runs.length ? runs[0].id : null);
|
|
7646
|
+
const [selectedNodeId, setSelectedNodeId] = react.useState(null);
|
|
7647
|
+
const rootRef = react.useRef(null);
|
|
7648
|
+
const graphRef = react.useRef(null);
|
|
7649
|
+
const [narrow, setNarrow] = react.useState(false);
|
|
7650
|
+
const [zoom, setZoom] = react.useState(1);
|
|
7651
|
+
react.useEffect(() => {
|
|
7652
|
+
const el = rootRef.current;
|
|
7653
|
+
if (!el || typeof ResizeObserver !== "function") return;
|
|
7654
|
+
const ro = new ResizeObserver((entries) => {
|
|
7655
|
+
const w = entries && entries[0] ? entries[0].contentRect.width : 0;
|
|
7656
|
+
// 阈值从 640 降到 320:竖链与阶段带的固有宽度都是 354px——栏窄一点反而更该画图。
|
|
7657
|
+
// 640 那档是按当年那张摊得很开的假数据定的,真数据下它只会把右栏(通常 400~600px)
|
|
7658
|
+
// 全部推给「一列节点卡片」那一支,于是「看不到图谱」。
|
|
7659
|
+
setNarrow(w > 0 && w < 320);
|
|
7660
|
+
});
|
|
7661
|
+
ro.observe(el);
|
|
7662
|
+
return () => ro.disconnect();
|
|
7663
|
+
}, []);
|
|
7664
|
+
// 滚轮缩放必须**自己挂非被动监听**:React 的 onWheel 是挂在 root 上的被动监听,
|
|
7665
|
+
// preventDefault 会被浏览器忽略(控制台里那句 "Unable to preventDefault inside passive
|
|
7666
|
+
// event listener"),结果是 Ctrl+滚轮既缩放画布、又缩放整页,谁也没拦住。
|
|
7667
|
+
// 只认 Ctrl/⌘:普通滚轮仍旧是滚动画布——劫持它会让人滚不到下面的图例。
|
|
7668
|
+
react.useEffect(() => {
|
|
7669
|
+
const el = graphRef.current;
|
|
7670
|
+
if (!el || typeof el.addEventListener !== "function") return undefined;
|
|
7671
|
+
const onWheel = (ev) => {
|
|
7672
|
+
if (!ev.ctrlKey && !ev.metaKey) return;
|
|
7673
|
+
ev.preventDefault();
|
|
7674
|
+
setZoom((z) => clampZoom(z * (ev.deltaY < 0 ? 1.15 : 1 / 1.15)));
|
|
7675
|
+
};
|
|
7676
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
7677
|
+
return () => el.removeEventListener("wheel", onWheel);
|
|
7678
|
+
}, [narrow]);
|
|
7679
|
+
|
|
7680
|
+
const run = runs.find((r) => r.id === selectedRunId) || null;
|
|
7681
|
+
const selectedNode = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) : null;
|
|
7682
|
+
if (!nodes.length) {
|
|
7683
|
+
return h("div", { className: "mneme-pipeview" },
|
|
7684
|
+
h("div", { className: "mneme-pipe-empty" },
|
|
7685
|
+
h("div", { className: "mneme-pipe-emptyTitle" }, t("memory.actions.empty.title")),
|
|
7686
|
+
t("memory.actions.empty.body")
|
|
7687
|
+
),
|
|
7688
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice"))
|
|
7689
|
+
);
|
|
7690
|
+
}
|
|
7691
|
+
return h("div", { className: "mneme-pipeview", ref: rootRef, style: { display: "flex", flexDirection: "column", flex: "1", minHeight: "0" } },
|
|
7692
|
+
h(PipelineRunChips, { t, runs, selectedRunId, onSelectRun: setSelectedRunId }),
|
|
7693
|
+
// 流程概览带:投影里 pipeline.done / trigger 此前一个都没渲染——第一屏
|
|
7694
|
+
// 说不出「这条流程要达成什么、什么时候起跑」。放画布上方、做成 HTML
|
|
7695
|
+
// 块而非 SVG 文本:125 字的完成判据要能换行。数据由投影保证存在。
|
|
7696
|
+
(data && data.pipeline) ? h("div", { className: "mneme-pipe-overview" },
|
|
7697
|
+
h("div", null,
|
|
7698
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.ov.done") + ":"),
|
|
7699
|
+
data.pipeline.done
|
|
7700
|
+
),
|
|
7701
|
+
h("div", null,
|
|
7702
|
+
h("span", { className: "mneme-pipe-dt" }, t("memory.actions.ov.trigger") + ":"),
|
|
7703
|
+
(data.pipeline.trigger || []).map((x) => x && x.value).filter(Boolean).join("、") || null
|
|
7704
|
+
)
|
|
7705
|
+
) : null,
|
|
7706
|
+
// 一条运行记录都没有 = 全部候选。这句话得说出来:卡片上「候选」两个字本身
|
|
7707
|
+
// 不解释任何东西,而用户在这一屏最容易问的就是「为什么全是候选」。
|
|
7708
|
+
runs.length ? null : h("div", { className: "mneme-hint" }, t("memory.actions.noRuns")),
|
|
7709
|
+
// 缩放条放在分支**外面**:不论宽窄都在,既是控制,也是「新代码到没到」的肉眼标记。
|
|
7710
|
+
h(PipelineZoomBar, { t, zoom, onZoom: (z) => setZoom(clampZoom(z)) }),
|
|
7711
|
+
narrow
|
|
7712
|
+
? h(PipelineList, { t, data, run, selectedNodeId, onSelectNode: setSelectedNodeId })
|
|
7713
|
+
: h("div", { className: "mneme-pipe-graph", ref: graphRef },
|
|
7714
|
+
// 宽度用百分比而不是 transform:百分比改的是**布局尺寸**,容器照常出滚动条,
|
|
7715
|
+
// 于是顺带就有了平移;transform 只改绘制,放大出去的那一半永远滚不到。
|
|
7716
|
+
h("div", { className: "mneme-pipe-zoomwrap", style: { width: (zoom * 100) + "%" } },
|
|
7717
|
+
h(PipelineGraphSvg, { t, data, run, selectedNodeId, onSelectNode: setSelectedNodeId })
|
|
7718
|
+
)
|
|
7719
|
+
),
|
|
7720
|
+
h(PipelineLegend, { t }),
|
|
7721
|
+
selectedNode ? h(PipelineNodeDetail, { t, node: selectedNode }) : null,
|
|
7722
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice"))
|
|
7723
|
+
);
|
|
7724
|
+
}
|
|
7725
|
+
|
|
7726
|
+
// 右栏 tab 的正文。单独包一层组件、而不是把 hook 塞进 slots.register 的回调里:
|
|
7727
|
+
// 那个回调是不是当组件渲染由宿主决定,hook 放在名字明确的组件里才不必赌这件事。
|
|
7728
|
+
function PipelinePane(props) {
|
|
7729
|
+
const { t, project } = props;
|
|
7730
|
+
return h(PipelineBrowser, { t, project });
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7733
|
+
// 流水线标记:两个步骤块 + 依赖连线 + 状态点;开合状态用右上角
|
|
7734
|
+
// 实心/空心点区分(会话头图标要反映浮层开合)。
|
|
7735
|
+
function PipelineMark(props) {
|
|
7736
|
+
const { size = 16, open } = props || {};
|
|
7737
|
+
return h("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", focusable: "false" },
|
|
7738
|
+
h("path", { d: "M2.5 3h4.2v4.2H2.5z M9.3 8.8h4.2V13H9.3z", fill: "none", stroke: "currentColor", "stroke-width": 1.4 }),
|
|
7739
|
+
h("path", { d: "M6.7 5.1h4v3.7", fill: "none", stroke: "currentColor", "stroke-width": 1.2 }),
|
|
7740
|
+
h("circle", {
|
|
7741
|
+
cx: 13.2, cy: 2.8, r: 1.9,
|
|
7742
|
+
fill: open ? "currentColor" : "none",
|
|
7743
|
+
stroke: "currentColor", "stroke-width": 1.2
|
|
7744
|
+
})
|
|
7745
|
+
);
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
// 会话头入口:悬停 150ms 开 / 移开 120ms 关 / 点击固定 / Escape 关 /
|
|
7749
|
+
// 点击外部关(交互规格与 Agent Teams 浮层一致)。浮层锚 trigger、
|
|
7750
|
+
// portal 到 body。老宿主没有右栏服务时补一个「完整视图」按钮退回
|
|
7751
|
+
// 居中 sheet(形态沿用记忆库 overlay 的背板 + 居中面板)。
|
|
7752
|
+
function PipelineHeaderEntry(props) {
|
|
7753
|
+
const { t, hasPane, openPane, project } = props;
|
|
7754
|
+
const [open, setOpen] = useState(false);
|
|
7755
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
7756
|
+
const openRef = useRef(false);
|
|
7757
|
+
const pinnedRef = useRef(false);
|
|
7758
|
+
const hoverTimer = useRef(null);
|
|
7759
|
+
const rootRef = useRef(null);
|
|
7760
|
+
const anchorRef = useRef(null);
|
|
7761
|
+
const panelRef = useRef(null);
|
|
7762
|
+
const labelRef = useRef(null);
|
|
7763
|
+
const sheetRef = useRef(null);
|
|
7764
|
+
|
|
7765
|
+
const changeOpen = (next) => {
|
|
7766
|
+
const was = openRef.current;
|
|
7767
|
+
const wasPinned = pinnedRef.current;
|
|
7768
|
+
openRef.current = next;
|
|
7769
|
+
setOpen(next);
|
|
7770
|
+
if (!next) pinnedRef.current = false;
|
|
7771
|
+
// announce 只挂「用户明确动作」的开合(点击固定 / Esc / 点外关);
|
|
7772
|
+
// 悬停开合是瞬态,播报反而刷屏——与 §5 无障碍约束对齐。
|
|
7773
|
+
if (next && !was && wasPinned) announce(t("memory.actions.announce.open"));
|
|
7774
|
+
if (!next && was && wasPinned) announce(t("memory.actions.announce.close"));
|
|
7775
|
+
};
|
|
7776
|
+
const cancelHoverChange = () => {
|
|
7777
|
+
if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; }
|
|
7778
|
+
};
|
|
7779
|
+
const scheduleHoverOpen = () => {
|
|
7780
|
+
if (pinnedRef.current || openRef.current) return;
|
|
7781
|
+
// 窄容器下文字标签被 @container 隐藏(图标态)→ 不弹浮层,点击才开。
|
|
7782
|
+
if (labelRef.current && labelRef.current.offsetWidth === 0) return;
|
|
7783
|
+
cancelHoverChange();
|
|
7784
|
+
hoverTimer.current = setTimeout(() => {
|
|
7785
|
+
hoverTimer.current = null;
|
|
7786
|
+
openRef.current = true;
|
|
7787
|
+
setOpen(true);
|
|
7788
|
+
}, 150);
|
|
7789
|
+
};
|
|
7790
|
+
const scheduleHoverClose = () => {
|
|
7791
|
+
if (pinnedRef.current) return;
|
|
7792
|
+
cancelHoverChange();
|
|
7793
|
+
hoverTimer.current = setTimeout(() => {
|
|
7794
|
+
hoverTimer.current = null;
|
|
7795
|
+
openRef.current = false;
|
|
7796
|
+
setOpen(false);
|
|
7797
|
+
}, 120);
|
|
7798
|
+
};
|
|
7799
|
+
|
|
7800
|
+
// 钩子顺序稳定:useAnchoredPosition 的存在性是模块常量条件,两次
|
|
7801
|
+
// 判断在所有渲染中走同一分支,不违反 hook 规则。
|
|
7802
|
+
const positioned = primitives && typeof primitives.useAnchoredPosition === "function"
|
|
7803
|
+
? primitives.useAnchoredPosition({ open, anchorRef, panelRef, gap: 5, margin: 16 })
|
|
7804
|
+
: null;
|
|
7805
|
+
const panelStyle = positioned
|
|
7806
|
+
? { ...positioned }
|
|
7807
|
+
: { position: "fixed", left: 0, top: 0, visibility: "hidden" };
|
|
7808
|
+
|
|
7809
|
+
if (primitives && typeof primitives.useDismissOnOutsidePointer === "function") {
|
|
7810
|
+
primitives.useDismissOnOutsidePointer(rootRef, open, changeOpen, panelRef);
|
|
7811
|
+
}
|
|
7812
|
+
useEffect(() => {
|
|
7813
|
+
if (primitives && typeof primitives.useDismissOnOutsidePointer === "function") return;
|
|
7814
|
+
if (!open) return;
|
|
7815
|
+
const onDown = (ev) => {
|
|
7816
|
+
const panel = panelRef.current;
|
|
7817
|
+
const root = rootRef.current;
|
|
7818
|
+
if (panel && panel.contains(ev.target)) return;
|
|
7819
|
+
if (root && root.contains(ev.target)) return;
|
|
7820
|
+
changeOpen(false);
|
|
7821
|
+
};
|
|
7822
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
7823
|
+
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
7824
|
+
}, [open]);
|
|
7825
|
+
useEffect(() => () => cancelHoverChange(), []);
|
|
7826
|
+
|
|
7827
|
+
// 居中 sheet 是「宿主没有右栏服务」时的唯一完整入口,键盘用户必须能进能出。
|
|
7828
|
+
// 它 portal 到 body,焦点默认留在原 trigger 上,而挂在 sheet 元素上的
|
|
7829
|
+
// onKeyDown 既因没有 tabIndex 不被聚焦、也收不到外部键盘事件——Escape 于是
|
|
7830
|
+
// 不生效,只剩 Tab 摸到关闭按钮一条路。做法与 MemoryOverlay 一致:打开时把
|
|
7831
|
+
// 焦点移进 sheet,Escape 在 window 上收,关掉再把焦点还给 trigger。
|
|
7832
|
+
useEffect(() => {
|
|
7833
|
+
if (!sheetOpen) return undefined;
|
|
7834
|
+
if (sheetRef.current) sheetRef.current.focus();
|
|
7835
|
+
const onKey = (ev) => {
|
|
7836
|
+
if (ev.key !== "Escape") return;
|
|
7837
|
+
ev.preventDefault();
|
|
7838
|
+
ev.stopPropagation();
|
|
7839
|
+
setSheetOpen(false);
|
|
7840
|
+
if (anchorRef.current) anchorRef.current.focus();
|
|
7841
|
+
};
|
|
7842
|
+
window.addEventListener("keydown", onKey);
|
|
7843
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
7844
|
+
}, [sheetOpen]);
|
|
7845
|
+
|
|
7846
|
+
const closeAll = () => {
|
|
7847
|
+
changeOpen(false);
|
|
7848
|
+
if (anchorRef.current) anchorRef.current.focus();
|
|
7849
|
+
};
|
|
7850
|
+
const onTriggerClick = () => {
|
|
7851
|
+
if (!openRef.current) {
|
|
7852
|
+
pinnedRef.current = true;
|
|
7853
|
+
changeOpen(true);
|
|
7854
|
+
setTimeout(() => { if (panelRef.current) panelRef.current.focus(); }, 0);
|
|
7855
|
+
} else if (pinnedRef.current) {
|
|
7856
|
+
changeOpen(false);
|
|
7857
|
+
} else {
|
|
7858
|
+
pinnedRef.current = true;
|
|
7859
|
+
if (panelRef.current) panelRef.current.focus();
|
|
7860
|
+
}
|
|
7861
|
+
};
|
|
7862
|
+
const doc = usePipeline(project, null);
|
|
7863
|
+
// 头部这一格标的是**这份清单读的是哪个项目**(某条工作流归谁,标在清单每一行上):
|
|
7864
|
+
// 只取目录名,绝对路径不上界面——面板是会被截图的那一面。认不出项目时如实说,
|
|
7865
|
+
// 而不是编一个「全局」顶上:认不出和全局是两件事。
|
|
7866
|
+
const projectSource = ((doc && doc.sources) || []).find((s) => s.scope === "project");
|
|
7867
|
+
const scopeLabel = projectSource && projectSource.label
|
|
7868
|
+
? t("memory.actions.scope.project").replace("{name}", projectSource.label)
|
|
7869
|
+
: t("memory.actions.scope.none");
|
|
7870
|
+
// 数的是这个项目里读出几条工作流;没拿到数据时是 0,不是随便挑一个数。
|
|
7871
|
+
const workflowCount = doc && doc.available ? doc.available.length : 0;
|
|
7872
|
+
|
|
7873
|
+
const panel = (open && reactDom && typeof document !== "undefined")
|
|
7874
|
+
? reactDom.createPortal(
|
|
7875
|
+
h("div", {
|
|
7876
|
+
ref: panelRef,
|
|
7877
|
+
// 注意:client.test.js 锁死全文件不得出现 role:"dialog"
|
|
7878
|
+
// (v0.7.15 清退 dialog 面的纪律),浮层只带 aria-label。
|
|
7879
|
+
"aria-label": t("memory.actions.title"),
|
|
7880
|
+
tabIndex: -1,
|
|
7881
|
+
className: "mneme-pipe mneme-pipe-panel",
|
|
7882
|
+
style: panelStyle,
|
|
7883
|
+
onMouseEnter: cancelHoverChange,
|
|
7884
|
+
onMouseLeave: scheduleHoverClose,
|
|
7885
|
+
onKeyDown: (ev) => { if (ev.key === "Escape") { ev.preventDefault(); ev.stopPropagation(); closeAll(); } }
|
|
7886
|
+
},
|
|
7887
|
+
h("div", { className: "mneme-pipe-head" },
|
|
7888
|
+
h("span", { className: "mneme-pipe-title" }, t("memory.actions.title")),
|
|
7889
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7890
|
+
h("span", { className: "mneme-pipe-scope" }, scopeLabel),
|
|
7891
|
+
h("button", {
|
|
7892
|
+
type: "button",
|
|
7893
|
+
className: "mneme-pipe-run",
|
|
7894
|
+
// 有右栏服务:openTab(kind) 打开原生右栏 tab(页面类型无
|
|
7895
|
+
// patterns,按 kind 打开)并收起浮层;打不开或老宿主
|
|
7896
|
+
// (无 sidebarRight)退回本插件的居中 sheet——形态沿用
|
|
7897
|
+
// 记忆库 overlay 的背板 + 居中面板。
|
|
7898
|
+
onClick: () => {
|
|
7899
|
+
if (openPane && openPane()) { changeOpen(false); return; }
|
|
7900
|
+
changeOpen(false);
|
|
7901
|
+
setSheetOpen(true);
|
|
7902
|
+
}
|
|
7903
|
+
}, hasPane && openPane ? t("memory.actions.openPane") : t("memory.actions.viewFull"))
|
|
7904
|
+
),
|
|
7905
|
+
h("div", { className: "mneme-pipe-notice" }, t("memory.actions.notice")),
|
|
7906
|
+
h(PipelineBrowser, { t, project })
|
|
7907
|
+
),
|
|
7908
|
+
document.body
|
|
7909
|
+
)
|
|
7910
|
+
: null;
|
|
7911
|
+
|
|
7912
|
+
const sheet = (sheetOpen && reactDom && typeof document !== "undefined")
|
|
7913
|
+
? reactDom.createPortal(
|
|
7914
|
+
h("div", {
|
|
7915
|
+
className: "mneme-backdrop",
|
|
7916
|
+
onClick: (ev) => { if (ev.target === ev.currentTarget) setSheetOpen(false); }
|
|
7917
|
+
},
|
|
7918
|
+
h("div", {
|
|
7919
|
+
ref: sheetRef,
|
|
7920
|
+
tabIndex: -1,
|
|
7921
|
+
className: "mneme-pipe mneme-pipe-sheet",
|
|
7922
|
+
"aria-label": t("memory.actions.title"),
|
|
7923
|
+
onKeyDown: (ev) => { if (ev.key === "Escape") { ev.stopPropagation(); setSheetOpen(false); } }
|
|
7924
|
+
},
|
|
7925
|
+
h("div", { className: "mneme-pipe-sheetbar" },
|
|
7926
|
+
h("span", { className: "mneme-pipe-title" }, t("memory.actions.title")),
|
|
7927
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7928
|
+
h("span", { className: "mneme-pipe-scope" }, scopeLabel),
|
|
7929
|
+
h("button", { type: "button", className: "mneme-pipe-run", onClick: () => setSheetOpen(false) }, t("memory.actions.close"))
|
|
7930
|
+
),
|
|
7931
|
+
h("div", { className: "mneme-pipe-sheetbody" }, h(PipelineBrowser, { t, project }))
|
|
7932
|
+
)
|
|
7933
|
+
),
|
|
7934
|
+
document.body
|
|
7935
|
+
)
|
|
7936
|
+
: null;
|
|
7937
|
+
|
|
7938
|
+
return h("div", {
|
|
7939
|
+
className: "mneme-pipe-root",
|
|
7940
|
+
ref: rootRef,
|
|
7941
|
+
onMouseEnter: scheduleHoverOpen,
|
|
7942
|
+
onMouseLeave: scheduleHoverClose
|
|
7943
|
+
},
|
|
7944
|
+
h("button", {
|
|
7945
|
+
type: "button",
|
|
7946
|
+
ref: anchorRef,
|
|
7947
|
+
className: "mneme-pipe-trigger",
|
|
7948
|
+
"aria-haspopup": "true",
|
|
7949
|
+
"aria-expanded": open ? "true" : "false",
|
|
7950
|
+
title: t("memory.actions.open"),
|
|
7951
|
+
onClick: onTriggerClick,
|
|
7952
|
+
onKeyDown: (ev) => { if (ev.key === "Escape" && open) { ev.preventDefault(); ev.stopPropagation(); closeAll(); } }
|
|
7953
|
+
},
|
|
7954
|
+
h(PipelineMark, { open }),
|
|
7955
|
+
h("span", { ref: labelRef, className: "mneme-pipe-triggerLabel" }, t("memory.actions.title")),
|
|
7956
|
+
workflowCount > 0 ? h("span", {
|
|
7957
|
+
className: "mneme-pipe-dot",
|
|
7958
|
+
title: t("memory.actions.list.count").replace("{n}", String(workflowCount))
|
|
7959
|
+
}, String(workflowCount)) : null
|
|
7960
|
+
),
|
|
7961
|
+
panel,
|
|
7962
|
+
sheet
|
|
7963
|
+
);
|
|
7964
|
+
}
|
|
7965
|
+
|
|
7966
|
+
// 对话行结算卡(turnTail):死的落款——「这次跑哪条被用上了」。
|
|
7967
|
+
// 与会话头浮层读同一份数据;卡上勾选 = usedNodeIds(真正被复用),
|
|
7968
|
+
// 其余 path 节点只是被走过。没有运行记录时整张卡不出现——发一条「本次没用上任何
|
|
7969
|
+
// 做法」的空卡比不发更吵:它每一轮都会来一次。
|
|
7970
|
+
function TurnRunCard(props) {
|
|
7971
|
+
const { t, project } = props;
|
|
7972
|
+
const doc = usePipeline(project, null);
|
|
7973
|
+
const data = doc || {};
|
|
7974
|
+
const run = (data.runs || []).find((r) => r.id === "r1") || (data.runs || [])[0];
|
|
7975
|
+
if (!run) return null;
|
|
7976
|
+
const used = run.usedNodeIds || [];
|
|
7977
|
+
const refutedUsed = used.some((id) => {
|
|
7978
|
+
const n = (data.nodes || []).find((x) => x.id === id);
|
|
7979
|
+
return n && n.state === "refuted";
|
|
7980
|
+
});
|
|
7981
|
+
return h("div", { className: "mneme-pipe mneme-pipe-settle" },
|
|
7982
|
+
h("div", { className: "mneme-pipe-settleHead" },
|
|
7983
|
+
h("span", { className: "mneme-pipe-settleTitle" },
|
|
7984
|
+
h(PipelineMark, { size: 13 }),
|
|
7985
|
+
t("memory.actions.settle.title")
|
|
7986
|
+
),
|
|
7987
|
+
h("span", { className: "mneme-pipe-beta" }, t("memory.actions.beta")),
|
|
7988
|
+
h("span", { className: "mneme-pipe-scope" },
|
|
7989
|
+
t("memory.actions.settle.line").replace("{title}", data.pipeline.title) + " · " + pipeFmtWhen(run.at))
|
|
7990
|
+
),
|
|
7991
|
+
h("div", { className: "mneme-pipe-steps" },
|
|
7992
|
+
(run.path || []).map((id, i) => {
|
|
7993
|
+
const n = (data.nodes || []).find((x) => x.id === id);
|
|
7994
|
+
if (!n) return null;
|
|
7995
|
+
const isUsed = used.includes(id);
|
|
7996
|
+
return h(react.Fragment, { key: id + ":" + i },
|
|
7997
|
+
i > 0 ? h("span", { className: "mneme-pipe-stepArrow", "aria-hidden": "true" }, "→") : null,
|
|
7998
|
+
h("span", {
|
|
7999
|
+
className: "mneme-pipe-stepChip" + (isUsed ? " mneme-pipe-stepChip--used" : ""),
|
|
8000
|
+
title: n.title
|
|
8001
|
+
},
|
|
8002
|
+
(isUsed ? "✓ " : "") + pipeClip(n.title, 12)
|
|
8003
|
+
)
|
|
8004
|
+
);
|
|
8005
|
+
})
|
|
8006
|
+
),
|
|
8007
|
+
h("div", { className: "mneme-pipe-scope" },
|
|
8008
|
+
t("memory.actions.settle.used").replace("{n}", String(used.length))
|
|
8009
|
+
),
|
|
8010
|
+
refutedUsed ? h("div", { className: "mneme-pipe-scope" }, t("memory.actions.settle.refutedWarn")) : null
|
|
8011
|
+
);
|
|
8012
|
+
}
|
|
8013
|
+
|
|
8014
|
+
// 座位注册(仅在旗标开启后被调用):会话头入口、对话行结算卡、
|
|
8015
|
+
// 右栏类型与正文。⚠️ 服务名 sidebarRight 与注册服务名 sidebarRightTabs
|
|
8016
|
+
// 是两个不同的名字——类型/引导挂 sidebarRightTabs,打开走
|
|
8017
|
+
// ctx.get("sidebarRight").openTab。无右栏服务时不注册 pane(浮层 +
|
|
8018
|
+
// 居中 sheet 退化已在 PipelineHeaderEntry 内)。
|
|
8019
|
+
function registerPipelineSeats(ctx, t) {
|
|
8020
|
+
let hasPane = false;
|
|
8021
|
+
try {
|
|
8022
|
+
const sb = typeof ctx.get === "function" ? ctx.get("sidebarRight") : null;
|
|
8023
|
+
hasPane = !!(sb && typeof sb.openTab === "function");
|
|
8024
|
+
} catch { hasPane = false; }
|
|
8025
|
+
// 右栏打开:页面类型(无 patterns)按 kind 打开——openTab(kind)。
|
|
8026
|
+
// 服务半可用 / kind 未注册抛错时返回 false,调用方退化到居中 sheet。
|
|
8027
|
+
const openPane = () => {
|
|
8028
|
+
try {
|
|
8029
|
+
const sb = typeof ctx.get === "function" ? ctx.get("sidebarRight") : null;
|
|
8030
|
+
if (sb && typeof sb.openTab === "function") {
|
|
8031
|
+
sb.openTab(PIPELINE_ACTIONS_TAB_KIND);
|
|
8032
|
+
return true;
|
|
8033
|
+
}
|
|
8034
|
+
} catch (err) {
|
|
8035
|
+
console.warn("[dsh-mneme] sidebarRight.openTab failed, falling back to sheet:", err);
|
|
8036
|
+
}
|
|
8037
|
+
return false;
|
|
8038
|
+
};
|
|
8039
|
+
|
|
8040
|
+
// 会话头入口:.actions 座位(.corner 被原生侧栏展开键占用,别用)。
|
|
8041
|
+
// 会话工作目录只有客户端拿得到,而「当前是哪个会话」只在座位注入里给——所以
|
|
8042
|
+
// 每个座位都要 inject: (sessionId) => ({ sessionId }),否则读到的永远是全局层。
|
|
8043
|
+
// 宿主若不吃这个选项,project 就是 null,面板如实说「项目未知」,不假装成功。
|
|
8044
|
+
ctx.slots.inject("conversation.session.header.actions", () => {
|
|
8045
|
+
return ctx.slots.register({
|
|
8046
|
+
name: "conversation.session.header.actions",
|
|
8047
|
+
id: "dsh-mneme:pipeline",
|
|
8048
|
+
order: 30,
|
|
8049
|
+
inject: (sessionId) => ({ sessionId })
|
|
8050
|
+
}, (props) => h(PipelineHeaderEntry, {
|
|
8051
|
+
t, hasPane, openPane, project: sessionProject(ctx, props && props.sessionId)
|
|
8052
|
+
}));
|
|
8053
|
+
});
|
|
8054
|
+
|
|
8055
|
+
// 对话行结算卡:回答「这次跑哪条被用上了」。
|
|
8056
|
+
ctx.slots.inject("conversation.chat.turnTail", () => {
|
|
8057
|
+
return ctx.slots.register({
|
|
8058
|
+
name: "conversation.chat.turnTail",
|
|
8059
|
+
id: "dsh-mneme:pipeline:settle",
|
|
8060
|
+
// kind/scope 是**声明方**(宿主)在 children 上写的,注册方不重复声明——
|
|
8061
|
+
// 座位本身已经声明成 { kind: "list", scope: "session" },多写一份只是噪音,
|
|
8062
|
+
// 万一注册侧按 schema 严校就是一次启动期抛错。
|
|
8063
|
+
inject: (sessionId) => ({ sessionId })
|
|
8064
|
+
}, (props) => h(TurnRunCard, { t, project: sessionProject(ctx, props && props.sessionId) }));
|
|
8065
|
+
});
|
|
8066
|
+
|
|
8067
|
+
// tab 的**类型**与它的**正文**必须各自兜自己的异常,而且类型注册要幂等。
|
|
8068
|
+
// 为什么:宿主对重复的类型 id 会抛(`sidebarRight: tab type id "…" is already registered`),
|
|
8069
|
+
// 而这两件事的生命周期不一样——类型注册挂在宿主服务 `sidebarRightTabs` 上、插件重挂(刷新 /
|
|
8070
|
+
// 热重载 / 会话重建)时**留着**;正文座位是挂插件 scope 的,会随插件回收、重挂时要重来。
|
|
8071
|
+
// 原来两件事写在同一个 try 里,于是第二次挂载时类型那一抛就把正文一起带走了:界面上
|
|
8072
|
+
// 只剩宿主的 fallback「这类内容还没有可用的查看方式」,而错误只落在浏览器控制台里
|
|
8073
|
+
// ——看不见、也搜不到,是最费时间的一类。
|
|
8074
|
+
try {
|
|
8075
|
+
const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : null;
|
|
8076
|
+
if (tabs && typeof tabs.register === "function") {
|
|
8077
|
+
const already = typeof tabs.get === "function" ? tabs.get(PIPELINE_ACTIONS_TAB_ID) : null;
|
|
8078
|
+
if (!already) {
|
|
8079
|
+
tabs.register({
|
|
8080
|
+
id: PIPELINE_ACTIONS_TAB_ID,
|
|
8081
|
+
kind: PIPELINE_ACTIONS_TAB_KIND,
|
|
8082
|
+
title: () => t("memory.actions.title"),
|
|
8083
|
+
guide: [{
|
|
8084
|
+
id: "dsh-mneme:pipeline:guide",
|
|
8085
|
+
order: 10,
|
|
8086
|
+
title: () => t("memory.actions.title"),
|
|
8087
|
+
description: () => t("memory.actions.notice"),
|
|
8088
|
+
icon: (size) => h(PipelineMark, { size, open: false })
|
|
8089
|
+
}]
|
|
8090
|
+
});
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
} catch (err) {
|
|
8094
|
+
console.warn("[dsh-mneme] action pipeline tab type registration failed:", err);
|
|
8095
|
+
}
|
|
8096
|
+
|
|
8097
|
+
try {
|
|
8098
|
+
const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : null;
|
|
8099
|
+
if (tabs && typeof tabs.register === "function") {
|
|
8100
|
+
// 正文/标题都是按键座位:key = 类型 id,宿主按当前 tab 取。
|
|
8101
|
+
ctx.slots.inject("sidebar.right.pane.tab", () => {
|
|
8102
|
+
return ctx.slots.register({
|
|
8103
|
+
name: "sidebar.right.pane.tab",
|
|
8104
|
+
key: PIPELINE_ACTIONS_TAB_ID,
|
|
8105
|
+
inject: (sessionId) => ({ sessionId })
|
|
8106
|
+
}, (props) => h(PipelinePane, { t, project: sessionProject(ctx, props && props.sessionId) }));
|
|
8107
|
+
});
|
|
8108
|
+
ctx.slots.inject("sidebar.right.pane.tab.title", () => {
|
|
8109
|
+
return ctx.slots.register({
|
|
8110
|
+
name: "sidebar.right.pane.tab.title",
|
|
8111
|
+
key: PIPELINE_ACTIONS_TAB_ID
|
|
8112
|
+
}, () => t("memory.actions.title"));
|
|
8113
|
+
});
|
|
8114
|
+
}
|
|
8115
|
+
} catch (err) {
|
|
8116
|
+
console.warn("[dsh-mneme] action pipeline pane body registration failed:", err);
|
|
8117
|
+
}
|
|
8118
|
+
|
|
8119
|
+
if (!hasPane) {
|
|
8120
|
+
console.info("[dsh-mneme] action pipeline: no sidebarRight service, pane tab skipped (floating panel + sheet fallback active)");
|
|
8121
|
+
}
|
|
8122
|
+
}
|
|
8123
|
+
|
|
6277
8124
|
function apply(ctx) {
|
|
6278
8125
|
const t = ctx.locale.bind(NS);
|
|
6279
8126
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
@@ -6345,6 +8192,26 @@ window.__ModuleLoader__.load({
|
|
|
6345
8192
|
}
|
|
6346
8193
|
return () => { dead = true; };
|
|
6347
8194
|
}, "dsh-mneme: better-sidebar mount");
|
|
8195
|
+
|
|
8196
|
+
// --- v0.9.0 M.O.(beta):旗标门控 ---
|
|
8197
|
+
// actionPipelineEnabled 关闭(默认)时一个座位都不注册——座位闸门
|
|
8198
|
+
// 在注册层而不是样式层(不是 display:none)。features 拉取失败时
|
|
8199
|
+
// 保持关闭(fail-closed),不猜。
|
|
8200
|
+
ctx.effect(() => {
|
|
8201
|
+
let dead = false;
|
|
8202
|
+
apiFetch("/api/dsh-mneme/features")
|
|
8203
|
+
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
|
|
8204
|
+
.then((j) => {
|
|
8205
|
+
if (dead) return;
|
|
8206
|
+
const eff = (j && j.effective) || {};
|
|
8207
|
+
if (!eff.actionPipelineEnabled) return;
|
|
8208
|
+
registerPipelineSeats(ctx, t);
|
|
8209
|
+
})
|
|
8210
|
+
.catch((err) => {
|
|
8211
|
+
console.warn("[dsh-mneme] features fetch failed, action pipeline stays off:", err);
|
|
8212
|
+
});
|
|
8213
|
+
return () => { dead = true; };
|
|
8214
|
+
}, "dsh-mneme: action-pipeline gate");
|
|
6348
8215
|
}
|
|
6349
8216
|
|
|
6350
8217
|
exports.apply = apply;
|
|
@@ -6352,3 +8219,7 @@ window.__ModuleLoader__.load({
|
|
|
6352
8219
|
return module.exports;
|
|
6353
8220
|
}
|
|
6354
8221
|
});
|
|
8222
|
+
|
|
8223
|
+
|
|
8224
|
+
|
|
8225
|
+
|