dsh-more 0.0.1 → 0.0.2

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/CONTRIBUTING.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  开发环境要求:
12
12
 
13
- - Node.js `>= 22.19`;
13
+ - Node.js `22.19.x` 或 `>= 24`;
14
14
  - pnpm;
15
15
  - 与项目当前依赖匹配的 DSH。本版本面向 DSH `0.1.0-rc.7`。
16
16
 
@@ -220,9 +220,9 @@ Standalone 规则:
220
220
  2. Host 路由继续经过 loopback/trusted-host、same-origin 和自定义请求头检查;
221
221
  3. 预览与提交分离,提交时重新校验会话 revision、目标和确认令牌;
222
222
  4. 忙碌会话应拒绝操作,或先完成明确的 cancel、idle、flush、unload 流程;
223
- 5. 删除路径必须从 DSH 返回的逐对象定位信息推导,并验证仍位于受控根目录;
223
+ 5. 删除路径必须从 DSH 返回的逐对象定位信息推导,只接受已知的逐会话 artifact 布局,不把本机路径返回 Client;
224
224
  6. 禁止对宽泛目录、未解析变量或 glob 执行递归删除;
225
- 7. 错误时保持原数据,不要用“尽量成功”的部分写入掩盖失败。
225
+ 7. 多步写入必须把可撤销副作用记录下来,并在后续步骤失败时执行补偿;不要用“尽量成功”的部分写入掩盖失败。
226
226
 
227
227
  以下内容属于持久化协议,不应在普通重构中随意改名:
228
228
 
@@ -283,7 +283,7 @@ pnpm run package:check
283
283
 
284
284
  - 当前分支已经设置 upstream,并与远端完全同步;
285
285
  - 工作区没有未提交文件;
286
- - 已执行 `npm login --registry=https://registry.npmjs.org/`,且当前 npm 用户有权发布 `dsh-more`;
286
+ - 正式发布前已执行 `npm login --registry=https://registry.npmjs.org/`,且当前 npm 用户有权发布 `dsh-more`;dry-run 不要求 npm 登录;
287
287
  - Git 远端允许推送当前分支和 `v<version>` tag。
288
288
 
289
289
  先执行不产生任何版本、tag 或发布记录的预检:
@@ -301,7 +301,7 @@ pnpm release:minor # 0.0.1 -> 0.1.0
301
301
  pnpm release:major # 0.0.1 -> 1.0.0
302
302
  ```
303
303
 
304
- 脚本会依次检查 Git/npm 状态,运行完整项目检查,自动修改 `package.json` 版本并创建 `chore(release): v<version>` 提交和同名 tag,发布到公共 npm registry,最后以一次原子 push 同时推送版本提交和 tag。首次发布时,若 npm 上还不存在这个包,`pnpm release` 会直接发布 `package.json` 中的当前版本并为当前提交创建 tag。
304
+ 脚本会依次检查 Git/npm 状态,运行完整项目检查,自动修改 `package.json` 版本并通过 npm `version` 生命周期同步 `dsh.plugin.json`,随后创建 `chore(release): v<version>` 提交和同名 tag,发布到公共 npm registry,最后以一次原子 push 同时推送版本提交和 tag。首次发布时,若 npm 上还不存在这个包,`pnpm release` 会直接发布 `package.json` 中的当前版本并为当前提交创建 tag。
305
305
 
306
306
  如果 npm 账户要求一次性验证码,可以使用:
307
307
 
@@ -319,7 +319,7 @@ pnpm run release -- --otp=123456
319
319
  - 运行时由 DSH 提供的外部包放入 `peerDependencies`,并在 `devDependencies` 固定本地开发版本;
320
320
  - Client bundle 真正产生外部 `require()` 时,将对应模块加入 `dsh.client.inject`;
321
321
  - 只用于 TypeScript module augmentation、且不会出现在发布声明或运行时 bundle 中的包,可以仅作为开发依赖;
322
- - 同步 `pnpm-lock.yaml` 和 `package-lock.json`;
322
+ - 同步 `pnpm-lock.yaml`;本仓库不维护并行的 `package-lock.json`;
323
323
  - 不要为了一个很小的 helper 引入大型依赖。
324
324
 
325
325
  更新锁文件后再次运行完整检查,并在变更说明中写明依赖的用途。
package/README.md CHANGED
@@ -6,7 +6,7 @@ Practical, independently switchable patches for missing context and history cont
6
6
 
7
7
  `dsh-more` integrates with the existing DSH interface instead of adding a separate management dashboard. Its current patch set can edit a user message and restart from that point, remove one message while preserving the surrounding context, and permanently delete a session while leaving native archive behavior intact.
8
8
 
9
- > **Compatibility:** package version `0.0.1` targets DeepSeek Harness `0.1.0-rc.7` and requires Node.js `>= 22.19`. DSH is still a release candidate, so upgrades may require changes in the centralized adapter layer described below.
9
+ > **Compatibility:** the current release targets DeepSeek Harness `0.1.0-rc.7` and requires Node.js `>= 24`. DSH is still a release candidate, so upgrades may require changes in the centralized adapter layer described below.
10
10
 
11
11
  ## Features
12
12
 
@@ -22,7 +22,7 @@ All three switches live under **Settings → Plugins → Plugin configuration
22
22
 
23
23
  ### Requirements
24
24
 
25
- - Node.js `>= 22.19`
25
+ - Node.js `>= 24`
26
26
  - A working `dsh` CLI and a Web profile compatible with DSH `0.1.0-rc.7`
27
27
  - Access to the npm registry that publishes `dsh-more`
28
28
 
@@ -105,9 +105,7 @@ Restart `dsh web` once more. Removing the package removes its UI and runtime pat
105
105
  ### Edit a user message and restart
106
106
 
107
107
  1. Hover over a user message and choose **Edit and restart from here**.
108
- 2. Change the text and select **Preview changes**.
109
- 3. Review how many turns will be discarded.
110
- 4. Confirm the edit.
108
+ 2. Change the text and select **Edit and restart**. The change is applied directly without a second preview-and-confirm step.
111
109
 
112
110
  The plugin cuts the durable log immediately before the selected message's owning turn, creates a child session with the same workspace, live preset composition, provider/model selection, and token limit, then sends the edited message as the next user input. The source session is archived, not physically deleted.
113
111
 
@@ -123,7 +121,7 @@ Deletion is implemented as context reconstruction, not as a tombstone placed in
123
121
 
124
122
  Only the selected node is removed unless a tool call/result range must remain atomic. Editing is the operation that intentionally discards the selected turn and everything after it.
125
123
 
126
- Both continuation operations preallocate their destination during preview and watch DSH's incremental `session-added` event while committing. As soon as the child appears, the client hands off directly instead of refreshing the list, passing through a blank view, and then reopening. A full refresh remains only as a compatibility fallback when the incremental event is missing.
124
+ Editing performs its state check and preallocates the continuation inside the single submission; message deletion still shows a preview before confirmation. Both operations watch DSH's incremental `session-added` event while committing. As soon as the child appears, the client hands off directly instead of refreshing the list, passing through a blank view, and then reopening. A full refresh remains only as a compatibility fallback when the incremental event is missing.
127
125
 
128
126
  ### Permanently delete a session
129
127
 
@@ -166,14 +164,16 @@ The generator rejects missing entries, invalid or mismatched IDs, duplicate orde
166
164
 
167
165
  The generic protocol is stable even as patches are added:
168
166
 
169
- - mutation routes accept `POST` only, cap JSON bodies at 64 KiB, and require `x-dsh-more: 1`;
167
+ - mutation routes accept `POST` with `application/json` only, cap bodies at 64 KiB, and require `x-dsh-more: 1`;
170
168
  - the request authority must be loopback or an explicitly trusted host, cross-site requests are rejected, and a supplied `Origin` must match;
171
169
  - disabled patches are rejected at the Host router even if an old Client still sends a request;
172
170
  - all wire payloads start as `unknown` and are validated before use;
173
- - message previews receive five-minute HMAC confirmation tokens bound to the session, log revision, surface generation, selected nodes, operation, target, preallocated continuation session, and edited-content digest;
171
+ - message-operation state checks receive five-minute HMAC confirmation tokens bound to the session, log revision, surface generation, selected nodes, operation, target, preallocated continuation session, and edited-content digest; editing performs the check and commit within one click, while message deletion retains its visible preview confirmation;
174
172
  - commits re-read current state, reject stale previews, and require an idle Agent maintenance window;
173
+ - failed continuation publication detaches the preallocated child and disposes its Agent handle before returning the error;
174
+ - unexpected Host failures are logged with diagnostics while the browser receives a generic internal-error response instead of local paths or stack details;
175
175
  - Host setup and Client DOM effects are independently disposable when a patch is disabled;
176
- - physical deletion accepts only an absolute per-session `jsonl` location, verifies the derived directory stays inside the persistence root, unloads the live session, then deletes that exact directory.
176
+ - physical deletion accepts only an absolute per-session `jsonl` location with DSH's fixed transcript filename, unloads the live session, then deletes that exact session-owned directory; local paths are not returned to the Client.
177
177
 
178
178
  ## Design principles
179
179
 
@@ -203,6 +203,7 @@ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before adding a patch. It documents th
203
203
 
204
204
  ```sh
205
205
  pnpm run generate # Validate manifests and rebuild src/generated/
206
+ pnpm run peers:check # Verify the locked peer-dependency graph
206
207
  pnpm run typecheck # Generate, then run strict TypeScript checks
207
208
  pnpm test # Generate, then run the Vitest suite
208
209
  pnpm run build # Generate declarations plus Host and Client bundles
package/README.zh-CN.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  `dsh-more` 不创建平行的“管理中心”,而是把能力直接嵌入 DSH 现有界面。当前补丁可以编辑用户消息并从该处重新开始、在保留前后上下文的前提下删除单条消息,以及在不改变原生归档语义的情况下永久删除会话。
8
8
 
9
- > **兼容性:** 当前包版本为 `0.0.1`,面向 DeepSeek Harness `0.1.0-rc.7`,要求 Node.js `>= 22.19`。DSH 仍是 RC 版本,升级后可能需要调整下文所述的集中适配层。
9
+ > **兼容性:** 当前发布面向 DeepSeek Harness `0.1.0-rc.7`,要求 Node.js `>= 24`。DSH 仍是 RC 版本,升级后可能需要调整下文所述的集中适配层。
10
10
 
