@xrn1997/dsh-session-delete 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +22 -13
  2. package/THIRD_PARTY_NOTICES.md +179 -0
  3. package/cordis.patch.yml +1 -1
  4. package/docs/screenshots/screenshot-01.png +0 -0
  5. package/docs/screenshots/screenshot-02.png +0 -0
  6. package/docs/screenshots/screenshot-03.png +0 -0
  7. package/icon.svg +24 -18
  8. package/lib/client.js +1439 -410
  9. package/lib/index.js +475 -244
  10. package/lib/types/api/connection-route.d.ts +1 -1
  11. package/lib/types/api/dispatch.d.ts +1 -1
  12. package/lib/types/api/route-table.d.ts +12 -7
  13. package/lib/types/client/api-base.d.ts +19 -0
  14. package/lib/types/client/config-settings.d.ts +13 -0
  15. package/lib/types/client/context-menu-actions.d.ts +47 -0
  16. package/lib/types/client/context.d.ts +3 -0
  17. package/lib/types/client/copy.d.ts +42 -7
  18. package/lib/types/client/delete-confirm.d.ts +2 -2
  19. package/lib/types/client/detail-blocks.d.ts +25 -0
  20. package/lib/types/client/index.d.ts +3 -0
  21. package/lib/types/client/maintenance-settings.d.ts +5 -4
  22. package/lib/types/client/remote.d.ts +43 -15
  23. package/lib/types/client/settings-cards.d.ts +19 -0
  24. package/lib/types/client/settings-view.d.ts +9 -0
  25. package/lib/types/client/trash-detail.d.ts +1 -1
  26. package/lib/types/client/trash-nav.d.ts +5 -1
  27. package/lib/types/client/trash-view.d.ts +34 -5
  28. package/lib/types/client/ui/MenuItemButton.d.ts +1 -1
  29. package/lib/types/client/ui/Toast.d.ts +4 -1
  30. package/lib/types/client/ui/icons.d.ts +23 -7
  31. package/lib/types/client/ui/recipe.d.ts +2 -0
  32. package/lib/types/client/ui/styles.d.ts +2 -0
  33. package/lib/types/client/use-detail-pager.d.ts +33 -0
  34. package/lib/types/config.d.ts +10 -11
  35. package/lib/types/deps.d.ts +9 -1
  36. package/lib/types/index.d.ts +8 -5
  37. package/lib/types/ports/agent-handles.d.ts +10 -2
  38. package/lib/types/ports/fs-port.d.ts +10 -0
  39. package/lib/types/ports/host-port.d.ts +4 -2
  40. package/lib/types/shared/wire.d.ts +37 -5
  41. package/lib/types/trash/attachments.d.ts +15 -2
  42. package/lib/types/verbs/config.d.ts +25 -0
  43. package/lib/types/verbs/errors.d.ts +3 -1
  44. package/lib/types/verbs/permanent-removal.d.ts +10 -5
  45. package/locale/en.json +1 -1
  46. package/locale/zh.json +6 -6
  47. package/package.json +30 -19
@@ -2,7 +2,7 @@
2
2
  * `MenuItemButton`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的同名组件,出处与三条
3
3
  * 改写规则见 `ui.css` 头注):菜单里的一行,宿主菜单容器渲染它、键盘巡游也认它。
4
4
  *
5
- * **形状必须与宿主自己那四行(pin/rename/fork/archive)逐字同构**:`<div class=itemWrap>` 里先是可选
5
+ * **形状必须与宿主自己的官方菜单行(pin/rename/fork/archive/session-id)逐字同构**:`<div class=itemWrap>` 里先是可选
6
6
  * 的发丝线(`role="separator"`),再是 `<button type="button" role="menuitem">`,按钮内是
7
7
  * 图标位 + 标签位(+ 官方的快捷键位)。少一层、换个标签,这一行就会在键盘顺序、hover 底色或
8
8
  * `.itemLabel` 的省略号上跟邻行错开。
@@ -13,6 +13,7 @@
13
13
  * **砍掉的**:`anchor`(把横幅对齐到某个元素的水平中心——本插件的撤销提示就是窗口居中)、
14
14
  * `actions[].prefix`(动作文案前的连接词,本插件只用「撤销」两个字)。**没砍 `text`/`icon`/`tone`/
15
15
  * `holdMs`/`actions`/`onDone`**:唯一的调用点这几样都在用。
16
+ * **本插件加的**:`paused`(撤销在飞时冻结停留与淡出,失败原文要落回同一条横幅;官方没有这个开关)。
16
17
  */
17
18
  import { type ReactElement, type ReactNode } from 'react';
18
19
  import './styles.js';
@@ -32,7 +33,9 @@ export interface ToastProps {
32
33
  holdMs?: number;
33
34
  /** 句尾的行内动作(照着句子的顺序排)。 */
34
35
  actions?: readonly ToastAction[] | undefined;
36
+ /** 暂停自动熄灭:这一趟还有下文(撤销在飞),先把横幅留着——失败原文要靠它上屏。 */
37
+ paused?: boolean | undefined;
35
38
  /** 淡出完成时调用一次:在这里卸载。 */
36
39
  onDone: () => void;
37
40
  }
