dsh-plugin-tool-management 0.18.0 → 0.18.5

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/CHANGELOG.md CHANGED
@@ -10,6 +10,37 @@
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.18.5] - 2026-10-05
14
+
15
+ 会话导出分三档累进(仅正文 → + 工具 → + 思考)+ 导出弹窗放大到看得到列表 + 导入可以挑工作区;修好行内「恢复」必报操作失败。
16
+
17
+ ### 新增
18
+
19
+ - **导出分三档,累进**:仅正文 → 正文 + 工具 → 正文 + 工具 + 思考,每一档都是上一档的超集。
20
+ - 中间档是这一节存在的理由:**要工具、不要思考**。思考是模型对自己干了什么的叙述,没有工具调用就没有它叙述的对象 —— 反向那一档(带思考不带工具)没有意义,所以三档只可能是这一个顺序。
21
+ - 附件(图片 / 文件的名字与大小)跟工具一起进中间档:它属于「对话里出现过什么」,不属于「模型想了什么」。
22
+ - 工具结果按上限截断并写明原文总长(一次文件读取就能有几 MB),不设限的话一个会话就能导出上百 MB。
23
+ - 附加段各自成节,且载荷整段缩进 —— 工具结果可以是任意文件内容,不缩进的话它里面的 `## User` 会在再导入时把你的轮次劈开。
24
+ - 后两档是给人看的,不是拿来回流的:再导入时工具输出与思考会并入助手正文;要回流对话请用「仅正文」。
25
+ - JSONL 的 `content` 仍是字符串而不是块数组 —— 块数组会让导入侧把思考又拼回正文,等于把这次修的缺陷原地复活。
26
+ - **导入会话可以挑工作区**:三条路并存 —— 从已登记的工作区下拉里选、浏览一个文件夹、直接粘绝对路径。留空仍归未分组。
27
+ - 下拉只列活登记的那几个。「已移除」的游离目录不冒充工作区(它按定义就是没登记),要用那个目录请走「浏览」。
28
+ - **导出弹窗从 560×640 放大到 720×760**:字段由五行压成三行(格式|导出内容 并一行、会话范围|工作区 并一行),省下的位置全给会话列表。只有列表内滚,提示 / 目录 / 动作行常驻不动。其余四页的导出弹窗与回收站尺寸不变。
29
+
30
+ ### 修复
31
+
32
+ - **归档列表每行「恢复」必弹「操作失败」**:宿主官方 `unarchiveSession` 是 `Promise<void>`,我们的门面把它的返回值原样透传,op 层再去读 `archivedSessionIds` 就抛 TypeError。崩溃点在**构造回执那一行** —— 宿主其实已经恢复成功、工作区记账也补回来了,用户看到的却是一句失败,而且列表不刷新,得手动刷一下才发现它好了。现在回执由门面按权威归档集合自造,op 层不再读注册表的返回值。
33
+ - **导出的正文里混着思考**:取文本那一步只判「这个对象有没有 text 字段」,而思考块 `{type:'reasoning', text}` 恰好符合,于是思考一直和正文拼在一起导出去。现在按块类型取文本 —— 同一条缺陷 0.16.x 已在子智能体侧修过,导出侧是它的第二个出口。
34
+ - 副作用要说清:**同一份会话在 0.18.5 前后的导出产物对不上**,少掉的那段就是当初混进来的思考。
35
+ - 因此「只有思考、没有正文」的助手轮次在简洁档会第一次变成导不出来,跳过原因里会写明是档位问题、不是内容丢了。
36
+
37
+ ### 变更
38
+
39
+ - **导入弹窗那一格改叫「工作区」**(原来叫「项目目录」),与导出弹窗的筛选同名同义;文件要求那一行跟着改口。
40
+ - **恢复回执不再带 `archivedSessionIds`**:界面只用 `ok` / `error` / `workspace`,而归档集合可能有几千条,每次单点恢复都全量回传一遍没有意义。
41
+
42
+ ---
43
+
13
44
  ## [0.18.0] - 2026-10-01
14
45
 
15
46
  斜杠命令「工具」段 + 快捷提示词(含场景绑定)+ 兼容页两段开关与挂载实况。
package/README.md CHANGED
@@ -138,7 +138,7 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
138
138
  ### 历史会话
139
139
 
140
140
  - 按项目分组、搜索、批量恢复 / 永久删除、保留期自动清理;工作区登记被删后可一键重新登记。
141
- - 导入 Claude Code / Cursor / Codex / 任意文本;导出 Markdown / JSONL。**导出是可读转录,不是完整备份**:只保留 user / assistant 的文本块,工具调用、图片、思考过程与 token 统计都不在里面。要留全量请用「归档」。
141
+ - 导入 Claude Code / Cursor / Codex / 任意文本,工作区可以从已登记的里挑、浏览文件夹或直接给路径;导出 Markdown / JSONL,分三档**累进**:仅正文 → 正文 + 工具 → 正文 + 工具 + 思考。**导出仍是可读转录,不是完整备份**:附加段每块有上限并明说截断,token 统计不在里面。要留全量请用「归档」。
142
142
 
143
143
  ### 宿主兼容
144
144
 
package/README_EN.md CHANGED
@@ -139,7 +139,7 @@ Then remind me to hard-refresh the browser.
139
139
  ### Archived sessions
140
140
 
141
141
  - Grouped by project, searchable, batch restore / delete, retention auto-cleanup; a workspace whose registration was deleted is rebuilt from the session directories and can be re-registered in one click.
142
- - Import Claude Code / Cursor / Codex / any text; export Markdown / JSONL. **An export is a readable transcript, not a full backup**: only user/assistant text blocks survive — tool calls, images, reasoning and token stats are dropped. Use archiving when you need the full record.
142
+ - Import Claude Code / Cursor / Codex / any text — pick a registered workspace, browse to a folder, or give a path directly; export Markdown / JSONL in three cumulative modes: text only → plus tool calls and results and attachment names → plus reasoning. **An export is still a readable transcript, not a full backup**: the added blocks are capped and say so, and token stats are not included. Use archiving when you need the full record.
143
143
 
144
144
  ### Host compatibility
145
145
 