11
11
  ## 功能概览
12
12
 
@@ -22,7 +22,7 @@
22
22
 
23
23
  ### 环境要求
24
24
 
25
- - Node.js `>= 22.19`
25
+ - Node.js `>= 24`
26
26
  - 可正常使用的 `dsh` CLI,以及与 DSH `0.1.0-rc.7` 兼容的 Web profile
27
27
  - 能访问发布 `dsh-more` 的 npm registry
28
28
 
@@ -105,9 +105,7 @@ dsh plugin --profile web remove dsh-more
105
105
  ### 编辑用户消息并从这里重新开始
106
106
 
107
107
  1. 把鼠标移到用户消息上,选择 **编辑并从这里重新开始**。
108
- 2. 修改文本并点击 **预览修改**。
109
- 3. 检查将丢弃的轮次数。
110
- 4. 确认编辑。
108
+ 2. 修改文本并点击 **修改并重新开始**,修改会直接生效,不再显示二次预览确认。
111
109
 
112
110
  插件会在所选消息所属轮次之前切断持久日志,创建继承相同 Workspace、源 Agent 当前 Preset composition、provider/model 和 token limit 的子会话,再把编辑后的消息作为下一条用户输入。源会话只会归档,不会物理删除。
113
111
 
@@ -123,7 +121,7 @@ dsh plugin --profile web remove dsh-more
123
121
 
124
122
  除非工具调用与结果必须保持原子性,否则只删除用户点选的节点。会主动丢弃所选轮次及其后内容的是“编辑并重新开始”。
125
123
 
126
- 编辑和消息删除都会在预览时预分配延续会话,并在提交期间监听 DSH 的 `session-added` 增量事件;新会话一出现就直接完成交接,不再先刷新列表、短暂进入空白页再打开新会话。全量刷新只保留为增量事件缺失时的兼容兜底。
124
+ 编辑会在单次提交内部完成状态检查并预分配延续会话;消息删除仍在确认前显示预览。两者都会在提交期间监听 DSH 的 `session-added` 增量事件;新会话一出现就直接完成交接,不再先刷新列表、短暂进入空白页再打开新会话。全量刷新只保留为增量事件缺失时的兼容兜底。
127
125
 
128
126
  ### 永久删除会话
129
127
 
@@ -166,14 +164,16 @@ dsh plugin --profile web remove dsh-more
166
164
 
167
165
  这套通用协议不会随补丁数量变化:
168
166
 
169
- - 写接口只接受 `POST`,JSON body 上限为 64 KiB,并要求 `x-dsh-more: 1`;
167
+ - 写接口只接受 `POST` 与 `application/json`,body 上限为 64 KiB,并要求 `x-dsh-more: 1`;
170
168
  - 请求 authority 必须是 loopback 或显式 trusted host;拒绝 cross-site;存在 `Origin` 时必须与 Host 匹配;
171
169
  - 即使旧 Client 继续发请求,Host router 也会拒绝已关闭补丁;
172
170
  - 所有线协议 payload 都从不可信 `unknown` 开始,先校验再使用;
173
- - 消息操作的预览会获得五分钟 HMAC 确认令牌,绑定 Session、日志 revision、surface generation、选中节点、操作类型、目标、预分配的延续会话与编辑内容摘要;
171
+ - 消息操作的状态检查会获得五分钟 HMAC 确认令牌,绑定 Session、日志 revision、surface generation、选中节点、操作类型、目标、预分配的延续会话与编辑内容摘要;编辑在一次点击内完成检查与提交,消息删除则保留用户可见的预览确认;
174
172
  - 提交阶段重新读取当前状态,过期或失配的预览会被拒绝,并要求 Agent 处于 idle maintenance window;
173
+ - 续接发布失败时,会先从 Workspace 移除预分配子会话并释放其 Agent handle,再返回错误;
174
+ - 未预期的 Host 异常保留在服务端日志中,浏览器只收到通用内部错误,不暴露本机路径或堆栈细节;
175
175
  - 关闭补丁时,Host setup 和 Client DOM 副作用都可以独立清理;
176
- - 物理删除只接受绝对的逐会话 `jsonl` 定位,验证推导目录仍位于持久化根目录内,卸载实时会话后只删除该精确目录。
176
+ - 物理删除只接受绝对的逐会话 `jsonl` 定位与 DSH 固定 transcript 文件名,卸载实时会话后只删除该会话自有目录;本机路径不会返回 Client。
177
177
 
178
178
  ## 设计思想
179
179
 
@@ -203,6 +203,7 @@ build.mjs # Host ESM、Client bundle、声明和 inject 校验
203
203
 
204
204
  ```sh
205
205
  pnpm run generate # 校验清单并重建 src/generated/
206
+ pnpm run peers:check # 校验锁定依赖的 peer 契约
206
207
  pnpm run typecheck # 生成后执行严格 TypeScript 检查
207
208
  pnpm test # 生成后执行 Vitest 测试
208
209
  pnpm run build # 生成声明、Host 与 Client bundle
package/dist/client.js CHANGED
@@ -25,19 +25,21 @@ __export(index_exports, {
25
25
  inject: () => inject
26
26
  });
27
27
  module.exports = __toCommonJS(index_exports);
28
- var import_react6 = require("react");
28
+ var import_react7 = require("react");
29
29
  var import_react_dom = require("react-dom");
30
30
 
31
31
  // src/generated/patch-catalog.ts
32
32
  var PATCH_CATALOG = [
33
33
  { "id": "message-edit", "name": "\u7F16\u8F91\u5E76\u91CD\u65B0\u5F00\u59CB", "description": "\u7F16\u8F91\u7528\u6237\u6D88\u606F\u5E76\u4E22\u5F03\u8BE5\u6D88\u606F\u4E4B\u540E\u7684\u5386\u53F2", "defaultEnabled": true, "order": 100, "clientKind": "message-action" },
34
34
  { "id": "message-delete", "name": "\u5355\u6761\u6D88\u606F\u5220\u9664", "description": "\u5220\u9664\u6307\u5B9A\u6D88\u606F\u5E76\u4FDD\u7559\u524D\u540E\u5386\u53F2\u8BB0\u5F55", "defaultEnabled": true, "order": 200, "clientKind": "message-action" },
35
- { "id": "session-delete", "name": "\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD", "description": "\u505C\u6B62\u76EE\u6807\u4F1A\u8BDD\u5E76\u4ECE\u78C1\u76D8\u6C38\u4E45\u5220\u9664\uFF0C\u4E0D\u5F71\u54CD\u539F\u751F\u5F52\u6863", "defaultEnabled": true, "order": 300, "clientKind": "standalone" }
35
+ { "id": "session-delete", "name": "\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD", "description": "\u505C\u6B62\u76EE\u6807\u4F1A\u8BDD\u5E76\u4ECE\u78C1\u76D8\u6C38\u4E45\u5220\u9664\uFF0C\u4E0D\u5F71\u54CD\u539F\u751F\u5F52\u6863", "defaultEnabled": true, "order": 300, "clientKind": "standalone" },
36
+ { "id": "conversation-markdown-export", "name": "\u5BFC\u51FA\u5B8C\u6574\u804A\u5929\u8BB0\u5F55", "description": "\u5C06\u5F53\u524D\u4F1A\u8BDD\u7684\u5B8C\u6574\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u4E3A Markdown \u6587\u4EF6", "defaultEnabled": true, "order": 400, "clientKind": "standalone" }
36
37
  ];
37
38
  var DEFAULT_PATCH_SETTINGS = {
38
39
  "message-edit": true,
39
40
  "message-delete": true,
40
- "session-delete": true
41
+ "session-delete": true,
42
+ "conversation-markdown-export": true
41
43
  };
42
44
  function decodePatchSettings(value) {
43
45
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -68,6 +70,7 @@ var DshMoreApiError = class extends Error {
68
70
  this.code = code;
69
71
  this.name = "DshMoreApiError";
70
72
  }
73
+ code;
71
74
  };
72
75
  async function callPatchApi(patchId, action, payload) {
73
76
  let response;
@@ -191,41 +194,28 @@ var api = {
191
194
  function useMessageActions(props, enabled) {
192
195
  const [target, setTarget] = (0, import_react.useState)(null);
193
196
  const [text, setText] = (0, import_react.useState)("");
194
- const [preview, setPreview] = (0, import_react.useState)(null);
195
197
  const [busy, setBusy] = (0, import_react.useState)(false);
196
198
  const [error, setError] = (0, import_react.useState)(null);
197
199
  (0, import_react.useEffect)(() => {
198
200
  if (enabled) return;
199
201
  setTarget(null);
200
- setPreview(null);
201
202
  setError(null);
202
203
  }, [enabled]);
203
204
  const open = (next) => {
204
205
  setTarget(next);
205
206
  setText(next.text);
206
- setPreview(null);
207
207
  setError(null);
208
208
  };
209
- const previewChange = async () => {
209
+ const applyChange = async () => {
210
210
  if (target === null) return;
211
211
  setBusy(true);
212
212
  setError(null);
213
- try {
214
- setPreview(await api.preview({ sessionId: props.sessionId, targetSeq: target.seq, text }));
215
- } catch (caught) {
216
- setError(apiErrorText(caught));
217
- } finally {
218
- setBusy(false);
219
- }
220
- };
221
- const commit = async () => {
222
- if (target === null || preview === null) return;
223
- setBusy(true);
224
- setError(null);
225
213
  const sessions = props.ctx.sessions;
226
- const continuationSessionId = preview.continuationSessionId;
227
- const handoff = followSessionHandoff(sessions, continuationSessionId);
214
+ let handoff;
228
215
  try {
216
+ const preview = await api.preview({ sessionId: props.sessionId, targetSeq: target.seq, text });
217
+ const continuationSessionId = preview.continuationSessionId;
218
+ handoff = followSessionHandoff(sessions, continuationSessionId);
229
219
  const result = await api.commit({
230
220
  sessionId: props.sessionId,
231
221
  targetSeq: target.seq,
@@ -236,13 +226,12 @@ function useMessageActions(props, enabled) {
236
226
  await handoff.finish(result.sessionId);
237
227
  setTarget(null);
238
228
  } catch (caught) {
239
- handoff.cancel();
229
+ handoff?.cancel();
240
230
  const sourceId = props.sessionId;
241
231
  const sessionList = sessions.list.getSnapshot();
242
232
  const sourceAvailable = sessionList.byId[sourceId] !== void 0;
243
233
  const sourceArchived = props.ctx.workspaces.list.getSnapshot().archivedSessionIds.includes(sourceId);
244
234
  if (sourceAvailable && !sourceArchived && sessionList.current !== sourceId) openSessionWithoutGap(sessions, sourceId);
245
- setPreview(null);
246
235
  setError(apiErrorText(caught));
247
236
  } finally {
248
237
  setBusy(false);
@@ -262,12 +251,9 @@ function useMessageActions(props, enabled) {
262
251
  closeLabel: "\u5173\u95ED",
263
252
  className: "dshmore-edit-modal",
264
253
  description: "\u4FEE\u6539\u540E\u4F1A\u521B\u5EFA\u5E72\u51C0\u7684\u524D\u7F00\u4F1A\u8BDD\uFF0C\u8FD9\u6761\u6D88\u606F\u4E4B\u540E\u7684\u5185\u5BB9\u4E0D\u4F1A\u4FDD\u7559\u3002",
265
- footer: preview === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
254
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
266
255
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "ghost", disabled: busy, onClick: () => setTarget(null), children: "\u53D6\u6D88" }),
267
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: busy || text.trim() === "", onClick: () => void previewChange(), children: busy ? "\u6B63\u5728\u68C0\u67E5\u2026" : "\u9884\u89C8\u4FEE\u6539" })
268
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
269
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "ghost", disabled: busy, onClick: () => setPreview(null), children: "\u8FD4\u56DE\u4FEE\u6539" }),
270
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: busy, onClick: () => void commit(), children: busy ? "\u6B63\u5728\u91CD\u5EFA\u2026" : "\u786E\u8BA4\u7F16\u8F91\u5E76\u4E22\u5F03\u540E\u7EED" })
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: busy || text.trim() === "", onClick: () => void applyChange(), children: busy ? "\u6B63\u5728\u4FEE\u6539\u2026" : "\u4FEE\u6539\u5E76\u91CD\u65B0\u5F00\u59CB" })
271
257
  ] }),
272
258
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshmore-dialog-body", children: [
273
259
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -275,23 +261,15 @@ function useMessageActions(props, enabled) {
275
261
  {
276
262
  className: "dshmore-editor",
277
263
  value: text,
278
- disabled: busy || preview !== null,
264
+ disabled: busy,
279
265
  onChange: (event) => {
280
266
  setText(event.currentTarget.value);
281
- setPreview(null);
282
267
  setError(null);
283
268
  },
284
269
  rows: 7,
285
270
  autoFocus: true
286
271
  }
287
272
  ),
288
- preview !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshmore-warning", children: [
289
- "\u5C06\u4ECE\u7B2C ",
290
- preview.turn,
291
- " \u8F6E\u4E4B\u524D\u91CD\u5EFA\u4F1A\u8BDD\uFF0C\u5E76\u4E22\u5F03\u5F53\u524D\u8F6E\u53CA\u4E4B\u540E\u5171 ",
292
- preview.laterTurnCount,
293
- " \u8F6E\u5185\u5BB9\u3002"
294
- ] }),
295
273
  error !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshmore-error", children: error })
296
274
  ] })
297
275
  }
@@ -403,6 +381,7 @@ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primi
403
381
 
404
382
  // src/platform/dsh/client/styles.ts
405
383
  var styles = `