38
- export declare function Toast({ text, icon, tone, holdMs, actions, onDone }: ToastProps): ReactElement;
41
+ export declare function Toast({ text, icon, tone, holdMs, actions, paused, onDone }: ToastProps): ReactElement;
@@ -1,9 +1,15 @@
1
1
  /**
2
- * 本插件 UI 层要用的图标(逐条抄自 `@deepseek-ai/dsh-client-ui-primitives` 的明文产物;
3
- * 出处与改写规则见 `ui.css` 头注)。
2
+ * 本插件 UI 层要用的图标。除**恢复箭头**外逐条抄自 `@deepseek-ai/dsh-client-ui-primitives`
3
+ * 的明文产物(出处与改写规则见 `ui.css` 头注)。
4
4
  *
5
- * 只留本插件用到的:垃圾桶、警示、打开的项目文件夹、关闭叉、圆圈对勾、对勾、下折/右折 chevron。
6
- * **几何、`viewBox`、`strokeWidth` 逐字不动**——它们是官方图标集的形状,改了就不再与宿主同屏同形。
5
+ * 只留本插件用到的:垃圾桶、警示、文件夹(开盖填充 / 闭盒描边)、**实心三角**(分组头悬停时的
6
+ * 折叠指示)、关闭叉、圆圈对勾、对勾、下折/右折 chevron、恢复(本插件自绘的回退箭头)、
7
+ * 子会话分支(branch)。
8
+ * **官方字形几何、`viewBox`、`strokeWidth` 逐字不动**——改了就不再与宿主同屏同形。
9
+ *
10
+ * **唯一的自绘字形是 `IconRestoreOutlineRegular`**:官方集里 `unarchive-outline`(箱子)与归档动作太像、
11
+ * `refresh-outline`(顺时针圆箭头)读起来是"刷新",都没有"把这条会话放回去"的意思。自绘的 ↩ 沿用宿主
12
+ * 16×16 / 1px 描边的一族几何,不伪装成官方图标。
7
13
  *
8
14
  * 两条与官方实现的差异(都是减法,不是改写):
9
15
  * - 只暴露 `size`:调用处只传 `size`,不摊开官方整个图标 API。
@@ -16,13 +22,23 @@ export interface IconProps {
16
22
  }
17
23
  export declare function IconTrashOutlineRegular({ size }: IconProps): ReactElement;
18
24
  export declare function IconWarningOutlineRegular({ size }: IconProps): ReactElement;
19
- export declare function IconFolderOpenOutlineRegular({ size }: IconProps): ReactElement;
20
25
  export declare function IconCloseOutlineRegular({ size }: IconProps): ReactElement;
21
26
  /** 撤销提示 success 档的前导 glyph(官方 `Toast` 在 `tone="success"` 时自带的那一枚)。 */
22
27
  export declare function IconCheckCircleOutlineRegular({ size }: IconProps): ReactElement;
23
28
  /** 行尾兜底勾选开关那枚对勾(状态点/归档行;官方几何逐字)。 */
24
29
  export declare function IconCheckOutlineRegular({ size }: IconProps): ReactElement;
25
- /** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
30
+ /** 分组头:展开时(官方 `IconFolderOpenRegular`,16×16;开盖那一片带 0.16 透明度)。 */
31
+ export declare function IconFolderOpenRegular({ size }: IconProps): ReactElement;
32
+ /** 分组头:收起时(官方 `IconFolderCloseRegular`,16×16)。 */
33
+ export declare function IconFolderCloseRegular({ size }: IconProps): ReactElement;
34
+ /** 分组头悬停时替换文件夹的折叠指示(官方 `IconTriangleRightFillRegular`);展开时旋转 90°。 */
35
+ export declare function IconTriangleRightFillRegular({ size }: IconProps): ReactElement;
36
+ /** 折叠头与标准档「工作步骤」组「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
26
37
  export declare function IconChevronDownOutlineRegular({ size }: IconProps): ReactElement;
27
- /** 分组头「已折叠」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
38
+ /** 折叠头与子会话导航行「已折叠 / 前进」那一枚(官方 `IconChevronRightOutlineRegular`)。 */
28
39
  export declare function IconChevronRightOutlineRegular({ size }: IconProps): ReactElement;
40
+ /** 行内「恢复」:**本插件自绘**的回退箭头(↩,16×16 / 1px,与宿主图标同一族几何)。
41
+ * 官方集里的 `unarchive` 与归档动作的"箱子"轮廓太近、`refresh` 读作"刷新",都不合适。 */
42
+ export declare function IconRestoreOutlineRegular({ size }: IconProps): ReactElement;
43
+ /** 子会话行的前导:官方 `IconBranchOutlineRegular`(16×16)。 */
44
+ export declare function IconBranchOutlineRegular({ size }: IconProps): ReactElement;
@@ -9,3 +9,5 @@ export declare const QUIET_DANGER: {
9
9
  };
10
10
  /** 单行截断。宽档那一列与可拖的分栏天然会挤文字,逐处手写这三行就会各写各的。 */
11
11
  export declare const ELLIPSIS: CSSProperties;
12
+ /** 数字等宽:清单行、信息表与计量行共用一处,读数横着扫时才不抖。 */
13
+ export declare const TABULAR_NUMS: CSSProperties;
@@ -1,2 +1,4 @@
1
+ /** 装上两个捕获阶段的监听器,返回摘除函数。重复调用会装两套,所以调用点只该是 `apply` 的一个 effect。 */
2
+ export declare function installPointerModality(): () => void;
1
3
  /** 最近一次输入是否来自指针(仅本插件自用)。 */
2
4
  export declare function isPointerModality(): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 详情分页的读侧编排:手动「加载更早」、简洁档自动连翻、prepend 后的滚动锚点。