package/docs/update.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  | 版本 | 发布 | 主题 |
6
6
  | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | [0.18.5](#0185---2026-10-05) | 2026-10-05 | 会话导出分三档累进(仅正文 → 正文 + 工具 → 正文 + 工具 + 思考,工具结果有上限并明说截断)+ 导出弹窗放大到 720×760、字段压成三行把位置还给会话列表 + 导入会话可以从已登记工作区里挑 / 浏览文件夹 + 修好归档列表每行「恢复」必弹「操作失败」(其实已经恢复了)与导出的正文里混着思考 |
7
8
  | [0.18.0](#0180---2026-10-01) | 2026-10-01 | 斜杠命令「工具」段(对话里打 `/` 就能进场景、启停六个域,点完不关)+ 快捷提示词(`/` 里点一下贴进输入框;场景可绑定一组)+ 兼容页两段开关与挂载实况 + 提示页药丸开关与可折叠卡片 + 修好分组卡片收不起来、退出场景回执说「什么都没改」、`/` 进 / 出场景不跟着切档案 |
8
9
  | [0.17.0](#0170---2026-09-30) | 2026-09-30 | 对抗性审查收尾(三批 19 条 + 复审 14 条 + 2 条替代项,全部处理):修好 3 处数据丢失与 2 处凭据外泄、停用 MCP 工具仍可调用等**执行侧**守卫失效、场景回收站与记忆索引的恢复问题;MCP 工具全名的切分收敛成一套规则、服务器名含 `__` 的歧义会出声提示 |
9
10
  | [0.16.6](#0166---2026-09-30) | 2026-09-30 | 官方内置技能补全 web 侧:web 照注的沙盒诊断技能可见可停 + Office 三件套 web 也暴露 + 补上接管后缺失的 LibreOffice Kit 路径段(0.16.5 桌面已缺) |
@@ -28,6 +29,18 @@
28
29
 
29
30
  ---
30
31
 
32
+ ## 0.18.5 - 2026-10-05
33
+
34
+ - **导出分三档,累进**:仅正文 → 正文 + 工具 → 正文 + 工具 + 思考,每档都是上一档的超集。工具结果按上限截断并写明原文一共多长 —— 一次文件读取就能有几 MB,不设限的话一个会话就能导出上百 MB。
35
+ - **中间档是「要工具、不要思考」**:思考是模型对自己干了什么的叙述,没有工具调用就没有它叙述的对象,所以反向那一档没有意义。附件跟着工具走中间档 —— 它属于「对话里出现过什么」,不属于「模型想了什么」。
36
+ - **导出的正文里不再混着思考**:此前取文本只判「有没有 text 字段」,思考块恰好符合,所以它一直和正文拼在一起出去。同一份会话在 0.18.5 前后的产物对不上,少掉的那段就是当初混进来的思考。
37
+ - **导出弹窗放大**(560×640 → 720×760):字段从五行压成三行 —— 格式与导出内容并一行、会话范围与工作区并一行(两个正交条件,没合成一个下拉),省下的位置全给会话列表。只有列表内滚,提示 / 目录 / 动作行常驻。其余四页的导出弹窗与回收站不变。
38
+ - **导入会话可以挑工作区了**:从已登记的工作区里选、浏览一个文件夹、或直接粘绝对路径,三条路并存;留空仍归未分组。那一格也跟着改口叫「工作区」,与导出弹窗的筛选同名。
39
+ - **后两档是给人看的,不是拿来回流的**:再导入时工具输出与思考会并入助手正文。要接着聊用「仅正文」。
40
+ - **修好归档列表每行的「恢复」**:点一下必弹「操作失败」,可宿主其实已经恢复成功、工作区记账也补回来了 —— 回执在构造时读了一个宿主根本不返回的字段,抛了。现在回执由插件自己按权威归档集合构造。
41
+
42
+ ---
43
+
31
44
  ## 0.18.0 - 2026-10-01
32
45
 
33
46
  - **斜杠命令「工具」段**:对话输入框打 `/` 就能进场景、启停技能 / MCP 服务器 / 子智能体 / 提示词 / 记忆,点完菜单不关、接着点下一条。走宿主官方 `inputTriggers` 通道(不碰官方包),只放可逆动作,读写复用面板那批接口。
package/lib/client.js CHANGED
@@ -469,6 +469,15 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
469
469
  .dsm-export-modal .dsm-export-form{display:flex;min-height:0;flex:1;flex-direction:column;gap:10px}
470
470
  .dsm-export-modal .dsm-pick-list{flex:1;min-height:0;max-height:none;overflow:auto}
471
471
  .dsm-export-modal .dsm-export-body{display:flex;min-height:0;flex:1;flex-direction:column;gap:6px}
472
+ /* 会话导出弹窗(0.18.5):字段压成三行之后,560×640 里会话列表只剩几行(用户反馈)。
473
+ 处方照 .dsm-modal-archive / .dsm-modal-fixed:固定高、只有列表内滚。宽度取 720 —— 与
474
+ .dsm-modal-lg / .dsm-modal-wide 同一档,两列字段各约 340px 刚好放得下(880 试过,标题行
475
+ 右边空成一片,用户说太宽)。高度取 760 而不是视口百分比 —— 760 既是 .dsm-modal 自己的
476
+ max-height 也是 .dsm-modal-fixed 那个数,窗口矮时 calc(100vh - 48px) 照常接管。
477
+ 写成两段类名(.dsm-modal.dsm-modal-export-lg)是为了**不靠书写顺序**压过 .dsm-modal-list
478
+ 的 !important(.dsm-modal-lg 排在 .dsm-modal-list 前面,直接借用会被它盖掉)。只加在这一颗
479
+ 弹窗上:.dsm-modal-list 还被四个页面的导出弹窗和回收站共用,动它就是它们一起变大。 */
480
+ .dsm-modal.dsm-modal-export-lg{width:min(720px,100%)!important;height:min(760px,calc(100vh - 48px))!important;max-height:none!important}
472
481
  .dsm-pick-group{display:flex;flex-direction:column}
473
482
  .dsm-pick-group-head{box-sizing:border-box;display:flex;width:100%;min-width:0;align-items:center;gap:7px;padding:7px 2px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;text-align:left;cursor:pointer}
474
483
  .dsm-pick-group-head:hover{color:var(--dsw-alias-label-primary)}
@@ -3359,7 +3368,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3359
3368
  "source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
3360
3369
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
3361
3370
  "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 skill_manager_save 建的是用户级技能",
3362
- "import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名", "upload.requirement.memory.3": "带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.4": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "项目目录可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
3371
+ "import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名", "upload.requirement.memory.3": "带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.4": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "工作区可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
3363
3372
  // F-029:服务端错误码补词条(zh 逐字复现服务端文案,命中词条前后中文显示不变;EN 不再回退中文原文)。
3364
3373
  "error.custom.absolute": "目录路径必须是绝对路径: {path}", "error.custom.notDirectory": "不是目录: {path}", "error.custom.unreadable": "无法读取目录: {path}", "error.custom.overlap": "目录与已有技能来源重叠: {path} ↔ {other}", "error.custom.invalidKey": "非法的自定义来源 key: {key}", "error.custom.notFound": "自定义来源不存在: {key}",
3365
3374
  // 与宿主 http-fence.ts 的 TOKEN_MSG **逐字相同**:同一件事在哪个页面出现都必须说同一句话,
@@ -3949,7 +3958,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3949
3958
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
3950
3959
  "error.import.noFiles": "没有选择要导入的文件",
3951
3960
  "nav.title": "工具",
3952
- "agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "按场景勾选", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按它恢复——删了就没法恢复了", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "项目目录(可留空)", "hist.import.cwd.placeholder": "绝对路径,可留空(会话归入未分组)", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
3961
+ "agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "按场景勾选", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按它恢复——删了就没法恢复了", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "工作区(可留空)", "hist.import.cwd.placeholder": "选择、浏览或直接输入绝对路径;留空 = 归入未分组", "hist.import.workspace": "选工作区", "hist.import.browse": "浏览", "hist.import.workspace.none": "本机还没有已登记的工作区:可以直接输入绝对路径,或用「浏览」选一个文件夹。", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.detail": "导出内容", "hist.export.detail.simple": "仅正文", "hist.export.detail.tools": "正文 + 工具", "hist.export.detail.full": "正文 + 工具 + 思考", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
3953
3962
  },
3954
3963
  en: {
3955
3964
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -3988,7 +3997,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3988
3997
  "source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; a default source, always read — it cannot be removed or toggled off",
3989
3998
  "detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
3990
3999
  "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "skill_manager_save in chat creates user-level skills",
3991
- "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The project directory may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
4000
+ "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The workspace may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
3992
4001
  "error.custom.absolute": "The folder path must be absolute: {path}", "error.custom.notDirectory": "Not a folder: {path}", "error.custom.unreadable": "Could not read the folder: {path}", "error.custom.overlap": "The folder overlaps an existing skill source: {path} ↔ {other}", "error.custom.invalidKey": "Invalid custom source key: {key}", "error.custom.notFound": "Custom source not found: {key}",
3993
4002
  // 逐字对应宿主 http-fence.ts 的 TOKEN_MSG_*(见中文侧注释)。
3994
4003
  "error.token.required": "Missing access token, or the wrong one",
@@ -4568,7 +4577,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4568
4577
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
4569
4578
  "error.import.noFiles": "No files selected",
4570
4579
  "nav.title": "Tools",
4571
- "agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
4580
+ "agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick, browse, or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.browse": "Browse", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path, or use Browse to pick a folder.", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
4572
4581
  }
4573
4582
  };
4574
4583
 
@@ -6844,6 +6853,11 @@ function enabledMemoryCount(rules) {
6844
6853
  }
6845
6854
  // 场景输入(combobox):既能从已有场景里挑,也能直接手输一个新场景名——
6846
6855
  // 宿主 rules-create 用 mkdir(recursive) 落盘,所以「自定义场景」无需先建目录。
6856
+ //
6857
+ // `onPickFolder` 是可选的第二颗按钮(0.18.5,会话导入选工作区):选项来自登记列表,但用户
6858
+ // 也可能要挑一个从没登记过的目录,那就得走目录浏览器。它渲染在 .dsm-combo-row **里面**
6859
+ // 而不是外面 —— .dsm-field 是列向 flex,放外面会掉到下拉框下方;而 .dsm-select-menu 相对
6860
+ // .dsm-select 绝对定位,另起一行就没有对齐的锚点了。两颗都是 .dsm-btn-secondary,同高。
6847
6861
  function SceneCombo(props) {
6848
6862
  var state = react.useState(false), open = state[0], setOpen = state[1], ref = react.useRef(null);
6849
6863
  react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
@@ -6851,7 +6865,9 @@ function enabledMemoryCount(rules) {
6851
6865
  return h("div", { className: "dsm-select", ref: ref },
6852
6866
  h("div", { className: "dsm-combo-row" },
6853
6867
  h("input", { className: "dsm-control" + (props.invalid ? " dsm-rule-invalid" : ""), type: "text", value: props.value || "", placeholder: props.placeholder, "aria-label": props.label, onChange: function (e) { props.onChange(e.target.value); } }),
6854
- h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel)),
6868
+ h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel),
6869
+ // 开目录浏览器前先收菜单:这颗按钮在 ref 里面,document 上那个 pointerdown 关闭器不会替我关。
6870
+ props.onPickFolder ? h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", title: props.pickFolderTitle, disabled: props.disabled === true, onClick: function () { setOpen(false); props.onPickFolder(); } }, props.pickFolderLabel) : null),
6855
6871
  open && options.length ? h("div", { className: "dsm-select-menu", role: "listbox" }, options.map(function (o) { return h("button", { key: o.value, type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
6856
6872
  }
6857
6873
  function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, title: props.title, disabled: props.disabled, onClick: props.onClick }); }
@@ -8098,10 +8114,10 @@ function enabledMemoryCount(rules) {
8098
8114
  // 多选集合:勾选的 sessionId。最小 React 支持函数式初值(Set 惰性创建)。
8099
8115
  var selState = React.useState(function () { return new Set() })
8100
8116
  var selected = selState[0], setSelected = selState[1]
8101
- // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
8117
+ // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd, pickingDir } = 打开。
8102
8118
  var impState = React.useState(null)
8103
8119
  var imp = impState[0], setImp = impState[1]
8104
- // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
8120
+ // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, detail, wsFilter, selectedIds } = 打开。
8105
8121
  var expState = React.useState(null)
8106
8122
  var exp = expState[0], setExp = expState[1]
8107
8123
  // 分组卡片折叠状态:key → true(收起)。默认全部展开。
@@ -8270,11 +8286,11 @@ function enabledMemoryCount(rules) {
8270
8286
  reader.readAsText(file)
8271
8287
  }
8272
8288
  function openImportPicker() {
8273
- setImp({ busy: false, error: null, result: null, cwd: '' })
8289
+ setImp({ busy: false, error: null, result: null, cwd: '', pickingDir: false })
8274
8290
  }
8275
8291
  // ---------- 导出对话:弹窗内选范围/工作区 → 勾选会话 → 写入指定目录 ----------
8276
8292
  function openExportPicker() {
8277
- setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
8293
+ setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', detail: 'simple', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
8278
8294
  // 拉取全部持久化会话(含未归档/冷会话),供选择导出。
8279
8295
  apiCall('history-sessions', {}).then(function (res) {
8280
8296
  setExp(function (prev) {
@@ -8315,7 +8331,7 @@ function enabledMemoryCount(rules) {
8315
8331
  var ids = Array.from(exp.selectedIds)
8316
8332
  if (!ids.length || !String(exp.outDir || '').trim()) return
8317
8333
  setExp(Object.assign({}, exp, { busy: true, error: null, result: null }))
8318
- apiCall('history-export', { sessionIds: ids, format: exp.format, outDir: String(exp.outDir).trim() }).then(function (res) {
8334
+ apiCall('history-export', { sessionIds: ids, format: exp.format, detail: exp.detail, outDir: String(exp.outDir).trim() }).then(function (res) {
8319
8335
  if (res && res.ok) setExp(Object.assign({}, exp, { busy: false, result: res }))
8320
8336
  else setExp(Object.assign({}, exp, { busy: false, error: (res && res.error) || t('hist.err.export') }))
8321
8337
  }).catch(function () {
@@ -8357,6 +8373,11 @@ function enabledMemoryCount(rules) {
8357
8373
  { value: 'markdown', label: 'Markdown' },
8358
8374
  { value: 'jsonl', label: 'JSONL' },
8359
8375
  ]
8376
+ var EXPORT_DETAIL_OPTS = [
8377
+ { value: 'simple', label: t('hist.export.detail.simple') },
8378
+ { value: 'tools', label: t('hist.export.detail.tools') },
8379
+ { value: 'full', label: t('hist.export.detail.full') },
8380
+ ]
8360
8381
  var EXPORT_ARCH_OPTS = [
8361
8382
  { value: 'all', label: t('hist.arch.all') },
8362
8383
  { value: 'archived', label: t('hist.arch.archived') },
@@ -8375,6 +8396,24 @@ function enabledMemoryCount(rules) {
8375
8396
  })
8376
8397
  exportWsOptions.push({ value: 'ungrouped', label: t('hist.ungrouped') })
8377
8398
  }
8399
+ // 导入弹窗的工作区候选:只列**活登记**的组,值用目录路径(history-import 吃的就是路径)。
8400
+ // 「已移除」的游离组不进来 —— 它按定义就是没登记,让它冒充「选一个工作区」会静默造出
8401
+ // 本页标着「已移除」的那种状态;真要用那个目录还有「浏览」,手输也一直留着。
8402
+ var importWsOptions = []
8403
+ ;(function () {
8404
+ var seen = {}
8405
+ function pushOption(title, path) {
8406
+ var p = String(path || '')
8407
+ if (!p || seen[p]) return
8408
+ seen[p] = true
8409
+ importWsOptions.push({ value: p, label: (title ? title + ' — ' : '') + p })
8410
+ }
8411
+ ;(data.groups || []).forEach(function (g) { if (g && g.kind === 'live') pushOption(g.title, g.path) })
8412
+ if (!importWsOptions.length) {
8413
+ var ws = data.workspaces || {}
8414
+ Object.keys(ws).forEach(function (wid) { pushOption(ws[wid] && ws[wid].title, ws[wid] && ws[wid].path) })
8415
+ }
8416
+ })()
8378
8417
  var exportItems = []
8379
8418
  if (exp) {
8380
8419
  var useGroups = !!(data.groups && data.groups.length)
@@ -8505,28 +8544,49 @@ function enabledMemoryCount(rules) {
8505
8544
  extra: React.createElement('div', null,
8506
8545
  React.createElement('div', { className: 'dsm-field' },
8507
8546
  React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
8508
- React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: t('hist.import.cwd.placeholder'), value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } }))),
8547
+ // 三条路并存:从已登记的工作区挑、浏览一个文件夹、直接粘路径。
8548
+ // 标签是 combo 的**兄弟**而不是祖先 —— 嵌套 label 会把点击转给清单里第一项。
8549
+ React.createElement(SceneCombo, {
8550
+ options: importWsOptions, value: imp.cwd || '',
8551
+ placeholder: t('hist.import.cwd.placeholder'), label: t('hist.import.cwd'),
8552
+ browseLabel: t('hist.import.workspace'),
8553
+ pickFolderLabel: t('hist.import.browse'), pickFolderTitle: t('hist.export.browse'),
8554
+ disabled: imp.busy === true,
8555
+ onChange: function (v) { setImp(Object.assign({}, imp, { cwd: v })) },
8556
+ onPickFolder: function () { setImp(Object.assign({}, imp, { pickingDir: true })) },
8557
+ }),
8558
+ imp.pickingDir ? React.createElement(DirPickerModal, { key: 'imp-dir-picker', title: t('dir.title'), initial: String(imp.cwd || ''), closeLabel: t('btn.close'), onClose: function () { setImp(Object.assign({}, imp, { pickingDir: false })) }, onPick: function (path) { setImp(Object.assign({}, imp, { cwd: path, pickingDir: false })) } }) : null,
8559
+ importWsOptions.length ? null : React.createElement('p', { className: 'dsm-help' }, t('hist.import.workspace.none')))),
8509
8560
  onClose: function () { setImp(null) },
8510
8561
  onSubmit: function (entries, files) { doImportFile(files[0]) },
8511
8562
  }) : null,
8512
- exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-list dsm-export-modal', list: true, title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
8513
- React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
8514
- React.createElement('div', { className: 'dsm-field' },
8515
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
8516
- React.createElement('div', { className: 'dsm-dir-row' },
8517
- React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: 'D:\\backups\\dsh\\exports', value: exp.outDir || '', onChange: function (e) { setExp(Object.assign({}, exp, { outDir: e.target.value })) } }),
8518
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: exp.busy, onClick: openExportDir, title: t('hist.export.browse') }, t('hist.btn.browse'))),
8519
- exp.pickingDir ? React.createElement(DirPickerModal, { key: 'exp-dir-picker', title: t('hist.export.pickDir'), initial: String(exp.outDir || ''), closeLabel: t('btn.close'), onClose: function () { setExp(Object.assign({}, exp, { pickingDir: false })) }, onPick: function (path) { setExp(Object.assign({}, exp, { outDir: path, pickingDir: false })) } }) : null),
8520
- React.createElement('div', { className: 'dsm-field' },
8521
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.format')),
8522
- React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
8523
- React.createElement('div', { className: 'dsm-field' },
8524
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.scope')),
8525
- React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
8526
- React.createElement('div', { className: 'dsm-field' },
8527
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
8528
- React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } })),
8529
- React.createElement('div', { className: 'dsm-field dsm-export-body' },
8563
+ // 导出弹窗(0.18.5):字段从五行压成三行(格式|导出内容 一行、会话范围|工作区
8564
+ // 一行),整块包进 .dsm-export-form 让列表吃掉剩下的高度。范围和工作是两个正交
8565
+ // 条件,不做成笛卡尔积的单个下拉 —— 合成一行只是省位置,语义没合。
8566
+ exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-list dsm-modal-export-lg dsm-export-modal', list: true, title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
8567
+ React.createElement('div', { className: 'dsm-form dsm-export-form' },
8568
+ React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
8569
+ React.createElement('div', { className: 'dsm-field' },
8570
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
8571
+ React.createElement('div', { className: 'dsm-dir-row' },
8572
+ React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: 'D:\\backups\\dsh\\exports', value: exp.outDir || '', onChange: function (e) { setExp(Object.assign({}, exp, { outDir: e.target.value })) } }),
8573
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: exp.busy, onClick: openExportDir, title: t('hist.export.browse') }, t('hist.btn.browse'))),
8574
+ exp.pickingDir ? React.createElement(DirPickerModal, { key: 'exp-dir-picker', title: t('hist.export.pickDir'), initial: String(exp.outDir || ''), closeLabel: t('btn.close'), onClose: function () { setExp(Object.assign({}, exp, { pickingDir: false })) }, onPick: function (path) { setExp(Object.assign({}, exp, { outDir: path, pickingDir: false })) } }) : null),
8575
+ React.createElement('div', { className: 'dsm-field-row' },
8576
+ React.createElement('div', { className: 'dsm-field' },
8577
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.format')),
8578
+ React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
8579
+ React.createElement('div', { className: 'dsm-field' },
8580
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.detail')),
8581
+ React.createElement(SourceSelect, { options: EXPORT_DETAIL_OPTS, value: exp.detail, onChange: function (v) { setExp(Object.assign({}, exp, { detail: v })) } }))),
8582
+ React.createElement('div', { className: 'dsm-field-row' },
8583
+ React.createElement('div', { className: 'dsm-field' },
8584
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.scope')),
8585
+ React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
8586
+ React.createElement('div', { className: 'dsm-field' },
8587
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
8588
+ React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } }))),
8589
+ React.createElement('div', { className: 'dsm-field dsm-export-body' },
8530
8590
  React.createElement('div', { className: 'dsm-label' }, t('hist.export.sessions', { count: exportItems.length })),
8531
8591
  React.createElement('div', { className: 'dsm-source dsm-pick-list' },
8532
8592
  exp.loading ? React.createElement('div', { className: 'dsm-empty' }, t('hist.export.loading'))
@@ -8556,7 +8616,8 @@ function enabledMemoryCount(rules) {
8556
8616
  React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? t('hist.tag.archived') : t('hist.tag.live'))))
8557
8617
  }) : null)