384
+ [data-dshmore-hidden] { display: none !important; }
406
385
  .dshmore-inline-actions { display: inline-flex; align-items: center; gap: 10px; }
407
386
  .dshmore-inline-button { width: 28px; height: 28px; color: var(--dsw-alias-label-tertiary); cursor: pointer; background: transparent; border: 0; border-radius: 28px; display: inline-flex; align-items: center; justify-content: center; padding: 6px; }
408
387
  .dshmore-inline-button:hover { color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-interactive-bg-hover); }
@@ -460,18 +439,24 @@ function replaceExactText(root, before, after) {
460
439
  }
461
440
  function installSessionDeleteMenuItems(onDelete) {
462
441
  let frame = null;
442
+ const deleteItems = /* @__PURE__ */ new WeakMap();
463
443
  const sync = () => {
464
444
  for (const archiveButton of document.querySelectorAll('button[role="menuitem"]')) {
465
445
  const text = archiveButton.textContent?.trim();
466
446
  const deleteLabel = text === "\u5F52\u6863\u4F1A\u8BDD" ? "\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD" : text === "Archive session" ? "Permanently delete session" : void 0;
467
- if (deleteLabel === void 0 || archiveButton.hasAttribute(DELETE_SOURCE_ATTRIBUTE)) continue;
447
+ if (deleteLabel === void 0) continue;
468
448
  const wrapper = archiveButton.parentElement;
469
449
  if (wrapper === null) continue;
450
+ if (archiveButton.hasAttribute(DELETE_SOURCE_ATTRIBUTE)) {
451
+ if (deleteItems.get(archiveButton)?.isConnected === true) continue;
452
+ archiveButton.removeAttribute(DELETE_SOURCE_ATTRIBUTE);
453
+ }
470
454
  const clone = wrapper.cloneNode(true);
471
455
  const deleteButton = clone.querySelector('button[role="menuitem"]');
472
456
  if (deleteButton === null) continue;
473
457
  archiveButton.setAttribute(DELETE_SOURCE_ATTRIBUTE, "");
474
458
  clone.setAttribute(DELETE_MENU_ATTRIBUTE, "");
459
+ deleteItems.set(archiveButton, clone);
475
460
  replaceExactText(deleteButton, text, deleteLabel);
476
461
  deleteButton.setAttribute("aria-label", deleteLabel);
477
462
  deleteButton.classList.add("dshmore-session-delete-menu-item");
@@ -623,15 +608,131 @@ var clientPatch = {
623
608
  }
624
609
  };
625
610
 
611
+ // src/patches/conversation-markdown-export/client/index.tsx
612
+ var import_react4 = require("react");
613
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
614
+
615
+ // src/patches/conversation-markdown-export/shared.ts
616
+ var CONVERSATION_MARKDOWN_EXPORT_PATCH_ID = "conversation-markdown-export";
617
+
618
+ // src/patches/conversation-markdown-export/client/filename.ts
619
+ function safeStem(value) {
620
+ return [...value.normalize("NFKC").replace(/[\u0000-\u001f\u007f/\\?%*:|"<>]+/g, "-").replace(/\s+/g, " ").replace(/^[.\s-]+|[.\s-]+$/g, "")].slice(0, 80).join("").trim();
621
+ }
622
+ function markdownFilename(title, sessionId) {
623
+ const fallback = safeStem(`dsh-conversation-${sessionId}`) || "dsh-conversation";
624
+ return `${safeStem(title) || fallback}.md`;
625
+ }
626
+
627
+ // src/patches/conversation-markdown-export/client/index.tsx
628
+ var import_jsx_runtime4 = require("react/jsx-runtime");
629
+ var exportStyles = `
630
+ .dshmore-markdown-export-button { box-sizing: border-box; min-width: 128px; height: 32px; color: var(--dsw-alias-label-primary); cursor: pointer; background: transparent; border: 1px solid var(--dsw-alias-border-l2); border-radius: 18px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 12px; font-family: var(--dsw-font-family); font-size: 13px; line-height: 20px; }
631
+ .dshmore-markdown-export-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
632
+ .dshmore-markdown-export-button:disabled { color: var(--dsw-alias-label-dimmed); cursor: wait; }
633
+ .dshmore-markdown-export-button > span, .dshmore-markdown-export-button > svg { flex: none; }
634
+ `;
635
+ function saveMarkdown(filename, markdown) {
636
+ const url = URL.createObjectURL(new Blob(["\uFEFF", markdown], { type: "text/markdown;charset=utf-8" }));
637
+ const anchor = document.createElement("a");
638
+ anchor.href = url;
639
+ anchor.download = filename;
640
+ anchor.style.display = "none";
641
+ document.body.appendChild(anchor);
642
+ anchor.click();
643
+ anchor.remove();
644
+ window.setTimeout(() => {
645
+ URL.revokeObjectURL(url);
646
+ }, 0);
647
+ }
648
+ function ConversationMarkdownExportButton(props) {
649
+ const settings = (0, import_react4.useSyncExternalStore)(props.activation.subscribe, props.activation.getSnapshot);
650
+ const enabled = settings[CONVERSATION_MARKDOWN_EXPORT_PATCH_ID];
651
+ const title = props.useSessions((state) => state.byId[props.sessionId]?.displayTitle ?? String(props.sessionId));
652
+ const [busy, setBusy] = (0, import_react4.useState)(false);
653
+ const [error, setError] = (0, import_react4.useState)(null);
654
+ const generation = (0, import_react4.useRef)(0);
655
+ const activeSession = (0, import_react4.useRef)(null);
656
+ activeSession.current = enabled ? String(props.sessionId) : null;
657
+ (0, import_react4.useEffect)(() => {
658
+ generation.current += 1;
659
+ setBusy(false);
660
+ setError(null);
661
+ }, [enabled, props.sessionId]);
662
+ const download = async () => {
663
+ const requestSessionId = String(props.sessionId);
664
+ const requestGeneration = ++generation.current;
665
+ setBusy(true);
666
+ setError(null);
667
+ try {
668
+ const result = await callPatchApi(
669
+ CONVERSATION_MARKDOWN_EXPORT_PATCH_ID,
670
+ "render",
671
+ { sessionId: props.sessionId, title }
672
+ );
673
+ if (generation.current !== requestGeneration || activeSession.current !== requestSessionId) return;
674
+ saveMarkdown(markdownFilename(title, String(props.sessionId)), result.markdown);
675
+ } catch (caught) {
676
+ if (generation.current === requestGeneration && activeSession.current === requestSessionId) setError(apiErrorText(caught));
677
+ } finally {
678
+ if (generation.current === requestGeneration && activeSession.current === requestSessionId) setBusy(false);
679
+ }
680
+ };
681
+ if (!enabled) return null;
682
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
683
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: exportStyles }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
685
+ "button",
686
+ {
687
+ type: "button",
688
+ className: "dshmore-markdown-export-button",
689
+ disabled: busy,
690
+ "aria-busy": busy,
691
+ "aria-label": "\u5BFC\u51FA\u5B8C\u6574\u804A\u5929\u8BB0\u5F55\u4E3A Markdown",
692
+ onClick: () => {
693
+ void download();
694
+ },
695
+ children: [
696
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: busy ? "\u6B63\u5728\u5BFC\u51FA\u2026" : "\u5BFC\u51FA Markdown" }),
697
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_dsh_client_ui_primitives4.IconDownloadOutline16, { size: 12 })
698
+ ]
699
+ }
700
+ ),
701
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
702
+ import_dsh_client_ui_primitives4.Modal,
703
+ {
704
+ open: error !== null,
705
+ onClose: () => setError(null),
706
+ title: "\u65E0\u6CD5\u5BFC\u51FA\u804A\u5929\u8BB0\u5F55",
707
+ closeLabel: "\u5173\u95ED",
708
+ description: error ?? "",
709
+ footer: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_dsh_client_ui_primitives4.Button, { variant: "primary", onClick: () => setError(null), children: "\u5173\u95ED" })
710
+ }
711
+ )
712
+ ] });
713
+ }
714
+ var clientPatch2 = {
715
+ id: CONVERSATION_MARKDOWN_EXPORT_PATCH_ID,
716
+ install: (ctx, activation) => {
717
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
718
+ name: "conversation.session.header.utilities",
719
+ id: `${PLUGIN_NAME}-${CONVERSATION_MARKDOWN_EXPORT_PATCH_ID}`,
720
+ order: 100,
721
+ registrant: PLUGIN_NAME
722
+ }, (props) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ConversationMarkdownExportButton, { ...props, activation })));
723
+ }
724
+ };
725
+
626
726
  // src/generated/client-registry.ts