3
+ * 组件只画;分页的两笔账(连翻预算与滚动位置)都住这里。
4
+ */
5
+ import { type RefObject } from 'react';
6
+ import type { TrashDetailBlockWire } from '../shared/wire.js';
7
+ /**
8
+ * 「加载更早」跟工作步骤档位联动时,一次点击最多自动连翻多少页。
9
+ *
10
+ * 服务端按**块**翻页;简洁档把过程块整段藏起来,一页可能全是过程块 ⇒ 点一下什么都不出现,
11
+ * 看着像没反应。所以简洁档下自动继续往前翻,直到这一批里出现对话块、翻完、或到上限。
12
+ * 切进简洁档时当前页若全是过程块,也按同一份预算自动连翻——否则用户看到的仍是一页空白。
13
+ * 上限防的是"一条几乎全是工具调用的巨型会话"把一次点击变成几十个串行请求。
14
+ */
15
+ export declare const AUTO_OLDER_PAGE_LIMIT = 10;
16
+ interface DetailPagerOptions {
17
+ blocks: readonly TrashDetailBlockWire[];
18
+ olderCursor: number | null;
19
+ loadingOlder: boolean;
20
+ /** 更早一页读失败的原文;正文本身仍是 ready。 */
21
+ olderFailure: string | null;
22
+ /** 简洁档才自动连翻;标准及以上每页至少有一行可见内容(过程组头)。 */
23
+ compact: boolean;
24
+ onLoadOlder: () => void;
25
+ /** 详情栏的滚动容器;prepend 更早一页后按高度差补偿滚动位置。 */
26
+ scrollRef?: RefObject<HTMLDivElement | null> | undefined;
27
+ }
28
+ export interface DetailPagerHandle {
29
+ /** 手动「加载更早」与切进简洁档时那一次自动翻页共用的入口:记下这一批的起点与预算,再交给上层取数。 */
30
+ loadOlder: () => void;
31
+ }
32
+ export declare function useDetailPager({ blocks, olderCursor, loadingOlder, olderFailure, compact, onLoadOlder, scrollRef, }: DetailPagerOptions): DetailPagerHandle;
33
+ export {};
@@ -3,21 +3,20 @@
3
3
  *
4
4
  * 为什么不用 `@deepseek-ai/schemastery` 写 schema:那是宿主包,值 import 会破本仓「跨插件一律
5
5
  * `import type`」的硬约束;Cordis 的 Config 接受 Standard Schema(`@standard-schema/spec`),
6
- * 手写这一小段即可,不需要把宿主 schema 库搬进插件。代价是宿主配置页拿不到字段级 JSON Schema
7
- * ——配置以 profile 的 `cordis.patch.yml` 为准(README「可调项」有示例)。
6
+ * 手写这一小段即可,不需要把宿主 schema 库搬进插件。代价是宿主只为 native Schemastery schema 投影
7
+ * 字段表单,这枚手写的 Standard Schema 不在其列——字段表单由插件设置页的「可调项」卡提供
8
+ * (ADR-0043),profile 的 `cordis.patch.yml` 仍是默认值与覆盖的家。
8
9
  *
9
10
  * 这里只做**默认值、归一与 schema**,不持有运行期现场:`apply` 归一一次,结果作为
10
11
  * `CoreDeps.config` 注入;读方从自己的 deps 读,不 import 模块级单例。
11
12
  * 客户端 6 秒撤销时长不在这里:它是浏览器半的产品决定,不跨半传。
12
13
  */
13
- export interface SessionDeleteConfig {
14
- /** 设置页维护卡「孤儿附件」预览清单最多列多少条(数量与字节仍按全量报告)。 */
15
- readonly orphanPreviewLimit: number;
16
- /** `GET /attachment` 单次响应字节上限(服务路径与直接 fs fallback 共用)。 */
17
- readonly maxAttachmentBytes: number;
18
- /** 附件引用扫描里单个 JSON 引用源的字节上限;超过就按「读不出」fail closed。 */
19
- readonly maxJsonRefBytes: number;
20
- }
14
+ import { type SessionDeleteConfigWire } from './shared/wire.js';
15
+ /** 三只可调项。字段名是线上契约(`shared/wire.ts` 的 `SessionDeleteConfigWire`),这里只是别名;
16
+ * Node 半读到的是归一后的只读快照,所以 `readonly` 保留。默认值、归一与 Standard Schema 住本文件,
17
+ * 接受范围住在 `shared/wire.ts` 的 `CONFIG_LIMITS`:表单与归一必须是同一格,不然会「表单说能存、
18
+ * 激活时报越界」。 */
19
+ export type SessionDeleteConfig = Readonly<SessionDeleteConfigWire>;
21
20
  export declare const DEFAULT_CONFIG: SessionDeleteConfig;