8558
8618
  })
8559
- })() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty')))),
8619
+ })() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty'))))),
8620
+ // ↑ 最后那个括号收掉 .dsm-export-form:结果条与动作行留在表单外面,才能常驻不动。
8560
8621
  exp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(exp.error)) : null,
8561
8622
  exp.result ? React.createElement('div', { className: 'dsm-feedback' },
8562
8623
  exp.result.exported
@@ -0,0 +1,223 @@
1
+ // 会话转录导出:把宿主的 SessionEvent[] 提成可再导入的转录文本。
2
+ //
3
+ // 从 src/index.ts 剥出来(0.18.5),同时补上两件事:
4
+ //
5
+ // 1. **按块类型取文本**。宿主的内容块是带 type 标签的联合(dsh-llm types.d.ts:114-122):
6
+ // `text` 是用户可见正文,`reasoning` 是思考,`tool-call` 是模型发起的调用。此前导出走
7
+ // `extractText`,它只判「这个对象有没有 text 字段」—— 而 `ReasoningBlock` 恰好就是
8
+ // `{type:'reasoning', text}`,所以**思考一直混在助手正文里导出去**。同一类缺陷 0.16.x
9
+ // 已在子智能体侧用 `textOfBlocks` 修过(persona-parse.ts:275),导出侧是它的第二个出口。
10
+ // 2. **导出分三档,且是累进的**:正文 → 另加工具 → 再加思考。每档都是上一档的超集,
11
+ // 所以一个下拉就说得清(两个独立勾选框是另一种答案,用户不要)。
12
+ // 三档仍是**可读转录,不是完整备份**:每块有上限并明说截断,token 统计与压缩结构不在里面。
13
+ //
14
+ // `extractText` 本身不动:它服务的是外部文件导入,那份宽松是故意的(外来转录稿没有
15
+ // `reasoning` 词表可守)。
16
+ import { textOfBlocks } from '../subagents/persona-parse.js';
17
+ // 上限是**病态保护**:工具结果可以是一次几 MB 的文件读取,不设限的话一个会话就能
18
+ // 导出上百 MB。取最靠前的那一段 —— 工具输出的头部几乎总是最有用的那部分。
19
+ const REASONING_MAX = 6000;
20
+ const TOOL_INPUT_MAX = 1000;
21
+ const TOOL_RESULT_MAX = 2000;
22
+ /** 裁到 max 个字符并留下标记。标记固定英文:op 跑在服务端,没有客户端词典可用。 */
23
+ function cap(text, max) {
24
+ const s = String(text || '');
25
+ if (s.length <= max)
26
+ return s;
27
+ return `${s.slice(0, max)}\n[truncated: showing ${max} of ${s.length} chars]`;
28
+ }
29
+ function str(value) {
30
+ return typeof value === 'string' ? value : '';
31
+ }
32
+ function blocksOf(content) {
33
+ return Array.isArray(content) ? content : [];
34
+ }
35
+ function formatBytes(n) {
36
+ const v = Number(n);
37
+ if (!Number.isFinite(v) || v < 0)
38
+ return 'unknown size';
39
+ if (v < 1024)
40
+ return `${v} B`;
41
+ if (v < 1024 * 1024)
42
+ return `${Math.round(v / 1024)} KB`;
43
+ return `${(Math.round((v / 1024 / 1024) * 10) / 10).toFixed(1)} MB`;
44
+ }
45
+ /** 附件只报名字 / 类型 / 尺寸 / 字节数 —— `attachmentId` 官方定义为不透明串,进了转录稿只是噪声。 */
46
+ function attachmentLines(content) {
47
+ const lines = [];
48
+ for (const block of blocksOf(content)) {
49
+ const ref = (block.attachment && typeof block.attachment === 'object' ? block.attachment : {});
50
+ if (block.type === 'image') {
51
+ const media = str(ref.mediaType) || 'image';
52
+ const w = Number(ref.width);
53
+ const h = Number(ref.height);
54
+ const dims = Number.isFinite(w) && Number.isFinite(h) ? ` ${w}×${h}` : '';
55
+ const name = str(ref.name);
56
+ lines.push(`image: ${name ? name + ' — ' : ''}${media}${dims} (${formatBytes(ref.bytes)})`);
57
+ }
58
+ else if (block.type === 'file') {
59
+ lines.push(`file: ${str(ref.name) || '(未命名)'} (${formatBytes(ref.bytes)})`);
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+ function toolLabel(name, kind, failed = false) {
65
+ return `Tool: ${name} — ${kind}${failed ? ' (failed)' : ''}`;
66
+ }
67
+ /**
68
+ * 把会话事件提成导出用的轮次。
69
+ *
70
+ * 工具的**发起**在两处都有记录:`assistant/message` 里的 `tool-call` 块(模型可见真相),
71
+ * 与 log-only 的 `tool/call` 事件(持久化记录)。两边都收、按 call id 去重 —— 只认一边
72
+ * 会在某种宿主形状下静默丢掉全部工具调用,而"导出里看不见工具调用"正是这份文件此前的毛病。
73
+ * 配不上任何调用的 `tool/result` 也照样单独成段,同样不静默丢。
74
+ */
75
+ export function extractExportTurns(events, detail) {
76
+ const wantTools = detail !== 'simple';
77
+ const wantThinking = detail === 'full';
78
+ const turns = [];
79
+ const namesByCallId = new Map();
80
+ const emittedCallIds = new Set();
81
+ let current = null;
82
+ const flush = () => {
83
+ // 空正文 + 空附加段 = 这一轮什么都没有,不占位。
84
+ if (current && (current.text || current.details.length))
85
+ turns.push(current);
86
+ current = null;
87
+ };
88
+ /** 工具细节总归属于助手轮;正文已经翻篇时就另起一轮。 */
89
+ const openAssistant = () => {
90
+ if (!current || current.role !== 'assistant') {
91
+ flush();
92
+ current = { role: 'assistant', text: '', details: [] };
93
+ }
94
+ return current;
95
+ };
96
+ for (const event of events || []) {
97
+ if (!event || typeof event !== 'object')
98
+ continue;
99
+ const e = event;
100
+ const data = (e.data && typeof e.data === 'object' ? e.data : {});
101
+ if (e.type === 'user/message') {
102
+ flush();
103
+ const turn = { role: 'user', text: textOfBlocks(data.content, 'text'), details: [] };
104
+ // 附件跟工具一起进中间档:它是「对话里出现过什么」的一部分,不是「模型想了什么」。
105
+ if (wantTools) {
106
+ const attachments = attachmentLines(data.content);
107
+ if (attachments)
108
+ turn.details.push({ label: 'Attachment', text: attachments });
109
+ }
110
+ current = turn;
111
+ continue;
112
+ }
113
+ if (e.type === 'assistant/message') {
114
+ flush();
115
+ const message = (data.message && typeof data.message === 'object' ? data.message : {});
116
+ const turn = { role: 'assistant', text: textOfBlocks(message.content, 'text'), details: [] };
117
+ // 顺序即模型产出的顺序:先思考、后调用。两档都开时(full)这个次序天然成立,
118
+ // 因为 full 必然也开 wantTools。
119
+ if (wantThinking) {
120
+ const reasoning = textOfBlocks(message.content, 'reasoning');
121
+ if (reasoning)
122
+ turn.details.push({ label: 'Thinking', text: cap(reasoning, REASONING_MAX) });
123
+ }
124
+ if (wantTools) {
125
+ for (const block of blocksOf(message.content)) {
126
+ if (block.type !== 'tool-call')
127
+ continue;
128
+ const id = str(block.id);
129
+ const name = str(block.name) || '(unnamed)';
130
+ if (id) {
131
+ namesByCallId.set(id, name);
132
+ emittedCallIds.add(id);
133
+ }
134
+ turn.details.push({ label: toolLabel(name, 'in'), text: cap(str(block.arguments), TOOL_INPUT_MAX) });
135
+ }
136
+ }
137
+ current = turn;
138
+ continue;
139
+ }
140
+ // 往下只剩工具与结果两类事件,中间档就要,只有简洁档跳过。
141
+ if (!wantTools)
142
+ continue;
143
+ if (e.type === 'tool/call') {
144
+ const id = str(data.callId);
145
+ const name = str(data.name) || namesByCallId.get(id) || '(unnamed)';
146
+ if (id)
147
+ namesByCallId.set(id, name);
148
+ // assistant 块里已经列过同一个调用,就别重复成段。
149
+ if (id && emittedCallIds.has(id))
150
+ continue;
151
+ if (id)
152
+ emittedCallIds.add(id);
153
+ openAssistant().details.push({ label: toolLabel(name, 'in'), text: cap(str(data.arguments), TOOL_INPUT_MAX) });
154
+ continue;
155
+ }
156
+ if (e.type === 'tool/result') {
157
+ const message = (data.message && typeof data.message === 'object' ? data.message : {});
158
+ const id = str(message.toolCallId);
159
+ const name = namesByCallId.get(id) || '(unknown)';
160
+ const failed = message.isError === true;
161
+ const body = textOfBlocks(message.content, 'text');
162
+ const blocks = blocksOf(message.content);
163
+ openAssistant().details.push({
164
+ label: toolLabel(name, 'out', failed),
165
+ text: cap(body || (blocks.length ? `(无文本输出,${blocks.length} 个内容块)` : '(无输出)'), TOOL_RESULT_MAX),
166
+ });
167
+ // `error` 官方注释明说它在模型可见消息之外 —— 带工具的那两档是用户唯一能看到它的地方。
168
+ const err = (data.error && typeof data.error === 'object' ? data.error : undefined);
169
+ if (err) {
170
+ const reason = str(err.reason) || str(err.code) || str(err.name);
171
+ if (reason)
172
+ openAssistant().details.push({ label: toolLabel(name, 'error'), text: cap(`${str(err.name)}/${str(err.code)} — ${reason}`, TOOL_RESULT_MAX) });
173
+ }
174
+ continue;
175
+ }
176
+ }
177
+ flush();
178
+ return turns;
179
+ }
180
+ /**
181
+ * 把载荷整段缩进 4 空格。
182
+ *
183
+ * 这是**回流保护**,不是排版:工具结果可以是任意文件内容,读进一份 markdown 转录稿就会把
184
+ * 里面的 `## User` 原样吐进助手正文,再导入时把轮次劈开。缩进同时废掉 `HEADING_RE` 与
185
+ * `BOLD_PREFIX_RE` 两个行首锚点(两者都不允许前导空白)。空行保持真空,不留尾随空格。
186
+ */
187
+ function indentBlock(text) {
188
+ return String(text || '')
189
+ .split('\n')
190
+ .map((line) => (line.trim() ? ` ${line}` : ''))
191
+ .join('\n');
192
+ }
193
+ /**
194
+ * 序列化为可再导入的转录文本:Codex 风格 Markdown 或 Claude Code 风格 JSONL。
195
+ *
196
+ * 签名与 0.18.5 之前一致 —— 附加信息全在 `turn.details` 里,所以 `details` 为空时两种输出
197
+ * 与旧版逐字节相同。`## User` / `### Assistant` 两级标题不能改:导入侧靠它切轮次与嗅探格式。
198
+ */
199
+ export function serializeTranscript(turns, format) {
200
+ const list = Array.isArray(turns) ? turns : [];
201
+ if (format === 'jsonl') {
202
+ // content 保持**字符串**:改成类型化块数组的话,导入侧的 extractText 会把 reasoning
203
+ // 又拼回正文(正是我们在修的那个泄漏),并让 tool-call 无声消失 —— 严格劣于字符串。
204
+ return list
205
+ .map((turn) => {
206
+ let content = turn.text;
207
+ for (const item of turn.details)
208
+ content += `${content ? '\n\n' : ''}[${item.label}]\n${item.text}`;
209
+ return JSON.stringify({ type: turn.role, message: { role: turn.role, content } });
210
+ })
211
+ .join('\n');
212
+ }
213
+ return list
214
+ .map((turn) => {
215
+ const head = turn.role === 'user' ? '## User' : '### Assistant';
216
+ const blocks = [turn.text ? `${head}\n${turn.text}` : head];
217
+ // 四级标题:`#{1,3}` 匹配不到,所以附加段再导入时只会并入所属轮次,不会伪装成边界。
218
+ for (const item of turn.details)
219
+ blocks.push(`#### ${item.label}\n${indentBlock(item.text)}`);
220
+ return blocks.join('\n\n');
221
+ })
222
+ .join('\n\n');
223
+ }
package/lib/index.js CHANGED
@@ -70,41 +70,13 @@ import { buildMemoryTools } from './tools/memory.js';
70
70
  import { buildSceneTools } from './tools/scene.js';
71
71
  import { buildSubagentTools } from './tools/subagent.js';
72
72
  import { createScenePromptSync } from './scene-prompt-sync.js';
73
- import { extractText } from './imports/parsers.js';
73
+ import { extractExportTurns, serializeTranscript } from './exports/transcript.js';
74
74
  import { spawn } from 'node:child_process';
75
75
  import { mkdir, readdir, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
76
76
  import { basename, dirname, join } from 'node:path';
77
77
  // ---------------------------------------------------------------------------
78
- // Transcript export helpers — mirror the import parsers (src/imports/parsers.js)
79
- // so an exported file can be imported back losslessly. Pure Node, no ctx.
78
+ // Transcript export lives in src/exports/transcript.ts (0.18.5).
80
79
  // ---------------------------------------------------------------------------
81
- /** 从会话事件数组提取用户/助手纯文本轮次(与导入解析器对称)。 */
82
- function extractTurnsFromEvents(events) {
83
- const turns = [];
84
- for (const ev of events || []) {
85
- if (!ev || typeof ev !== 'object')
86
- continue;
87
- const e = ev;
88
- let content;
89
- if (e.type === 'user/message')
90
- content = e.data && e.data.content;
91
- else if (e.type === 'assistant/message')
92
- content = e.data && e.data.message && e.data.message.content;
93
- else
94
- continue;
95
- const text = extractText(content).trim();
96
- if (text)
97
- turns.push({ role: e.type === 'user/message' ? 'user' : 'assistant', text });
98
- }
99
- return turns;
100
- }
101
- /** 序列化为可再导入的转录文本:Codex 风格 Markdown 或 Claude Code 风格 JSONL。 */
102
- function serializeTurns(turns, format) {
103
- if (format === 'jsonl') {
104
- return turns.map((t) => JSON.stringify({ type: t.role, message: { role: t.role, content: t.text } })).join('\n');
105
- }
106
- return turns.map((t) => (t.role === 'user' ? '## User\n' : '### Assistant\n') + t.text).join('\n\n');
107
- }
108
80
  /**
109
81
  * `inject` 的服务名清单(单一来源):既声明给宿主,也是「挂载心跳」与 doctor 核对的内容。
110
82
  *
@@ -1534,8 +1506,8 @@ export default {
1534
1506
  quickPromptsDir,
1535
1507
  rulesList: (args) => memoriesService.ops['rules-list'](args),
1536
1508
  skillDetail: (args) => skillsService.ops['skill-detail'](args),
1537
- extractTurnsFromEvents,
1538
- serializeTurns,
1509
+ extractTurnsFromEvents: extractExportTurns,
1510
+ serializeTurns: serializeTranscript,
1539
1511
  }),
1540
1512
  // rules 域里要走「场景 ↔ 全局基线同步」包装的四个写 op(ops/scene-sync.ts 提供)。
1541
1513
  // 必须排在 ...memoriesService.ops 之后 —— 这里是显式覆盖同名 op,不是新增。
@@ -205,9 +205,13 @@ export function buildSessionOps(deps) {
205
205
  if (!sessionId)
206
206
  return { ok: false, error: '缺少 sessionId' };
207
207
  try {
208
- const r = await registry.unarchiveSession(sessionId);
208
+ // 不接 registry 的返回值:官方 `unarchiveSession` 是 `Promise<void>`,而界面只要
209
+ // ok / error / workspace —— 每次单条恢复都把整个归档集合回传一遍纯属浪费。
210
+ // 0.18.5 之前这里读 `r.archivedSessionIds`,于是宿主恢复**成功之后**构造回执时抛
211
+ // TypeError,界面弹「操作失败」,用户以为没恢复、其实已经恢复了。
212
+ await registry.unarchiveSession(sessionId);
209
213
  const workspace = await deps.restoreWorkspaceAccounting([sessionId]);
210
- return { ok: true, archivedSessionIds: r.archivedSessionIds, ...(workspace ? { workspace } : {}) };
214
+ return { ok: true, sessionId, ...(workspace ? { workspace } : {}) };
211
215
  }
212
216
  catch (e) {
213
217
  return { ok: false, error: deps.message(e) };
@@ -278,7 +282,8 @@ export function buildSessionOps(deps) {
278
282
  try {
279
283
  const r = await registry.unarchiveSessions(parsed.target);
280
284
  const workspace = await deps.restoreWorkspaceAccounting(r.unarchivedSessionIds || []);
281
- return { ok: true, unarchivedSessionIds: r.unarchivedSessionIds, archivedSessionIds: r.archivedSessionIds, ...(workspace ? { workspace } : {}) };
285
+ // 只报「这次恢复了哪些」,不把整个归档集合快照发回界面(同上,界面不读它)。
286
+ return { ok: true, unarchivedSessionIds: r.unarchivedSessionIds, ...(workspace ? { workspace } : {}) };
282
287
  }
283
288
  catch (e) {
284
289
  return { ok: false, error: deps.message(e) };
@@ -564,6 +569,8 @@ export function buildSessionOps(deps) {
564
569
  },
565
570
  // 把选中的归档会话导出为可再导入的转录文件(Markdown / JSONL),写入指定目录。
566
571
  // 每个会话一个文件;读不到正文的冷会话列入 skipped,不中断其余导出。
572
+ // `detail` 三档**累进**:`simple`(默认)只有正文;`tools` 另加工具调用与结果、附件名;
573
+ // `full` 再加思考。
567
574
  'history-export': async (args) => {
568
575
  const rawIds = (args && args.sessionIds) || [];
569
576
  const ids = Array.isArray(rawIds) ? rawIds.map((s) => String(s).trim()).filter(Boolean) : [];
@@ -572,6 +579,11 @@ export function buildSessionOps(deps) {
572
579
  const format = String((args && args.format) || 'markdown').toLowerCase();
573
580
  if (format !== 'markdown' && format !== 'jsonl')
574
581
  return { ok: false, error: 'format 需为 markdown 或 jsonl' };
582
+ // 收到不认识的档位就出声:静默降级会让用户以为导出的是带工具的版本,而它其实只有正文。
583
+ const rawDetail = String((args && args.detail) || 'simple').toLowerCase();
584
+ if (rawDetail !== 'simple' && rawDetail !== 'tools' && rawDetail !== 'full')
585
+ return { ok: false, error: 'detail 需为 simple / tools / full' };
586
+ const detail = rawDetail;
575
587
  const outDir = String((args && args.outDir) || '').trim();
576
588
  if (!/^([A-Za-z]:[\\/]|\\\\|\/)/.test(outDir))
577
589
  return { ok: false, error: '导出目录需为绝对路径' };
@@ -627,9 +639,13 @@ export function buildSessionOps(deps) {
627
639
  const path = join(outDir, fileName);
628
640
  try {
629
641
  const events = await readEvents(sessionId);
630
- const turns = deps.extractTurnsFromEvents(events || []);
642
+ const turns = deps.extractTurnsFromEvents(events || [], detail);
631
643
  if (!turns.length) {
632
- skipped.push({ sessionId, error: events ? '会话中没有可导出的消息' : '无法读取会话内容(不在活动存储且无持久化句柄)' });
644
+ // 简洁档现在**不含思考与工具**(0.18.5 之前思考被混在正文里),所以"只有思考没正文"
645
+ // 的会话会第一次变成导不出来 —— 得说清楚是档位问题,不是内容丢了。
646
+ skipped.push({ sessionId, error: !events ? '无法读取会话内容(不在活动存储且无持久化句柄)'
647
+ : detail !== 'full' ? '会话中没有可导出的正文(思考与工具调用不计入本档 —— 换更高一档可以导出它们)'
648
+ : '会话中没有可导出的消息' });
633
649
  continue;
634
650
  }
635
651
  // `'wx'`:绝不覆盖已存在的同名文件 —— 覆盖是静默的,用户会以为导出成功
@@ -4,7 +4,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { hubPath } from "../hub.js";
5
5
  import { sessionDir } from "@deepseek-ai/dsh-spill-local";
6
6
  import { trackTombstone } from "./tombstone.js";
7
- import { archivedBatchTargetSchema, workspaceIdSchema, } from "./archive-schemas.js";
7
+ import { archivedBatchTargetSchema, archivedSetSchema, workspaceIdSchema, } from "./archive-schemas.js";
8
8
  import { workspaceBaseName, workspacePathKey } from "./path-key.js";
9
9
  import { noteRuntime } from "../compat/runtime-notes.js";
10
10
  /**
@@ -242,6 +242,23 @@ var ArchiveWorkspaceRegistry = class {
242
242
  if (changed)
243
243
  await this.saveArchivedAt();
244
244
  }
245
+ /**
246
+ * 以宿主权威归档集合构造恢复回执。
247
+ *
248
+ * 官方 `unarchiveSession` 只回 `Promise<void>`,**宿主给什么一律不认**,委托与自持两条
249
+ * 分支共用这一个形状。读公开 getter 而不是 `requireState()`:后者经 `bridge.state()`
250
+ * 落到 `method("requireState")`,宿主缺这个私有成员时会直接抛(bridge.ts:331)——
251
+ * 恢复已经成功了,不该被一份回执拖成失败。
252
+ */
253
+ archivedSetResult() {
254
+ const raw = this.archivedSessionIds;
255
+ if (!Array.isArray(raw)) {
256
+ this.ctx.logger.warn("archive-manager: registry.archivedSessionIds is not an array; reporting an empty archive set");
257
+ return { archivedSessionIds: [] };
258
+ }
259
+ // 集合由我们自己拼,schema 在这里是后置断言:真触发说明 getter 的声明与运行时不符。
260
+ return archivedSetSchema.parse({ archivedSessionIds: [...new Set(raw.map((id) => String(id)))] });
261
+ }
245
262
  async ensureArchivedAtLoaded() {
246
263
  await this.ready;
247
264
  if (this.archivedAtLoaded)
@@ -689,9 +706,10 @@ var ArchiveWorkspaceRegistry = class {
689
706
  async unarchiveSession(sessionId) {
690
707
  this.bridge.checkWorkspace("unarchive");
691
708
  if (typeof this.registry.unarchiveSession === "function") {
692
- const result = await this.registry.unarchiveSession(sessionId);
709
+ await this.registry.unarchiveSession(sessionId);
693
710
  await this.reconcileArchiveLedger();
694
- return result;
711
+ // 官方只回 Promise<void>,返回值不认;回执按权威集合自造(见 archivedSetResult)。
712
+ return this.archivedSetResult();
695
713
  }
696
714
  return this.enqueueOperation(async () => {
697
715
  if (!(await this.sessionKnown(sessionId)))
@@ -757,6 +775,9 @@ var ArchiveWorkspaceRegistry = class {
757
775
  * 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
758
776
  */
759
777
  async unarchiveSessions(target) {
778
+ // ⚠️ 这一支今天**跑不到**:宿主没有 `unarchiveSessions`(dsh-workspace 全库零命中),
779
+ // 所以无从测试。它保留了「直接认宿主返回值」这个和 unarchiveSession 同型的过度信任 ——
780
+ // 官方哪天补上这个方法时,先照 unarchiveSession 的做法改成自造回执,别照抄这一行。
760
781
  if (typeof this.registry.unarchiveSessions === "function") {
761
782
  const result = await this.registry.unarchiveSessions(target);
762
783
  await this.reconcileArchiveLedger();
@@ -766,7 +787,7 @@ var ArchiveWorkspaceRegistry = class {
766
787
  const ids = this.archivedSessionIdsForTarget(target);
767
788
  for (const id of ids)
768
789
  await this.unarchiveSession(id);
769
- return { unarchivedSessionIds: ids, archivedSessionIds: [...this.registry.archivedSessionIds] };
790
+ return { unarchivedSessionIds: ids, ...this.archivedSetResult() };
770
791
  }
771
792
  return this.enqueueOperation(async () => {
772
793
  const unarchivedSessionIds = this.archivedSessionIdsForTarget(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tool-management",
3
- "version": "0.18.0",
3
+ "version": "0.18.5",
4
4
  "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagent personas and AGENTS.md presets — a scene switches the whole environment, and the model can drive it from chat too.",
5
5
  "repository": {
6
6
  "type": "git",