627
727
  var MESSAGE_ACTION_PATCHES = [{ id: "message-edit", order: 100, useActions: useMessageActions }, { id: "message-delete", order: 200, useActions: useMessageActions2 }];
628
- var CLIENT_PATCHES = [clientPatch];
728
+ var CLIENT_PATCHES = [clientPatch, clientPatch2];
629
729
 
630
730
  // src/kernel/client/activation.ts
631
731
  var ClientPatchActivation = class {
632
732
  constructor(scope) {
633
733
  this.scope = scope;
634
734
  }
735
+ scope;
635
736
  lastScope;
636
737
  lastValue = { ...DEFAULT_PATCH_SETTINGS };
637
738
  getSnapshot = () => {
@@ -647,19 +748,19 @@ var ClientPatchActivation = class {
647
748
  };
648
749
 
649
750
  // src/kernel/client/config-card.tsx
650
- var import_react4 = require("react");
651
- var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
652
- var import_jsx_runtime4 = require("react/jsx-runtime");
751
+ var import_react5 = require("react");
752
+ var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
753
+ var import_jsx_runtime5 = require("react/jsx-runtime");
653
754
  function PatchConfigCard({ activation, initiallyOpen = false }) {
654
- const scope = (0, import_react4.useSyncExternalStore)(
755
+ const scope = (0, import_react5.useSyncExternalStore)(
655
756
  activation.subscribe,
656
757
  activation.getSettingsSnapshot,
657
758
  activation.getSettingsSnapshot
658
759
  );
659
760
  const settings = activation.getSnapshot();
660
- const [open, setOpen] = (0, import_react4.useState)(initiallyOpen);
661
- const [busy, setBusy] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
662
- const [error, setError] = (0, import_react4.useState)(null);
761
+ const [open, setOpen] = (0, import_react5.useState)(initiallyOpen);
762
+ const [busy, setBusy] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
763
+ const [error, setError] = (0, import_react5.useState)(null);
663
764
  if (scope.status === "unavailable") return null;
664
765
  const update = async (id, enabled) => {
665
766
  setBusy((current) => /* @__PURE__ */ new Set([...current, id]));
@@ -676,9 +777,9 @@ function PatchConfigCard({ activation, initiallyOpen = false }) {
676
777
  });
677
778
  }
678
779
  };
679
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { className: `dshmore-config-card${open ? " dshmore-config-card-open" : ""}`, children: [
680
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: styles }),
681
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
780
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("li", { className: `dshmore-config-card${open ? " dshmore-config-card-open" : ""}`, children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: styles }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
682
783
  "button",
683
784
  {
684
785
  type: "button",
@@ -689,23 +790,23 @@ function PatchConfigCard({ activation, initiallyOpen = false }) {
689
790
  setOpen((current) => !current);
690
791
  },
691
792
  children: [
692
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dshmore-config-heading-copy", children: [
693
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: "DSH More" }),
694
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "\u52A8\u6001\u5F00\u542F\u6216\u5173\u95ED\u72EC\u7ACB\u8865\u4E01\uFF0C\u9ED8\u8BA4\u5168\u90E8\u5F00\u542F\u3002" })
793
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "dshmore-config-heading-copy", children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "DSH More" }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "\u52A8\u6001\u5F00\u542F\u6216\u5173\u95ED\u72EC\u7ACB\u8865\u4E01\uFF0C\u9ED8\u8BA4\u5168\u90E8\u5F00\u542F\u3002" })
695
796
  ] }),
696
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_dsh_client_ui_primitives4.IconChevronDownOutline14, { className: `dshmore-config-chevron${open ? " dshmore-config-chevron-open" : ""}` })
797
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_dsh_client_ui_primitives5.IconChevronDownOutline14, { className: `dshmore-config-chevron${open ? " dshmore-config-chevron-open" : ""}` })
697
798
  ]
698
799
  }
699
800
  ),
700
- open && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dshmore-config-body", children: [
701
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dshmore-config-list", children: PATCH_CATALOG.map((patch) => {
801
+ open && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dshmore-config-body", children: [
802
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dshmore-config-list", children: PATCH_CATALOG.map((patch) => {
702
803
  const waiting = busy.has(patch.id);
703
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dshmore-config-row", children: [
704
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dshmore-config-copy", children: [
705
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: patch.name }),
706
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: patch.description })
804
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "dshmore-config-row", children: [
805
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "dshmore-config-copy", children: [
806
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: patch.name }),
807
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: patch.description })
707
808
  ] }),
708
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
809
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
709
810
  "input",
710
811
  {
711
812
  type: "checkbox",
@@ -720,9 +821,9 @@ function PatchConfigCard({ activation, initiallyOpen = false }) {
720
821
  )
721
822
  ] }, patch.id);
722
823
  }) }),
723
- scope.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dshmore-config-note", children: "\u6B63\u5728\u8BFB\u53D6\u63D2\u4EF6\u914D\u7F6E\u2026" }),
724
- scope.status === "ready" && !scope.writable && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dshmore-config-note", children: "\u5F53\u524D Settings \u5B58\u50A8\u4E3A\u53EA\u8BFB\u3002" }),
725
- error !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dshmore-config-error", children: error })
824
+ scope.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "dshmore-config-note", children: "\u6B63\u5728\u8BFB\u53D6\u63D2\u4EF6\u914D\u7F6E\u2026" }),
825
+ scope.status === "ready" && !scope.writable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "dshmore-config-note", children: "\u5F53\u524D Settings \u5B58\u50A8\u4E3A\u53EA\u8BFB\u3002" }),
826
+ error !== null && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "dshmore-config-error", children: error })
726
827
  ] })
727
828
  ] });
728
829
  }
@@ -732,14 +833,14 @@ function installPatchConfigCard(ctx, activation) {
732
833
  key: PATCH_SETTINGS_NAMESPACE,
733
834
  priority: 90,
734
835
  registrant: PLUGIN_NAME
735
- }, (props) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PatchConfigCard, { ...props, activation })));
836
+ }, (props) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PatchConfigCard, { ...props, activation })));
736
837
  }
737
838
 
738
839
  // src/kernel/message-visibility.ts
739
840
  var MESSAGE_VISIBILITY_PROJECTION_KEY = "dshMoreMessageDelete";
740
841
 
741
842
  // src/platform/dsh/client/message-targets.tsx
742
- var import_react5 = require("react");
843
+ var import_react6 = require("react");
743
844
 
744
845
  // src/platform/dsh/client/message-content.ts