22
21
  /**
23
22
  * 把 patch 里的 `config` 归一成完整配置:缺省取默认值,未知键 / 非整数 / 越界都抛。
@@ -33,7 +32,7 @@ export declare const Config: {
33
32
  readonly version: 1;
34
33
  readonly vendor: "dsh-session-delete";
35
34
  readonly validate: (value: unknown) => {
36
- value: SessionDeleteConfig;
35
+ value: Readonly<SessionDeleteConfigWire>;
37
36
  issues?: never;
38
37
  } | {
39
38
  issues: {
@@ -23,8 +23,16 @@ export type DeleteSessionDeps = CoreDeps & {
23
23
  export type RemovalDeps = CoreDeps & {
24
24
  fs: CoreFsPort & Pick<FsPort, 'removeDir' | 'removeFile'>;
25
25
  };
26
+ /** 宿主 `configEditor` 的窄面:`edit(entry, change)` 把完整 raw config 落盘并让 Loader 重启这一行。
27
+ * `entry` 是 `ctx.fiber.entry`(Loader 挂在 fiber 上,Cordis 类型不声明它,组合根按窄镜像取)。 */
28
+ export interface ConfigEditorLike {
29
+ edit(entry: unknown, change: (current: Record<string, unknown>) => Record<string, unknown>): Promise<void>;
30
+ }
26
31
  /** 组合根装出的全量面:`/apply` 的分发同时够到删除的时钟与永久删除的 `removeDir` / `removeFile`
27
- * (后者也供孤儿附件清理段使用)。`attachments` 只给 `attachment` 读段(ADR-0035);动词不拿它。 */
32
+ * (后者也供孤儿附件清理段使用)。`attachments` 只给 `attachment` 读段(ADR-0035);动词不拿它。
33
+ * `configEditor` / `entry` 只给 `/config` 的写支;宿主没有这套面时写支回 `io`,读支照常。 */
28
34
  export type DeleteDeps = DeleteSessionDeps & RemovalDeps & {
29
35
  readonly attachments: AttachmentsPort;
36
+ readonly configEditor?: ConfigEditorLike;
37
+ readonly entry?: unknown;
30
38
  };
@@ -1,3 +1,5 @@
1
+ /** 只用于类型面:`@deepseek-ai/cordis` 留在 devDependencies 供本仓与 `lib/types` 解析,
2
+ * 不是 peer、也不在运行期加载;纯类型消费者在宿主外使用本包时需要自备它。 */
1
3
  import type { Context } from '@deepseek-ai/cordis';
2
4
  import { Config } from './config.js';
3
5
  export declare const name = "@xrn1997/dsh-session-delete";
@@ -10,14 +12,15 @@ export { Config };
10
12
  * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。**不进 inject 的**:
11
13
  * - `remote`:那是**浏览器半**的服务(asar `dsh-api-gateway/lib/client.js`:`super(ctx, "remote")`),
12
14
  * Node 半挂的是 `ctx.typertGateway`。把它写进 Node 半的 inject 会让插件永远 pending(同 `slots` 那次的形状)。