745
846
  function contentText(content) {
@@ -845,8 +946,8 @@ function trajectorySourceSeq(row) {
845
946
  }
846
947
  function useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys) {
847
948
  const snapshot = props.useSession((state) => state);
848
- const [targets, setTargets] = (0, import_react5.useState)([]);
849
- (0, import_react5.useLayoutEffect)(() => {
949
+ const [targets, setTargets] = (0, import_react6.useState)([]);
950
+ (0, import_react6.useLayoutEffect)(() => {
850
951
  let frame = null;
851
952
  const scan = () => {
852
953
  frame = null;
@@ -858,13 +959,11 @@ function useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys) {
858
959
  const seq = presentationSeq(node);
859
960
  if (seq !== void 0 && hiddenSeqs.has(seq)) {
860
961
  row.dataset.dshmoreHidden = "";
861
- row.style.display = "none";
862
962
  row.querySelector("[data-dshmore-message-actions]")?.remove();
863
963
  continue;
864
964
  }
865
965
  if (row.dataset.dshmoreHidden !== void 0) {
866
966
  delete row.dataset.dshmoreHidden;
867
- row.style.removeProperty("display");
868
967
  }
869
968
  const info = messageNodeInfo(node);
870
969
  if (info !== null) next.push({ key, ...info, host: hostForRow(row, info.kind) });
@@ -874,10 +973,8 @@ function useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys) {
874
973
  const key = trajectoryRecordKey(row);
875
974
  if (seq !== void 0 && hiddenSeqs.has(seq) || key !== void 0 && hiddenTrajectoryKeys.has(key)) {
876
975
  row.dataset.dshmoreHidden = "";
877
- row.style.display = "none";
878
976
  } else if (row.dataset.dshmoreHidden !== void 0) {
879
977
  delete row.dataset.dshmoreHidden;
880
- row.style.removeProperty("display");
881
978
  }
882
979
  }
883
980
  setTargets((current) => sameTargets(current, next) ? current : next);
@@ -893,7 +990,6 @@ function useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys) {
893
990
  if (frame !== null) window.cancelAnimationFrame(frame);
894
991
  for (const row of document.querySelectorAll("[data-dshmore-hidden]")) {
895
992
  delete row.dataset.dshmoreHidden;
896
- row.style.removeProperty("display");
897
993
  }
898
994
  for (const host of document.querySelectorAll("[data-dshmore-message-actions]")) host.remove();
899
995
  };
@@ -902,14 +998,14 @@ function useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys) {
902
998
  }
903
999
 
904
1000
  // src/client/index.tsx
905
- var import_jsx_runtime5 = require("react/jsx-runtime");
1001
+ var import_jsx_runtime6 = require("react/jsx-runtime");
906
1002
  var inject = ["slots", "sessions", "workspaces", "settingsScope"];
907
1003
  function MessageActionPatchController({ patch, props, targets, enabled }) {
908
1004
  const actions = patch.useActions(props, enabled);
909
1005
  const style = { "--dshmore-action-order": patch.order };
910
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1006
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
911
1007
  enabled && targets.map((target) => (0, import_react_dom.createPortal)(
912
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dshmore-action-contribution", style, children: actions.renderAction(target) }),
1008
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dshmore-action-contribution", style, children: actions.renderAction(target) }),
913
1009
  target.host,
914
1010
  `${patch.id}:${target.key}`
915
1011
  )),
@@ -917,14 +1013,14 @@ function MessageActionPatchController({ patch, props, targets, enabled }) {
917
1013
  ] });
918
1014
  }
919
1015
  function MessageActionsController(props) {
920
- const settings = (0, import_react6.useSyncExternalStore)(props.activation.subscribe, props.activation.getSnapshot);
1016
+ const settings = (0, import_react7.useSyncExternalStore)(props.activation.subscribe, props.activation.getSnapshot);
921
1017
  const projected = props.useProjection(MESSAGE_VISIBILITY_PROJECTION_KEY);
922
- const hiddenSeqs = (0, import_react6.useMemo)(() => new Set(projected?.deletedSeqs ?? []), [projected]);
923
- const hiddenTrajectoryKeys = (0, import_react6.useMemo)(() => new Set(projected?.hiddenTrajectoryKeys ?? []), [projected]);
1018
+ const hiddenSeqs = (0, import_react7.useMemo)(() => new Set(projected?.deletedSeqs ?? []), [projected]);
1019
+ const hiddenTrajectoryKeys = (0, import_react7.useMemo)(() => new Set(projected?.hiddenTrajectoryKeys ?? []), [projected]);
924
1020
  const targets = useMessageTargets(props, hiddenSeqs, hiddenTrajectoryKeys);
925
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
926
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: styles }),
927
- MESSAGE_ACTION_PATCHES.map((patch) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1021
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1022
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: styles }),
1023
+ MESSAGE_ACTION_PATCHES.map((patch) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
928
1024
  MessageActionPatchController,
929
1025
  {
930
1026
  patch,
@@ -948,7 +1044,7 @@ function apply(ctx) {
948
1044
  id: `${PLUGIN_NAME}-message-actions`,
949
1045
  order: 90,
950
1046
  registrant: PLUGIN_NAME
951
- }, (props) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MessageActionsController, { ...props, ctx, activation })));
1047
+ }, (props) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MessageActionsController, { ...props, ctx, activation })));
952
1048
  for (const patch of CLIENT_PATCHES) patch.install(ctx, activation);
953
1049
  }
954
1050
  return module.exports; } });
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ var DshMoreError = class extends Error {
16
16
  this.status = status;
17
17
  this.name = "DshMoreError";
18
18
  }
19
+ code;
20
+ status;
19
21
  };
20
22
 
21
23
  // src/platform/dsh/host/agent-session.ts
@@ -76,13 +78,20 @@ function verifyConfirmationToken(secret, token, expected, now = Date.now()) {
76
78
 
77
79
  // src/platform/dsh/host/wire.ts
78
80
  var MAX_BODY_BYTES = 64 * 1024;
81
+ function requireJsonContentType(req) {
82
+ const value = req.headers["content-type"];
83
+ const contentType = (Array.isArray(value) ? value[0] : value)?.split(";", 1)[0]?.trim().toLowerCase();
84
+ if (contentType !== "application/json") {
85
+ throw new DshMoreError("unsupported-media-type", "\u8BF7\u6C42\u5FC5\u987B\u4F7F\u7528 application/json\u3002", 415);
86
+ }
87
+ }
79
88
  async function readJson(req) {
80
89
  const chunks = [];
81
90
  let size = 0;
82
91
  for await (const chunk of req) {
83
92
  const bytes = Buffer.from(chunk);
84
93
  size += bytes.length;
85
- if (size > MAX_BODY_BYTES) throw new DshMoreError("bad-request", "\u8BF7\u6C42\u5185\u5BB9\u8FC7\u5927\u3002");
94
+ if (size > MAX_BODY_BYTES) throw new DshMoreError("payload-too-large", "\u8BF7\u6C42\u5185\u5BB9\u8FC7\u5927\u3002", 413);
86
95
  chunks.push(bytes);
87
96
  }
88
97
  const text = Buffer.concat(chunks).toString("utf8");
@@ -119,8 +128,7 @@ function writeError(res, error) {
119
128
  write(res, error.status, { ok: false, error: { code: error.code, message: error.message } });
120
129
  return;
121
130
  }
122
- const message = error instanceof Error ? error.message : String(error);
123
- write(res, 500, { ok: false, error: { code: "internal", message } });
131
+ write(res, 500, { ok: false, error: { code: "internal", message: "\u5185\u90E8\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5\u6216\u67E5\u770B DSH \u65E5\u5FD7\u3002" } });
124
132
  }
125
133
 
126
134
  // src/patches/message-edit/shared.ts
@@ -180,6 +188,25 @@ function replaySeedRuntimeContext(agentCtx, seed) {
180
188
  }));
181
189
  }
182
190
 
191
+ // src/platform/dsh/host/continuation.ts
192
+ async function rollbackFailedContinuation(child, workspace, attached, cause) {
193
+ const errors = [cause];
194
+ if (attached && workspace !== void 0) {
195
+ try {
196
+ await workspace.detachSession(child.agent.id);
197
+ } catch (error) {
198
+ errors.push(error);
199
+ }
200
+ }
201
+ try {
202
+ await child.dispose();
203
+ } catch (error) {
204
+ errors.push(error);
205
+ }
206
+ if (errors.length === 1) throw cause;
207
+ throw new AggregateError(errors, "continuation failed and cleanup was incomplete");
208
+ }
209
+
183
210
  // src/patches/message-edit/host/edit-continuation.ts
184
211
  function textDigest(text) {
185
212
  return createHash("sha256").update(text).digest("hex");
@@ -244,20 +271,23 @@ async function createEditedContinuation(ctx, sourceAgent, cut, editedText, child
244
271
  releaseRuntimeContextReplay = replaySeedRuntimeContext(agentCtx, seed);
245
272
  }
246
273
  });
274
+ const workspace = ctx.workspaceRegistry.list().find((candidate) => candidate.sessionIds.includes(source.id));
275
+ let attached = false;
247
276
  try {
248
- const workspace = ctx.workspaceRegistry.list().find((candidate) => candidate.sessionIds.includes(source.id));
249
- if (workspace !== void 0) await workspace.attachSession(childId);
250
- await ctx.workspaceRegistry.archiveSession(source.id);
277
+ if (workspace !== void 0) {
278
+ await workspace.attachSession(childId);
279
+ attached = true;
280
+ }
251
281
  child.agent.followup(createUserMessage({
252
282
  content: editedContent(original.data.content, editedText.trim()),
253
283
  source: { kind: "user" }
254
284
  }));
255
285
  void child.agent.whenIdle().then(releaseReplay, releaseReplay);
286
+ await ctx.workspaceRegistry.archiveSession(source.id);
256
287
  return { sessionId: childId };
257
288
  } catch (error) {
258
289
  releaseReplay();
259
- await child.dispose().catch(() => void 0);
260
- throw error;
290
+ return rollbackFailedContinuation(child, workspace, attached, error);
261
291
  }
262
292
  }
263
293
 
@@ -547,15 +577,18 @@ async function createDeletedContinuation(ctx, sourceAgent, selection, childId =
547
577
  });
548
578
  }
549
579
  });
580
+ const workspace = ctx.workspaceRegistry.list().find((candidate) => candidate.sessionIds.includes(source.id));
581
+ let attached = false;
550
582
  try {
551
- const workspace = ctx.workspaceRegistry.list().find((candidate) => candidate.sessionIds.includes(source.id));
552
- if (workspace !== void 0) await workspace.attachSession(childId);
583
+ if (workspace !== void 0) {
584
+ await workspace.attachSession(childId);
585
+ attached = true;
586
+ }
553
587
  await ctx.workspaceRegistry.archiveSession(source.id);
554
588
  return { sessionId: childId };
555
589
  } catch (error) {
556
590
  releaseReplay();
557
- await child.dispose().catch(() => void 0);
558
- throw error;
591
+ return rollbackFailedContinuation(child, workspace, attached, error);
559
592
  }
560
593
  }
561
594
 
@@ -618,7 +651,7 @@ var hostPatch2 = {
618
651
  var SESSION_DELETE_PATCH_ID = "session-delete";
619
652
 
620
653
  // src/patches/session-delete/host/live-session-handles.ts
621
- var TRACKER = Symbol.for("dsh-more.session-delete.live-session-handles");
654
+ var TRACKER = /* @__PURE__ */ Symbol.for("dsh-more.session-delete.live-session-handles");
622
655
  function trackedHandle(tracker, handle) {
623
656
  let disposed = false;
624
657
  const wrapped = {
@@ -667,7 +700,7 @@ function getLiveSessionHandle(ctx, sessionId) {
667
700
 
668
701
  // src/patches/session-delete/host/session-deletion.ts
669
702
  import { rm, stat } from "node:fs/promises";
670
- import { dirname, isAbsolute, relative } from "node:path";
703
+ import { basename, dirname, isAbsolute } from "node:path";
671
704
  import { SessionId as SessionId6 } from "@deepseek-ai/dsh-session";
672
705
  function detachInternal(registry, sessionId, label) {
673
706
  const internal = registry;
@@ -704,14 +737,16 @@ async function unloadLiveSession(ctx, sessionId) {
704
737
  throw new DshMoreError("internal", "\u4F1A\u8BDD\u505C\u6B62\u540E\u4ECD\u5904\u4E8E\u52A0\u8F7D\u72B6\u6001\uFF0C\u672A\u6267\u884C\u78C1\u76D8\u5220\u9664\u3002", 500);
705
738
  }
706
739
  }
707
- function safeSessionDirectory(locationPath) {
740
+ function sessionDirectoryFromLocation(locationPath) {
708
741
  if (!isAbsolute(locationPath)) {
709
742
  throw new DshMoreError("internal", "\u5F53\u524D\u6301\u4E45\u5316\u540E\u7AEF\u6CA1\u6709\u8FD4\u56DE\u7EDD\u5BF9\u4F1A\u8BDD\u8DEF\u5F84\u3002", 409);
710
743
  }
744
+ const artifact = basename(locationPath);
745
+ if (artifact !== "session.jsonl" && artifact !== "session.jsonl.zstd") {
746
+ throw new DshMoreError("internal", "\u6301\u4E45\u5316\u540E\u7AEF\u8FD4\u56DE\u4E86\u672A\u77E5\u7684\u4F1A\u8BDD\u6587\u4EF6\u5E03\u5C40\u3002", 500);
747
+ }
711
748
  const sessionDir = dirname(locationPath);
712
- const sessionsRoot = dirname(dirname(sessionDir));
713
- const withinRoot = relative(sessionsRoot, sessionDir);
714
- if (withinRoot === "" || withinRoot.startsWith("..") || isAbsolute(withinRoot)) {
749
+ if (sessionDir === dirname(sessionDir)) {
715
750
  throw new DshMoreError("internal", "\u6301\u4E45\u5316\u540E\u7AEF\u8FD4\u56DE\u4E86\u4E0D\u5B89\u5168\u7684\u4F1A\u8BDD\u8DEF\u5F84\u3002", 500);
716
751
  }
717
752
  return sessionDir;
@@ -728,7 +763,7 @@ async function deleteSessionPermanently(ctx, rawSessionId) {
728
763
  if (location.kind !== "jsonl") {
729
764
  throw new DshMoreError("internal", `\u6301\u4E45\u5316\u540E\u7AEF ${location.kind} \u4E0D\u652F\u6301\u5B89\u5168\u7684\u9010\u76EE\u5F55\u5220\u9664\u3002`, 409);
730
765
  }
731
- const sessionDir = safeSessionDirectory(location.path);
766
+ const sessionDir = sessionDirectoryFromLocation(location.path);
732
767
  await unloadLiveSession(ctx, sessionId);
733
768
  const info = await stat(sessionDir).catch(() => void 0);
734
769
  if (info !== void 0) {
@@ -737,7 +772,7 @@ async function deleteSessionPermanently(ctx, rawSessionId) {
737
772
  }
738
773
  const workspaces = ctx.workspaceRegistry.list().filter((candidate) => candidate.sessionIds.includes(sessionId));
739
774
  await Promise.all(workspaces.map(async (workspace) => workspace.detachSession(sessionId)));
740
- return { sessionId, deletedPath: sessionDir };
775
+ return { sessionId };
741
776
  }
742
777
 
743
778
  // src/patches/session-delete/host/index.ts
@@ -755,14 +790,177 @@ var hostPatch3 = {
755
790
  })
756
791
  };
757
792
 
793
+ // src/patches/conversation-markdown-export/shared.ts
794
+ var CONVERSATION_MARKDOWN_EXPORT_PATCH_ID = "conversation-markdown-export";
795
+
796
+ // src/patches/conversation-markdown-export/host/transcript.ts
797
+ function singleLine(value) {
798
+ return value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
799
+ }
800
+ function isoTime(value) {
801
+ const date = new Date(value);
802
+ return Number.isFinite(date.getTime()) ? date.toISOString() : "\u672A\u77E5\u65F6\u95F4";
803
+ }
804
+ function codeFence(value, language = "") {
805
+ const longest = Math.max(0, ...[...value.matchAll(/`+/g)].map((match) => match[0].length));
806
+ const fence = "`".repeat(Math.max(3, longest + 1));
807
+ return `${fence}${language}
808
+ ${value}
809
+ ${fence}`;
810
+ }
811
+ function jsonBlock(value) {
812
+ let serialized;
813
+ try {
814
+ serialized = JSON.stringify(value, null, 2) ?? String(value);
815
+ } catch {
816
+ serialized = String(value);
817
+ }
818
+ return codeFence(serialized, "json");
819
+ }
820
+ function imageDescription(block) {
821
+ const attachment = block.attachment;
822
+ const name2 = singleLine(attachment.name ?? "\u56FE\u7247") || "\u56FE\u7247";
823
+ return `[${name2}\uFF1B\u9644\u4EF6 ID\uFF1A${String(attachment.attachmentId)}\uFF1B${attachment.mediaType}\uFF1B${String(attachment.width)}\xD7${String(attachment.height)}\uFF1B${String(attachment.bytes)} \u5B57\u8282]`;
824
+ }
825
+ function contentBlockMarkdown(block) {
826
+ switch (block.type) {
827
+ case "text":
828
+ return block.text;
829
+ case "reasoning":
830
+ return `<details>
831
+ <summary>\u601D\u8003\u8FC7\u7A0B</summary>
832
+
833
+ ${block.text}
834
+
835
+ </details>`;
836
+ case "image":
837
+ return imageDescription(block);
838
+ case "tool-call": {
839
+ const name2 = singleLine(block.name) || "\u672A\u547D\u540D\u5DE5\u5177";
840
+ return `### \u5DE5\u5177\u8C03\u7528\uFF1A${name2}
841
+
842
+ ${codeFence(block.arguments, "json")}`;
843
+ }
844
+ case "tool-result": {
845
+ const status = block.isError === true ? "\uFF08\u5931\u8D25\uFF09" : "";
846
+ return `### \u5DE5\u5177\u7ED3\u679C${status}
847
+
848
+ ${contentMarkdown(block.content)}`;
849
+ }
850
+ default:
851
+ return `### \u672A\u8BC6\u522B\u5185\u5BB9
852
+
853
+ ${jsonBlock(block)}`;
854
+ }
855
+ }
856
+ function contentMarkdown(content) {
857
+ if (content.length === 0) return "\uFF08\u7A7A\u6D88\u606F\uFF09";
858
+ return content.map(contentBlockMarkdown).filter((part) => part !== "").join("\n\n") || "\uFF08\u7A7A\u6D88\u606F\uFF09";
859
+ }
860
+ function messageSection(label, time, body) {
861
+ return `## ${singleLine(label)}
862
+
863
+ > \u65F6\u95F4\uFF1A${isoTime(time)}
864
+
865
+ ${body}`;
866
+ }
867
+ function sourceLabel(event) {
868
+ const source = event.data.source;
869
+ if (source.kind === "user") return "\u7528\u6237";
870
+ if (source.kind === "plugin") return `\u4E0A\u4E0B\u6587 \xB7 ${singleLine(source.plugin) || "\u63D2\u4EF6"}`;
871
+ return "\u4E0A\u4E0B\u6587";
872
+ }
873
+ function toolCallIds(content) {
874
+ return content.filter((block) => block.type === "tool-call").map((block) => String(block.id));
875
+ }
876
+ function conversationMarkdown(events, options) {
877
+ const sections = [];
878
+ const renderedCalls = /* @__PURE__ */ new Set();
879
+ const callNames = /* @__PURE__ */ new Map();
880
+ for (const event of events) {
881
+ if (event.type === "user/message") {
882
+ sections.push(messageSection(sourceLabel(event), event.time, contentMarkdown(event.data.content)));
883
+ continue;
884
+ }
885
+ if (event.type === "assistant/message") {
886
+ for (const callId of toolCallIds(event.data.message.content)) renderedCalls.add(callId);
887
+ sections.push(messageSection("\u52A9\u624B", event.time, contentMarkdown(event.data.message.content)));
888
+ continue;
889
+ }
890
+ if (event.type === "tool/call") {
891
+ const callId = String(event.data.callId);
892
+ callNames.set(callId, event.data.name);
893
+ if (!renderedCalls.has(callId)) {
894
+ sections.push(messageSection(
895
+ `\u5DE5\u5177\u8C03\u7528 \xB7 ${singleLine(event.data.name) || "\u672A\u547D\u540D\u5DE5\u5177"}`,
896
+ event.time,
897
+ codeFence(event.data.arguments, "json")
898
+ ));
899
+ renderedCalls.add(callId);
900
+ }
901
+ continue;
902
+ }
903
+ if (event.type === "tool/result") {
904
+ const callId = String(event.data.message.source.callId);
905
+ const block = event.data.message.content[0];
906
+ const failed = block?.type === "tool-result" && block.isError === true;
907
+ const resultContent = block?.type === "tool-result" ? block.content : event.data.message.content;
908
+ const name2 = singleLine(callNames.get(callId) ?? callId) || "\u672A\u547D\u540D\u5DE5\u5177";
909
+ sections.push(messageSection(
910
+ `\u5DE5\u5177\u7ED3\u679C \xB7 ${name2}${failed ? "\uFF08\u5931\u8D25\uFF09" : ""}`,
911
+ event.time,
912
+ contentMarkdown(resultContent)
913
+ ));
914
+ }
915
+ }
916
+ const title = singleLine(options.title) || "\u672A\u547D\u540D\u4F1A\u8BDD";
917
+ const exportedAt = isoTime(options.exportedAt ?? Date.now());
918
+ const header2 = [
919
+ `# ${title}`,
920
+ `- \u4F1A\u8BDD ID\uFF1A${singleLine(options.sessionId)}`,
921
+ `- \u5BFC\u51FA\u65F6\u95F4\uFF1A${exportedAt}`,
922
+ `- \u8BB0\u5F55\u6761\u76EE\uFF1A${String(sections.length)}`
923
+ ].join("\n\n");
924
+ return {
925
+ markdown: sections.length === 0 ? `${header2}
926
+
927
+ \uFF08\u6682\u65E0\u804A\u5929\u8BB0\u5F55\uFF09
928
+ ` : `${header2}
929
+
930
+ ---
931
+
932
+ ${sections.join("\n\n---\n\n")}
933
+ `,
934
+ entryCount: sections.length
935
+ };
936
+ }
937
+
938
+ // src/patches/conversation-markdown-export/host/index.ts
939
+ var MAX_TITLE_LENGTH = 500;
940
+ function render(ctx, payload) {
941
+ const sessionId = requireString(payload, "sessionId");
942
+ const title = requireString(payload, "title");
943
+ if ([...title].length > MAX_TITLE_LENGTH) throw new DshMoreError("bad-request", "\u4F1A\u8BDD\u6807\u9898\u8FC7\u957F\u3002");
944
+ const session = requireLiveSession(ctx, sessionId);
945
+ requireIdleAgent(ctx, sessionId, session);
946
+ return conversationMarkdown(session.events, { title, sessionId });
947
+ }
948
+ var hostPatch4 = {
949
+ id: CONVERSATION_MARKDOWN_EXPORT_PATCH_ID,
950
+ routes: ({ ctx }) => ({
951
+ render: (payload) => render(ctx, payload)
952
+ })
953
+ };
954
+
758
955
  // src/generated/host-registry.ts
759
- var HOST_PATCHES = [hostPatch, hostPatch2, hostPatch3];
956
+ var HOST_PATCHES = [hostPatch, hostPatch2, hostPatch3, hostPatch4];
760
957
 
761
958
  // src/generated/patch-catalog.ts
762
959
  var DEFAULT_PATCH_SETTINGS = {
763
960
  "message-edit": true,
764
961
  "message-delete": true,
765
- "session-delete": true
962
+ "session-delete": true,
963
+ "conversation-markdown-export": true
766
964
  };
767
965
 
768
966
  // src/kernel/host/activation.ts
@@ -772,6 +970,7 @@ var HostPatchActivation = class {
772
970
  this.ctx = ctx;
773
971
  this.byId = new Map(patches.map((patch) => [patch.id, patch]));
774
972
  }
973
+ ctx;
775
974
  byId;
776
975
  disposers = /* @__PURE__ */ new Map();
777
976
  settings = { ...DEFAULT_PATCH_SETTINGS };
@@ -805,7 +1004,8 @@ import z2 from "@deepseek-ai/schemastery";
805
1004
  var PatchSettingsSchema = z2.object({
806
1005
  "message-edit": z2.boolean().default(true),
807
1006
  "message-delete": z2.boolean().default(true),
808
- "session-delete": z2.boolean().default(true)
1007
+ "session-delete": z2.boolean().default(true),
1008
+ "conversation-markdown-export": z2.boolean().default(true)
809
1009
  });
810
1010
 
811
1011
  // src/kernel/host/settings.ts
@@ -878,6 +1078,7 @@ function collectRoutes(patches, runtime) {
878
1078
  function registerPatchApi(ctx, patches, confirmationSecret, isEnabled) {
879
1079
  const host = ctx;
880
1080
  const routes = collectRoutes(patches, { ctx, confirmationSecret });
1081
+ const logger = ctx.logger(PLUGIN_NAME);
881
1082
  ctx.effect(() => ctx.webServer.register({
882
1083
  kind: "prefix",
883
1084
  path: PLUGIN_API_PREFIX,
@@ -903,8 +1104,13 @@ function registerPatchApi(ctx, patches, confirmationSecret, isEnabled) {
903
1104
  return;
904
1105
  }
905
1106
  try {
1107
+ requireJsonContentType(req);
906
1108
  writeOk(res, await registered.handler(await readJson(req)));
907
1109
  } catch (error) {
1110
+ if (!(error instanceof DshMoreError) || error.status >= 500) {
1111
+ const detail = error instanceof Error ? error.stack ?? error.message : String(error);
1112
+ logger.error("patch API %s failed: %s", route, detail);
1113
+ }
908
1114
  writeError(res, error);
909
1115
  }
910
1116
  }
@@ -3,8 +3,10 @@ export declare const PatchSettingsSchema: z<Schemastery.ObjectS<{
3
3
  "message-edit": z<boolean, boolean>;
4
4
  "message-delete": z<boolean, boolean>;
5
5
  "session-delete": z<boolean, boolean>;
6
+ "conversation-markdown-export": z<boolean, boolean>;
6
7
  }>, Schemastery.ObjectT<{
7
8
  "message-edit": z<boolean, boolean>;
8
9
  "message-delete": z<boolean, boolean>;
9
10
  "session-delete": z<boolean, boolean>;
11
+ "conversation-markdown-export": z<boolean, boolean>;
10
12
  }>>;
@@ -19,12 +19,20 @@ export declare const PATCH_CATALOG: readonly [{
19
19
  readonly defaultEnabled: true;
20
20
  readonly order: 300;
21
21
  readonly clientKind: "standalone";
22
+ }, {
23
+ readonly id: "conversation-markdown-export";
24
+ readonly name: "导出完整聊天记录";
25
+ readonly description: "将当前会话的完整聊天记录下载为 Markdown 文件";
26
+ readonly defaultEnabled: true;
27
+ readonly order: 400;
28
+ readonly clientKind: "standalone";
22
29
  }];
23
30
  export type PatchId = typeof PATCH_CATALOG[number]['id'];
24
31
  export interface PatchSettings {
25
32
  "message-edit": boolean;
26
33
  "message-delete": boolean;
27
34
  "session-delete": boolean;
35
+ "conversation-markdown-export": boolean;
28
36
  }
29
37
  export declare const DEFAULT_PATCH_SETTINGS: PatchSettings;
30
38
  export declare function decodePatchSettings(value: unknown): PatchSettings | undefined;
@@ -0,0 +1 @@
1
+ export declare function markdownFilename(title: string, sessionId: string): string;
@@ -0,0 +1,2 @@
1
+ import type { ClientPatch } from '../../../kernel/client/patch.js';
2
+ export declare const clientPatch: ClientPatch;
@@ -0,0 +1,2 @@
1
+ import type { HostPatch } from '../../../kernel/host/patch.js';
2
+ export declare const hostPatch: HostPatch;
@@ -0,0 +1,9 @@
1
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
2
+ import type { ConversationMarkdownExport } from '../shared.js';
3
+ export interface ConversationMarkdownOptions {
4
+ title: string;
5
+ sessionId: string;
6
+ exportedAt?: number;
7
+ }
8
+ /** Convert one immutable Session event snapshot into a complete Markdown transcript. */
9
+ export declare function conversationMarkdown(events: readonly SessionEvent[], options: ConversationMarkdownOptions): ConversationMarkdownExport;
@@ -0,0 +1,5 @@
1
+ export declare const CONVERSATION_MARKDOWN_EXPORT_PATCH_ID = "conversation-markdown-export";
2
+ export interface ConversationMarkdownExport {
3
+ markdown: string;
4
+ entryCount: number;
5
+ }
@@ -2,5 +2,4 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  /** Stop/unload a Session, then recursively remove its exact persistence directory. */
3
3
  export declare function deleteSessionPermanently(ctx: Context, rawSessionId: string): Promise<{
4
4
  sessionId: string;
5
- deletedPath: string;
6
5
  }>;
@@ -1 +1 @@
1
- export declare const styles = "\n.dshmore-inline-actions { display: inline-flex; align-items: center; gap: 10px; }\n.dshmore-inline-button { width: 28px; height: 28px; color: var(--dsw-alias-label-tertiary); cursor: pointer; background: transparent; border: 0; border-radius: 28px; display: inline-flex; align-items: center; justify-content: center; padding: 6px; }\n.dshmore-inline-button:hover { color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-interactive-bg-hover); }\n.dshmore-edit-modal { box-sizing: border-box; width: min(620px, calc(100vw - 32px)) !important; max-width: min(620px, calc(100vw - 32px)) !important; }\n.dshmore-dialog-body { box-sizing: border-box; display: grid; gap: 12px; width: 100%; min-width: 0; overflow: hidden; }\n.dshmore-message-preview { box-sizing: border-box; width: 100%; min-width: 0; max-height: 132px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; padding: 12px; border-radius: 10px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); font-size: 13px; line-height: 1.55; }\n.dshmore-editor { box-sizing: border-box; display: block; width: 100%; min-width: 0; max-width: 100%; resize: vertical; min-height: 150px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; outline: none; padding: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); font: inherit; line-height: 1.55; overflow-x: hidden; overflow-wrap: anywhere; word-break: break-word; }\n.dshmore-editor:focus { border-color: var(--dsw-alias-state-business-primary); }\n.dshmore-warning { padding: 10px 12px; border: 1px solid rgba(220,138,40,.35); border-radius: 10px; background: rgba(220,138,40,.09); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.5; }\n.dshmore-danger { display: flex; gap: 8px; align-items: flex-start; padding: 10px 12px; border-radius: 10px; background: rgba(208,58,58,.10); color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; line-height: 1.5; }\n.dshmore-error { padding: 10px 12px; border-radius: 10px; background: rgba(208,58,58,.10); color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; line-height: 1.5; }\n@media (max-width: 620px) { .dshmore-edit-modal { width: calc(100vw - 24px) !important; max-width: calc(100vw - 24px) !important; } }\n.dshmore-action-contribution { display: inline-flex; align-items: center; order: var(--dshmore-action-order); }\n.dshmore-config-card { box-sizing: border-box; list-style: none; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-3); overflow: hidden; transition: border-color .16s, background .16s; }\n.dshmore-config-card:hover { border-color: var(--dsw-alias-label-dimmed); }\n.dshmore-config-card-open { border-color: var(--dsw-alias-label-dimmed); background: var(--dsw-alias-bg-layer-2); }\n.dshmore-config-heading { appearance: none; box-sizing: border-box; width: 100%; color: inherit; font: inherit; text-align: left; cursor: pointer; background: transparent; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; }\n.dshmore-config-heading:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }\n.dshmore-config-heading-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }\n.dshmore-config-heading-copy strong { color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 1.4; }\n.dshmore-config-heading-copy > span, .dshmore-config-copy span, .dshmore-config-note { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }\n.dshmore-config-chevron { flex: none; color: var(--dsw-alias-label-tertiary); }\n.dshmore-config-chevron-open { transform: rotate(180deg); }\n.dshmore-config-body { margin: 0 16px; padding-bottom: 8px; border-top: 1px solid var(--dsw-alias-border-l2); }\n.dshmore-config-list { display: grid; }\n.dshmore-config-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; cursor: pointer; }\n.dshmore-config-row + .dshmore-config-row { border-top: 1px solid var(--dsw-alias-border-l1); }\n.dshmore-config-row:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dshmore-config-copy { min-width: 0; display: grid; gap: 2px; }\n.dshmore-config-copy strong { color: var(--dsw-alias-label-primary); font-size: 13px; }\n.dshmore-config-row input { width: 34px; height: 18px; flex: none; cursor: pointer; accent-color: var(--dsw-alias-state-business-primary); }\n.dshmore-config-row input:disabled { cursor: default; opacity: .5; }\n.dshmore-config-note, .dshmore-config-error { margin: 0; padding: 10px 0; border-top: 1px solid var(--dsw-alias-border-l1); }\n.dshmore-config-error { color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; }\n@media (prefers-reduced-motion: no-preference) { .dshmore-config-chevron { transition: transform .16s; } }\n";
1
+ export declare const styles = "\n[data-dshmore-hidden] { display: none !important; }\n.dshmore-inline-actions { display: inline-flex; align-items: center; gap: 10px; }\n.dshmore-inline-button { width: 28px; height: 28px; color: var(--dsw-alias-label-tertiary); cursor: pointer; background: transparent; border: 0; border-radius: 28px; display: inline-flex; align-items: center; justify-content: center; padding: 6px; }\n.dshmore-inline-button:hover { color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-interactive-bg-hover); }\n.dshmore-edit-modal { box-sizing: border-box; width: min(620px, calc(100vw - 32px)) !important; max-width: min(620px, calc(100vw - 32px)) !important; }\n.dshmore-dialog-body { box-sizing: border-box; display: grid; gap: 12px; width: 100%; min-width: 0; overflow: hidden; }\n.dshmore-message-preview { box-sizing: border-box; width: 100%; min-width: 0; max-height: 132px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; padding: 12px; border-radius: 10px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); font-size: 13px; line-height: 1.55; }\n.dshmore-editor { box-sizing: border-box; display: block; width: 100%; min-width: 0; max-width: 100%; resize: vertical; min-height: 150px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; outline: none; padding: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); font: inherit; line-height: 1.55; overflow-x: hidden; overflow-wrap: anywhere; word-break: break-word; }\n.dshmore-editor:focus { border-color: var(--dsw-alias-state-business-primary); }\n.dshmore-warning { padding: 10px 12px; border: 1px solid rgba(220,138,40,.35); border-radius: 10px; background: rgba(220,138,40,.09); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.5; }\n.dshmore-danger { display: flex; gap: 8px; align-items: flex-start; padding: 10px 12px; border-radius: 10px; background: rgba(208,58,58,.10); color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; line-height: 1.5; }\n.dshmore-error { padding: 10px 12px; border-radius: 10px; background: rgba(208,58,58,.10); color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; line-height: 1.5; }\n@media (max-width: 620px) { .dshmore-edit-modal { width: calc(100vw - 24px) !important; max-width: calc(100vw - 24px) !important; } }\n.dshmore-action-contribution { display: inline-flex; align-items: center; order: var(--dshmore-action-order); }\n.dshmore-config-card { box-sizing: border-box; list-style: none; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-3); overflow: hidden; transition: border-color .16s, background .16s; }\n.dshmore-config-card:hover { border-color: var(--dsw-alias-label-dimmed); }\n.dshmore-config-card-open { border-color: var(--dsw-alias-label-dimmed); background: var(--dsw-alias-bg-layer-2); }\n.dshmore-config-heading { appearance: none; box-sizing: border-box; width: 100%; color: inherit; font: inherit; text-align: left; cursor: pointer; background: transparent; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; }\n.dshmore-config-heading:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }\n.dshmore-config-heading-copy { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }\n.dshmore-config-heading-copy strong { color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 1.4; }\n.dshmore-config-heading-copy > span, .dshmore-config-copy span, .dshmore-config-note { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }\n.dshmore-config-chevron { flex: none; color: var(--dsw-alias-label-tertiary); }\n.dshmore-config-chevron-open { transform: rotate(180deg); }\n.dshmore-config-body { margin: 0 16px; padding-bottom: 8px; border-top: 1px solid var(--dsw-alias-border-l2); }\n.dshmore-config-list { display: grid; }\n.dshmore-config-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; cursor: pointer; }\n.dshmore-config-row + .dshmore-config-row { border-top: 1px solid var(--dsw-alias-border-l1); }\n.dshmore-config-row:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dshmore-config-copy { min-width: 0; display: grid; gap: 2px; }\n.dshmore-config-copy strong { color: var(--dsw-alias-label-primary); font-size: 13px; }\n.dshmore-config-row input { width: 34px; height: 18px; flex: none; cursor: pointer; accent-color: var(--dsw-alias-state-business-primary); }\n.dshmore-config-row input:disabled { cursor: default; opacity: .5; }\n.dshmore-config-note, .dshmore-config-error { margin: 0; padding: 10px 0; border-top: 1px solid var(--dsw-alias-border-l1); }\n.dshmore-config-error { color: var(--dsw-alias-state-error-primary, rgb(220,90,90)); font-size: 12px; }\n@media (prefers-reduced-motion: no-preference) { .dshmore-config-chevron { transition: transform .16s; } }\n";
@@ -0,0 +1,4 @@
1
+ import type { AgentHandle } from '@deepseek-ai/dsh-agent';
2
+ import type { Workspace } from '@deepseek-ai/dsh-workspace';
3
+ /** Undo the public side effects of a continuation that failed before publication. */
4
+ export declare function rollbackFailedContinuation(child: AgentHandle, workspace: Workspace | undefined, attached: boolean, cause: unknown): Promise<never>;
@@ -1,4 +1,4 @@
1
- export type DshMoreErrorCode = 'bad-request' | 'forbidden' | 'method-not-allowed' | 'not-found' | 'session-not-live' | 'session-busy' | 'invalid-turn-range' | 'selection-empty' | 'stale-preview' | 'patch-disabled' | 'internal';
1
+ export type DshMoreErrorCode = 'bad-request' | 'payload-too-large' | 'forbidden' | 'method-not-allowed' | 'unsupported-media-type' | 'not-found' | 'session-not-live' | 'session-busy' | 'invalid-turn-range' | 'selection-empty' | 'stale-preview' | 'patch-disabled' | 'internal';
2
2
  export declare class DshMoreError extends Error {
3
3
  readonly code: DshMoreErrorCode;
4
4
  readonly status: number;
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ export declare function requireJsonContentType(req: IncomingMessage): void;
2
3
  export declare function readJson(req: IncomingMessage): Promise<unknown>;
3
4
  export declare function requireString(value: unknown, key: string): string;
4
5
  export declare function requireInteger(value: unknown, key: string): number;
package/dsh.plugin.json CHANGED
@@ -1,10 +1,20 @@
1
1
  {
2
2
  "name": "dsh-more",
3
3
  "description": "Practical patches for missing DSH Web context and history controls",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "entry": {
6
6
  "name": "dsh-more",
7
- "inject": ["webServer", "webRuntime", "agents", "sessions", "sessionPersistence", "sessionProjections", "workspaceRegistry", "agentPresets", "settings"]
7
+ "inject": [
8
+ "webServer",
9
+ "webRuntime",
10
+ "agents",
11
+ "sessions",
12
+ "sessionPersistence",
13
+ "sessionProjections",
14
+ "workspaceRegistry",
15
+ "agentPresets",
16
+ "settings"
17
+ ]
8
18
  },
9
19
  "client": {
10
20
  "platform": "web"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-more",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Practical patches for missing DeepSeek Harness Web context and history controls.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "dsh-plugin"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=22.19"
22
+ "node": "^22.19.0 || >=24.0.0"
23
23
  },
24
24
  "packageManager": "pnpm@11.6.0",
25
25
  "publishConfig": {
@@ -66,11 +66,13 @@
66
66
  }
67
67
  },
68
68
  "scripts": {
69
+ "validate:package": "node scripts/validate-package-manifests.mjs",
70
+ "peers:check": "pnpm peers check",
69
71
  "generate": "node scripts/generate-patch-registry.mjs",
70
72
  "build": "node build.mjs",
71
73
  "typecheck": "npm run generate && tsc --noEmit",
72
74
  "test": "npm run generate && vitest run",
73
- "check": "npm run typecheck && npm run test && npm run build",
75
+ "check": "npm run validate:package && npm run peers:check && npm run typecheck && npm run test && npm run build",
74
76
  "package": "npm pack",
75
77
  "package:check": "npm pack --dry-run",
76
78
  "release": "node scripts/release.mjs",
@@ -78,6 +80,7 @@
78
80
  "release:patch": "node scripts/release.mjs patch",
79
81
  "release:minor": "node scripts/release.mjs minor",
80
82
  "release:major": "node scripts/release.mjs major",
83
+ "version": "node scripts/sync-plugin-version.mjs",
81
84
  "prepack": "npm run check"
82
85
  },
83
86
  "peerDependencies": {
@@ -130,7 +133,7 @@
130
133
  "@types/node": "^24.0.0",
131
134
  "@types/react": "~18.3.0",
132
135
  "@types/react-dom": "~18.3.0",
133
- "esbuild": "^0.25.0",
136
+ "esbuild": "^0.28.2",
134
137
  "react": "18.3.1",
135
138
  "react-dom": "18.3.1",
136
139
  "typescript": "^5.9.2",