13
- * - `sessions` / `sessionProjectionCache`:两者都按 `ctx.get(...)` **可选读**——`sessions` 只给 `evictLive`
14
- * 用(活跃判据走 waterfall,见 src/ports/host-port.ts;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
15
- * inject 一个可能没挂的服务 = 整个插件 pending。
16
- * 它取不到时 `titleOf` 返回空串,正是契约允许的降级。
15
+ * - `sessions` / `sessionProjectionCache` / `sessionPersistence`:都按 `ctx.get(...)` **可选读**——
16
+ * `sessions` 给 `evictLive` 和 live header 用(活跃判据走 waterfall,见 src/ports/host-port.ts;
17
+ * 服务缺席回 `unknown`),`sessionProjectionCache` 是标题的投影读面,`sessionPersistence` 是
18
+ * `projectDirOf` / `titleOf` 的公开 header 读面(workspaceRegistry 硬注入它,标准组合必有)。
19
+ * inject 一个可能没挂的服务 = 整个插件 pending;标题取不到时 `titleOf` 返回空串,正是契约允许的降级。
17
20
  * - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
18
21
  * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
19
22
  *
20
- * `connection` 是浏览器半取数通道的唯一承载者:九段注册到 `connection.fetch.register` 的
23
+ * `connection` 是浏览器半取数通道的唯一承载者:十段注册到 `connection.fetch.register` 的
21
24
  * `/api/dsh-session-delete-api/*`,Host/Origin/浏览器认证由宿主 `/api` bridge 完成(ADR-0033)。
22
25
  * 支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`;服务缺席时插件停在 pending
23
26
  * ——可见的激活失败,而不是静默地没有入口。`webServer` 不再直接使用:exact route 仍由宿主的
@@ -1,10 +1,12 @@
1
- /** 捕获所需的 ctx 窄面:可选服务读法(`get`)与按依赖注入的子上下文(`inject`,晚挂载的 `agents` 靠它补装)。
2
- * 有些 ctx / 假件没有 `inject`——那就只在 apply 时 `get` 一次。 */
1
+ /** 捕获所需的 ctx 窄面:可选服务读法(`get`)、按依赖注入的子上下文(`inject`,晚挂载的 `agents` 靠它补装)
2
+ * 与卸载钩子(`effect`,wrapper 的恢复挂在这里)。有些 ctx / 假件没有后两者——那就只在 apply 时 `get` 一次,
3
+ * 且不注册恢复。 */
3
4
  export interface AgentCtxLike {
4
5
  get(name: string): unknown;
5
6
  inject?(names: readonly string[], callback: (child: {
6
7
  get(name: string): unknown;
7
8
  }) => void): void;
9
+ effect?(execute: () => () => unknown, label?: string): unknown;
8
10
  }
9
11
  /** 一次捕获交回的句柄表:按会话 id `dispose`。没捕到 / 已经 dispose 过都是 no-op;
10
12
  * dispose 本身的失败**原样抛出**(调用方 `evictLive` 把它折成 `unknown`,不通告)。 */
@@ -19,6 +21,12 @@ export interface AgentHandleStore {
19
21
  * **安装时机**:先 `get('agents')`,没装成再 `inject(['agents'])`(两者只装一次,符号表兜住)。
20
22
  * **symbol 表**:句柄表挂在服务实例的 {@link CAPTURED_AGENT_HANDLES} 上;同一个 agents 服务上的第二次
21
23
  * 安装(`get` 与 `inject` 各一次,或第二个 `createHostPort`)看见它就直接共用,不把 wrapper 套第二层。
24
+ * **安装者拥有 wrapper**:包装记录先在闭包里留一份(`ownWrappers`,建立于 symbol 写入之前),同时挂到
25
+ * {@link CAPTURED_AGENT_WRAPPERS};第一次成功安装时经 `ctx.effect` 注册一条卸载恢复——按 own property
26
+ * descriptor 的身份把 `resume` / `create` 写回目标自有的原函数(`restoreTo`;代理读回的影子不写回,
27
+ * 别人后装的 wrapper 不动),并摘掉仍归本闭包的 symbol 表。恢复是总的:一条写回失败不跳过其余记录。
28
+ * 恢复一次即锁死:晚到的安装与晚到的 handle 落地都被 `restored` 挡住。没有 `ctx.effect`(假件 / 旧 ctx)
29
+ * 时包装照装,只是没有恢复。
22
30
  * **读回判据**:cordis 的 traceable 服务代理每次读函数都会新包一层(`get` 里的 `createShadowMethod`),
23
31
  * 所以只能看 own property descriptor 的身份;拿 `service[name]` 读回比身份是必然的假警报。
24
32
  */
@@ -1,7 +1,17 @@
1
+ /**
2
+ * **缺席**只认 ENOENT 与 ENOTDIR("路径中间有一截不是目录"与"不存在"是同一种回答)。
3
+ *
4
+ * EACCES / EIO / ELOOP / 断挂载**不是缺席**:这些读数说明"我们看不见",不是"那里没有"。
5
+ * 破坏性动作(删目录、删附件对象、按"没有引用"清共享图)据此做决定,把读不出折成没有
6
+ * 就是数据消失;ADR-0039 是这条判据的裁决。
7
+ */
8
+ export declare function isAbsentPath(error: unknown): boolean;
1
9
  export interface FsPort {
2
10
  /** 全部成员写成属性声明:端口方法会被**解构出来单独调**(`const { sizeOfDir } = port`,见下方
3
11
  * 模块级递归的注释),实现体一律不读 `this`。方法简写的隐式 `this: FsPort` 会让
4
12
  * `unbound-method` 把这种正常用法报成可疑;契约(返回的 Promise 形状)一字未动。 */
13
+ /** 路径在不在。**只有 ENOENT / ENOTDIR 回 `false`**;权限、盘挂、I/O 等读错误照抛——
14
+ * 调用方拿它当"缺席"判据时,读不出必须是失败,不是"没有"(ADR-0039)。 */
5
15
  exists: (p: string) => Promise<boolean>;
6
16
  /** 建目录(含中间层)。首删时 `~/.dsh/session-trash/` 还不存在,而 `rename` 不建父目录。 */
7
17
  ensureDir: (p: string) => Promise<void>;
@@ -9,10 +9,12 @@ export interface HostIdentityPort {
9
9
  * 宿主的「删除工作区」只删注册、保留目录与日志;恢复这类会话时跳过 attach,归入「未分组」。 */
10
10
  workspaceExists(workspaceId: string): Promise<boolean>;
11
11
  /** 会话落盘的**项目目录名**(`<sessions>/<projectKey(cwd)>`);账本里没有这个会话 ⇒ `undefined`。
12
- * 与 `workspaceOf` 同一口径:**未命中是正常的 no-op,不是故障**(存储真故障照抛)。 */
12
+ * header 走公开 live/持久化读面;与 `workspaceOf` 同一口径:**未命中是正常的 no-op,不是故障**
13
+ * (存储真故障照抛)。 */
13
14
  projectDirOf(sessionId: string): Promise<string | undefined>;
14
15
  /** 删除时**尽力**取的会话标题;取不到返回空串,绝不因此让删除失败。
15
- * 标题只存在于宿主自己的账本/缓存里(`FsPort` 不直读文件)⇒ 取数面归宿主端口,接官方投影缓存 API。 */
16
+ * 标题只存在于宿主自己的账本/缓存里(`FsPort` 不直读文件)⇒ 取数面归宿主端口,接官方投影缓存 API;
17
+ * header 与 `projectDirOf` 走同一个公开读面。 */
16
18
  titleOf(sessionId: string): Promise<string>;
17
19
  isLive(sessionId: string): Promise<boolean>;
18
20
  isArchived(sessionId: string): Promise<boolean>;
@@ -1,12 +1,12 @@
1
1
  /**
2
- * 跨半 wire 契约:前缀、九个端点、请求/响应形状与失败码。
2
+ * 跨半 wire 契约:前缀、十个端点、请求/响应形状与失败码。
3
3
  * Node 半(src/api/dispatch.ts)与浏览器半(src/client/remote.ts)共用这一份,不各写字面量。
4
4
  *
5
5
  * 构建约束:本文件零运行时依赖、无 import;浏览器半把整个文件 inline 进 bundle。
6
6
  * 信封 {ok:true,value} / {ok:false,error:{code,message,details?}} 也是两半的共同契约。
7
7
  */
8
8
  /**
9
- * 插件专属前缀(九段相对它命名)。
9
+ * 插件专属前缀(十段相对它命名)。
10
10
  *
11
11
  * 为什么必须带插件名:宿主 `connection.fetch.register` 对**重复的精确路径**直接抛错
12
12
  * (`connection: exact Fetch route "…" is already registered`),撞了就是整棵插件树 boot 失败——
@@ -14,11 +14,11 @@
14
14
  */
15
15
  export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
16
16
  /**
17
- * 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
17
+ * 唯一的取数基础路径:宿主 `/api` + 插件前缀。十段 exact Fetch 路由、客户端探测与附件 URL
18
18
  * 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
19
19
  */
20
20
  export declare const OFFICIAL_SESSION_DELETE_API_PREFIX = "/api/dsh-session-delete-api";
21
- /** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
21
+ /** 十段段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
22
22
  * (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
23
23
  * 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
24
24
  * 两半都不逐段手写一份。
@@ -33,7 +33,8 @@ export declare const OFFICIAL_SESSION_DELETE_API_PREFIX = "/api/dsh-session-dele
33
33
  * 这一段只负责"有多少、在哪、多大"。
34
34
  * - `attachment`:附件原始字节(全仓唯一的二进制响应段;失败仍回 JSON 信封)。
35
35
  * - `attachmentOrphans`:无引用图片对象的扫描报告(纯读;数量/字节 + 供预览的对象清单)。
36
- * - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。 */
36
+ * - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
37
+ * - `config`:三只可调项的读/写共用一段——空体读当前值,带字段写回 profile 的 patch 层。 */
37
38
  export declare const ROUTES: {
38
39
  readonly list: {
39
40
  readonly path: "/list";
@@ -44,6 +45,11 @@ export declare const ROUTES: {
44
45
  readonly path: "/ping";
45
46
  readonly method: "GET";
46
47
  };
48
+ /** 可调项:空体读(与 `/detail` 同法,用 POST 表达一个读);带字段写,写必须进到达序链。 */
49
+ readonly config: {
50
+ readonly path: "/config";
51
+ readonly method: "POST";
52
+ };
47
53
  /** 单条详情(读回收站目录里那条会话自己的日志)。 */
48
54
  readonly detail: {
49
55
  readonly path: "/detail";
@@ -83,6 +89,32 @@ export declare const ROUTES: {
83
89
  /** 段名 → 那一段的路径。从结构化段名表派生:加一段只在上面加一项,`Record<RouteName, …>` 类型闸
84
90
  * (`src/api/route-table.ts`)与运行期键集比对(`tests/route-table.test.ts`)都跟着这一格走。 */
85
91
  export type RouteName = (typeof ROUTES)[keyof typeof ROUTES]['path'];
92
+ /** 三只可调项的线上形状。Node 半的 `SessionDeleteConfig`(`src/config.ts`)是它的别名:
93
+ * 这一格固定线上字段名与接受范围({@link CONFIG_LIMITS}),默认值与归一在 `src/config.ts`。 */
94
+ export interface SessionDeleteConfigWire {
95
+ /** 设置页「孤儿附件」预览清单最多列多少条(数量与字节仍按全量报告)。 */
96
+ orphanPreviewLimit: number;
97
+ /** `GET /attachment` 单次响应字节上限(服务路径与直接 fs fallback 共用)。 */
98
+ maxAttachmentBytes: number;
99
+ /** 附件引用扫描里单个 JSON 引用源的字节上限;超过就按「读不出」fail closed。 */
100
+ maxJsonRefBytes: number;
101
+ }
102
+ /** 三只可调项的接受范围(闭区间整数)。Node 半的归一与浏览器半的表单共用这一格:
103
+ * 两处各写一份范围就会「表单说能存、宿主激活时说越界」。 */
104
+ export declare const CONFIG_LIMITS: {
105
+ readonly orphanPreviewLimit: {
106
+ readonly min: 1;
107
+ readonly max: 10000;
108
+ };
109
+ readonly maxAttachmentBytes: {
110
+ readonly min: 1;
111
+ readonly max: number;
112
+ };
113
+ readonly maxJsonRefBytes: {
114
+ readonly min: 1;
115
+ readonly max: number;
116
+ };
117
+ };
86
118
  /** `/apply` 的五个动作。id 的含义随动作走:`delete`/`count`/`orphan-clean` 是**宿主会话 id**
87
119
  * (前两个还没进回收站,第三个压根没有回收站行),`restore`/`purge` 是**回收站条目 id**。
88
120
  * 两域都是 string(线上形状);这里不引入品牌类型——错域不靠类型拦,交给各动词在运行期按自己的
@@ -1,3 +1,4 @@
1
+ import { isAbsentPath } from '../ports/fs-port.js';
1
2
  import type { FsPort } from '../ports/fs-port.js';
2
3
  import type { SessionDeleteConfig } from '../config.js';
3
4
  import { type AttachmentCleanupWire } from '../shared/wire.js';
@@ -22,8 +23,10 @@ export type AttachmentRef = ImageRef | FileRef;
22
23
  * 假支若按「是 string」取反会被 TS 收成 `never`(参数本来就是 string),插值就成了 never 读数。
23
24
  * 这里判的仍是同一条正则,谓词只是把「形状」写进类型面。 */
24
25
  export declare function isAttachmentId(value: unknown): value is `sha256:${string}`;
25
- /** `statFile` / `readFile` / `sizeOfDir` 里只有 ENOENT 能当"不在";权限、盘挂、I/O 错误都必须露成读不出。 */
26
- export declare function isMissing(error: unknown): boolean;
26
+ /** 路径缺席的判据只有一处:`src/ports/fs-port.ts` 的 `isAbsentPath`(ENOENT / ENOTDIR)。
27
+ * `statFile` / `readFile` / `sizeOfDir` 的读错误里,"不在"只认这两枚;权限、盘挂、I/O 错误
28
+ * 都必须露成读不出(ADR-0039),别在这里另写一份只有 ENOENT 的抄本。 */
29
+ export declare const isMissing: typeof isAbsentPath;
27
30
  /** 合法附件 ID → 宿主附件库里的绝对路径。`name` 不参与拼接。 */
28
31
  export declare function attachmentPathOf(home: string, kind: 'image' | 'file', id: string): string;
29
32
  /** 一条消息的 `content[]` 里的附件块;畸形 id / 非附件段一律跳过。 */
@@ -67,6 +70,16 @@ export interface ImageRemovalPlan {
67
70
  * 剩余引用源读不出 ⇒ 整体跳过;候选日志读不出 / 附件对象 stat 失败 ⇒ 只跳过那一部分,其余照常。
68
71
  */
69
72
  export declare function planImageAttachmentRemoval(deps: AttachmentReadDeps, removedDirs: readonly string[]): Promise<ImageRemovalPlan>;
73
+ /**
74
+ * 执行期复核:计划与真删对象之间隔着一整趟目录删除(秒级),这期间别的会话可能把同一张图
75
+ * (内容寻址 ⇒ 同一个 sha256 id)再引用一次,或派生存储新写一条引用。计划是数据、会过期;
76
+ * 删对象之前用同一套剩余引用扫描再判一次,**只删这一刻仍无引用的那些**(ADR-0041)。
77
+ *
78
+ * 调用时机:会话 / 回收站目录已经删掉之后、`executeImageAttachmentRemoval` 之前——那时被删的
79
+ * 引用源已经不在了,扫到的就是真正的"剩余"。读不出照旧 fail closed:回空计划 + `skipped`,
80
+ * 宁可留下孤儿也不删共享。空计划直接返回(不为零删付一次全 home 扫描)。
81
+ */
82
+ export declare function recheckImageRemovalExclusivity(deps: AttachmentReadDeps, plan: ImageRemovalPlan): Promise<ImageRemovalPlan>;
70
83
  /**
71
84
  * 逐文件删;ENOENT 由端口的 `force` 吞掉;其他错误计数并继续。
72
85
  *
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `/config` 的读/写动词:三只可调项的当前值与写回。
3
+ *
4
+ * 读是免费快照(`apply` 归一后的那一份),写才碰宿主:
5
+ * - 先按 `src/config.ts` 的 `normalizeConfig` 校验(未知键 / 非整数 / 越界回 400,不拿半套值写盘);
6
+ * - 再走宿主 `configEditor.edit(entry, change)`:完整 raw config 落进 profile 的 patch 层,
7
+ * 由编辑器串行化、校验并让这一行重启(字段不是 volatile,重启是它给的语义)。
8
+ * - `configEditor` / `entry` 缺席(老宿主、嵌套上下文)回 `io` 并在原文里说清替代路径——
9
+ * 不静默假装保存成功。
10
+ *
11
+ * `entry` 由组合根从 `ctx.fiber.entry` 取(见 `src/index.ts`);本文件不碰 ctx。
12
+ */
13
+ import { type SessionDeleteConfig } from '../config.js';
14
+ import type { ConfigEditorLike } from '../deps.js';
15
+ /** `/config` 的写支需要的三件;读支只要 `config`。 */
16
+ export interface ConfigRouteDeps {
17
+ readonly config: SessionDeleteConfig;
18
+ readonly configEditor?: ConfigEditorLike;
19
+ readonly entry?: unknown;
20
+ }
21
+ /** `/config` 的请求体:`null` 或 `{}` 是读;只要带一个键就当写(未知键由 {@link normalizeConfig} 400 拒掉)。
22
+ * 路由表按同一句话定纯度,所以别在别处再写一遍这个判据。 */
23
+ export declare function configPatchOf(body: Record<string, unknown> | null): Record<string, unknown> | undefined;
24
+ /** 空体 = 读当前值;带字段 = 写。请求体已经由适配器解析成对象或 null。 */
25
+ export declare function readOrSaveConfig(deps: ConfigRouteDeps, body: Record<string, unknown> | null): SessionDeleteConfig | Promise<SessionDeleteConfig>;
@@ -21,5 +21,7 @@ export declare function routeError(code: RouteCode, httpStatus: number, message:
21
21
  export declare function rollbackReason(label: string, error: unknown): string;
22
22
  /** 原错误保留,回滚失败作为 `details.rollbackFailures` 上线;码仍按原错误的动词码,
23
23
  * 没有可继承的就落 io。**内层的回滚失败要并进来**:一个成员自己退失败、兄弟也退失败时,
24
- * 两份失败都得在 details 里,消息也不能出现第二个「回滚未完成」。 */
24
+ * 两份失败都得在 details 里,消息也不能出现第二个「回滚未完成」。
25
+ * **原错误 details 的其余键原样保留**:孤儿清理的部分回执(`removed` / `freedBytes` /
26
+ * `failedSessionId`)就挂在它上面,整份替换会把已经发生的删除报成 0。 */
25
27
  export declare function withRollbackFailures(error: unknown, failures: readonly string[]): VerbError;
@@ -130,15 +130,20 @@ export declare function planOrphanRemoval(deps: CoreDeps, members: readonly {
130
130
  *
131
131
  * 顺序:整批摘归档集 → 摘有账本的成员的账本 → 逐条回收站行
132
132
  * (在才删目录 → 摘清册行 → 通告成员离场)→ 逐条盘上成员(删目录 → 通告成员离场)→ 逐条附件对象。
133
- * 目录删掉就没有"退回去"这回事,中途失败**不回滚**;已经删掉的由下面的计数器如实交回。
134
- *
135
- * 附件清理排在**所有会话目录删完之后**:对象只在会话真的没了之后才动;单文件失败只进
136
- * `attachments.failed`,不影响(也不回滚)会话删除的回执。
133
+ * 目录删掉就没有"退回去"这回事,中途失败**不回滚目录**;已经删掉的由下面的计数器如实交回。
134
+ * **归档态回滚**:`unarchive` 排在所有删除之前(免得留下"会话没了、归档集还挂着"的幽灵 id),
135
+ * 所以中途失败时目录还在、归档位却已经摘了。失败出口把**目录还在**的那些(这一趟没删掉、也不是计划时
136
+ * 就缺席)按删除前的读数重新归档(`isArchived` 先读过一次,不是按 `wasArchived` 猜);目录不在的跳过
137
+ * ——那时恢复归档只会造幽灵 id。
138
+ * 回滚自己的失败走 `details.rollbackFailures`,不让它把原错误吞掉(ADR-0040)。
139
+ * **附件计划在执行期复核**:计划到删对象之间的窗口里新增的引用会让计划过期;删对象前用同一套
140
+ * 剩余引用扫描重判一次,只删这一刻仍无引用的对象(ADR-0041)。
137
141
  *
138
142
  * 失败形状:
139
143
  * - `kind:'orphan'`(orphan-clean)⇒ `io` + `{removed, freedBytes, failedSessionId}`,不伪装成整组没动;
140
144
  * - `kind:'trash'`(purge)⇒ 同一个标签、不带 details;
141
- * - orphan-clean 的 unarchive / detach 原样抛出。
145
+ * - orphan-clean 的归档实况预读 / unarchive / detach 原样抛出;
146
+ * - 归档态回滚不干净时,原错误带 `details.rollbackFailures`(码仍按原错误的动词码)。
142
147
  *
143
148
  * 通告只经 `member-presence`(摘活体三态 + `api-session/removed`),不在这里另写一份。
144
149
  */
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Session Delete",
3
+ "title": "Session delete",
4
4
  "description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "meta": {
3
- "title": "会话删除",
4
- "description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
5
- }
6
- }
1
+ {
2
+ "meta": {
3
+ "title": "会话删除",
4
+ "description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrn1997/dsh-session-delete",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "在 DeepSeek Harness 里删除会话:删除=移入回收站(可恢复),另设显式彻底删除",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.25.0",
@@ -27,7 +27,9 @@
27
27
  "icon.svg",
28
28
  "cordis.patch.yml",
29
29
  "README.md",
30
- "LICENSE"
30
+ "LICENSE",
31
+ "THIRD_PARTY_NOTICES.md",
32
+ "docs/screenshots"
31
33
  ],
32
34
  "engines": {
33
35
  "node": ">=24",
@@ -44,8 +46,16 @@
44
46
  "gates": "pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm run pack:dry"
45
47
  },
46
48
  "peerDependencies": {
47
- "@deepseek-ai/cordis": "^4.0.4",
48
- "react": "^18.3.1"
49
+ "react": "^18.3.1",
50
+ "react-dom": "18.3.1"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "react": {
54
+ "optional": true
55
+ },
56
+ "react-dom": {
57
+ "optional": true
58
+ }
49
59
  },
50
60
  "devDependencies": {
51
61
  "@deepseek-ai/cordis": "^4.0.4",
@@ -63,6 +73,8 @@
63
73
  "jsdom": "^26.1.0",
64
74
  "react": "^18.3.1",
65
75
  "react-dom": "18.3.1",
76
+ "react-markdown": "^10.1.0",
77
+ "remark-gfm": "^4.0.1",
66
78
  "semver": "^7.8.5",
67
79
  "tsdown": "0.23.0",
68
80
  "typescript": "5.9.3",
@@ -70,23 +82,26 @@
70
82
  "vitest": "3.2.7"
71
83
  },
72
84
  "dsh": {
85
+ "manifestVersion": 1,
73
86
  "bundle": {
74
87
  "patch": "./cordis.patch.yml"
75
88
  },
76
89
  "client": {
77
90
  "inject": [
78
- "@deepseek-ai/dsh-client-locale"
91
+ "@deepseek-ai/dsh-client-locale",
92
+ "@deepseek-ai/dsh-client-ui-context-menu"
79
93
  ],
80
94
  "platform": "web"
81
- },
82
- "compatibility": {
83
- "dshReleases": {
84
- "0.2.0-rc.2": "compatible",
85
- "0.2.1-rc.1": "compatible",
86
- "0.2.1-rc.2": "compatible",
87
- "0.2.1-rc.3": "compatible",
88
- "0.2.1-rc.5": "compatible"
89
- }
95
+ }
96
+ },
97
+ "xrn1997": {
98
+ "dshReleases": {
99
+ "0.2.0-rc.2": "compatible",
100
+ "0.2.1-rc.1": "compatible",
101
+ "0.2.1-rc.2": "compatible",
102
+ "0.2.1-rc.3": "compatible",
103
+ "0.2.1-rc.5": "compatible",
104
+ "0.2.2-rc.1": "compatible"
90
105
  }
91
106
  },
92
107
  "publishConfig": {
@@ -109,9 +124,5 @@
109
124
  "delete"
110
125
  ],
111
126
  "author": "xrn1997",
112
- "license": "Apache-2.0",
113
- "dependencies": {
114
- "react-markdown": "^10.1.0",
115
- "remark-gfm": "^4.0.1"
116
- }
127
+ "license": "Apache-2.0"
117
128
  }