@xrn1997/dsh-session-delete 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +10 -28
  2. package/lib/client.js +2012 -1117
  3. package/lib/index.js +1271 -893
  4. package/lib/types/api/dispatch.d.ts +2 -2
  5. package/lib/types/api/route-table.d.ts +1 -1
  6. package/lib/types/api/trash-rows.d.ts +13 -6
  7. package/lib/types/client/batch-bar.d.ts +7 -6
  8. package/lib/types/client/batch-confirm.d.ts +6 -4
  9. package/lib/types/client/batch-menu-item.d.ts +2 -2
  10. package/lib/types/client/batch-select.d.ts +7 -50
  11. package/lib/types/client/batch-selection-store.d.ts +30 -0
  12. package/lib/types/client/busy-gate.d.ts +6 -0
  13. package/lib/types/client/copy.d.ts +4 -1
  14. package/lib/types/client/delete-confirm.d.ts +2 -0
  15. package/lib/types/client/host-rows.d.ts +50 -0
  16. package/lib/types/client/main-view.d.ts +42 -0
  17. package/lib/types/client/orphan-maintenance.d.ts +27 -0
  18. package/lib/types/client/receipt.d.ts +73 -0
  19. package/lib/types/client/remote.d.ts +25 -19
  20. package/lib/types/client/store.d.ts +48 -0
  21. package/lib/types/client/trash-nav.d.ts +30 -0
  22. package/lib/types/client/trash-panel.d.ts +1 -1
  23. package/lib/types/client/trash-store.d.ts +2 -0
  24. package/lib/types/client/ui/icons.d.ts +3 -3
  25. package/lib/types/client/undo-toast.d.ts +30 -6
  26. package/lib/types/client/use-column-resize.d.ts +2 -2
  27. package/lib/types/client/use-panel-width.d.ts +1 -1
  28. package/lib/types/client/use-trash-panel-controller.d.ts +42 -0
  29. package/lib/types/client/use-trash-query.d.ts +20 -9
  30. package/lib/types/client/write-seats.d.ts +52 -0
  31. package/lib/types/client/write-settlement.d.ts +72 -0
  32. package/lib/types/client/write.d.ts +51 -0
  33. package/lib/types/deps.d.ts +14 -0
  34. package/lib/types/index.d.ts +7 -13
  35. package/lib/types/log/zstd-frames.d.ts +1 -1
  36. package/lib/types/ports/agent-handles.d.ts +25 -0
  37. package/lib/types/ports/home.d.ts +19 -0
  38. package/lib/types/ports/host-port.d.ts +60 -44
  39. package/lib/types/remote/map-error.d.ts +2 -2
  40. package/lib/types/shared/wire.d.ts +18 -9
  41. package/lib/types/trash/detail.d.ts +6 -3
  42. package/lib/types/trash/entry.d.ts +4 -4
  43. package/lib/types/trash/groups.d.ts +17 -6
  44. package/lib/types/trash/ledger.d.ts +20 -0
  45. package/lib/types/trash/manifest.d.ts +4 -2
  46. package/lib/types/trash/reconcile.d.ts +6 -5
  47. package/lib/types/trash/storage-manifest.d.ts +12 -0
  48. package/lib/types/verbs/batch-carried.d.ts +20 -0
  49. package/lib/types/verbs/batch.d.ts +1 -1
  50. package/lib/types/verbs/delete-session.d.ts +11 -13
  51. package/lib/types/verbs/errors.d.ts +2 -6
  52. package/lib/types/verbs/live-admission.d.ts +13 -0
  53. package/lib/types/verbs/member-journal.d.ts +26 -6
  54. package/lib/types/verbs/member-presence.d.ts +67 -0
  55. package/lib/types/verbs/move-group.d.ts +20 -0
  56. package/lib/types/verbs/orphan-subagents.d.ts +21 -40
  57. package/lib/types/verbs/permanent-removal.d.ts +114 -0
  58. package/lib/types/verbs/purge-trash.d.ts +21 -9
  59. package/lib/types/verbs/restore-session.d.ts +14 -12
  60. package/package.json +3 -2
@@ -0,0 +1,30 @@
1
+ import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
2
+ import type { TrashEntryLike } from './remote.js';
3
+ /**
4
+ * 导航要的面。`ordered` 是**屏幕上的顺序**(只含展开的组),由面板/选择面算好传进来——
5
+ * 布局常量与本 module 无关。`drillEnabled` 是这一档支不支持整行下钻(窄档为真;宽档 Enter/Space
6
+ * 只移动选中,与整行点击的 `{ drill: !split }` 同一条判据)。四个动作都由调用方注入:
7
+ * `select` 只移动选中(带 `drill` 才下钻),`exitDrill` 是控制器那条「← 返回清单」的同一动作,
8
+ * `focusRow` 把焦点交给某一行。
9
+ */
10
+ export interface TrashNavInput {
11
+ ordered: readonly TrashEntryLike[];
12
+ selectedId: string | null;
13
+ selectMode: boolean;
14
+ drillEnabled: boolean;
15
+ drilled: boolean;
16
+ select(entry: TrashEntryLike, options?: {
17
+ drill?: boolean;
18
+ }): Promise<void>;
19
+ toggleCheck(entryId: string): void;
20
+ exitDrill(): void;
21
+ focusRow(entryId: string): void;
22
+ }
23
+ export interface TrashNav {
24
+ /** `aria-activedescendant` 的那条 id:选中行不在 DOM 里时是 `null`。 */
25
+ readonly activeDescendantId: string | null;
26
+ /** 清单里唯一拿着 `tabIndex=0` 的那一行;清单空时是 `null`。 */
27
+ readonly tabStopId: string | null;
28
+ onListKeyDown(event: ReactKeyboardEvent<HTMLElement>): void;
29
+ }
30
+ export declare function useTrashNav(input: TrashNavInput): TrashNav;
@@ -1,7 +1,7 @@
1
1
  import { type Translate } from './copy.js';
2
2
  import { type SessionDeleteRemote } from './remote.js';
3
3
  /** 面板的依赖面:三个读(`list` / `detail` / `scanOrphans`)+ **一段 `apply`** + 清空。
4
- * **没有第二条路**(架构复核候选 6):「恢复一条」也走 `apply`(N=1),面板不再同时吃
4
+ * **没有第二条路**:「恢复一条」也走 `apply`(N=1),面板不再同时吃
5
5
  * `applyOutcomes` 与一个便捷的 `restore`。 `scanOrphans` 是回收站之外那批无主子会话的纯读报告。 */
6
6
  export type TrashPanelDeps = Pick<SessionDeleteRemote, 'list' | 'detail' | 'applyOutcomes' | 'emptyTrash' | 'scanOrphans'>;
7
7
  export interface TrashPanelProps {
@@ -2,3 +2,5 @@
2
2
  export declare function invalidateTrash(): void;
3
3
  /** 供面板订阅。 */
4
4
  export declare function useTrashRevision(): number;
5
+ /** 测试专用:修订号归零(订阅者随组件卸载自行退订)。 */
6
+ export declare function resetTrashStore(): void;
@@ -3,8 +3,8 @@
3
3
  * 随包发布的明文产物 `lib/index.js`;出处与改写规则见 `ui.css` 头注)。
4
4
  *
5
5
  * 只留本插件用到的:垃圾桶(删除动作 / 回收站)、警示(失败面)、打开的项目文件夹、
6
- * 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17
7
- * 第十三批加;行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
6
+ * 关闭叉(对话框)、圆圈对勾(撤销提示的 success 档)、对勾(行尾兜底勾选开关,2026-10-17 加;
7
+ * 行首主勾选走原生 Checkbox)、下折与右折 chevron(清单的分组折叠头,
8
8
  * 2026-10-15 加:`DisclosureRow` 那套折叠行的两态 glyph)。「刷新」那枚随按钮一起删了
9
9
  * (2026-10-16,见 `trash-panel.tsx` 工具排的注释)——本文件不留无人消费的几何。
10
10
  * **几何、`viewBox`、`strokeWidth` 逐字不动**——它们是官方图标集的形状,改了就不再与宿主同屏同形。
@@ -25,7 +25,7 @@ export declare function IconFolderOpenOutlineRegular({ size }: IconProps): React
25
25
  export declare function IconCloseOutlineRegular({ size }: IconProps): ReactElement;
26
26
  /** 撤销提示 success 档的前导 glyph(官方 `Toast` 在 `tone="success"` 时自带的那一枚)。 */
27
27
  export declare function IconCheckCircleOutlineRegular({ size }: IconProps): ReactElement;
28
- /** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 第十三批加)。 */
28
+ /** 行尾兜底「勾选开关」那枚对勾(状态点/归档行;官方 `IconCheckOutlineRegular`,`viewBox`/`d` 逐字;2026-10-17 加)。 */
29
29
  export declare function IconCheckOutlineRegular({ size }: IconProps): ReactElement;
30
30
  /** 分组头「展开中」那一枚(官方 `IconChevronDownOutlineRegular`,`viewBox`/`d` 逐字)。 */
31
31
  export declare function IconChevronDownOutlineRegular({ size }: IconProps): ReactElement;
@@ -1,9 +1,24 @@
1
+ /**
2
+ * 撤销提示(docs/ui-spec.md 状态 ③):删除成功后一条可撤销的横幅。
3
+ *
4
+ * 为什么是**独立组件 + 模块级通知台**,而不是死在菜单行里:宿主的菜单行随菜单**一起卸载**
5
+ * (真产物 Menu 的注释原文 "the rows unmount with the list"),而提示要在菜单关掉之后还活着。
6
+ * 宿主的官方配方同形——会话行的动作把请求写进一个模块级 store,`shell.overlay` 里的
7
+ * `workspace.row-toast` 读它(asar 原文:`inject("shell.overlay", … id: "workspace.row-toast" …
8
+ * inject: rowToastInjected … }, RowActionToast)`,而
9
+ * `rowToastInjected = () => ({ hooks: { toast: rowToast }, dismissToast, undoArchive: unarchiveSession })`)。
10
+ * 本文件就是那份 store + 那个常驻组件。
11
+ *
12
+ * 状态源走 `store.ts` 的 `createStore`(内部用 React 自带的 `useSyncExternalStore`)——
13
+ * 不引 `@deepseek-ai/dsh-client-store`(它在平台表内,但这里用不着;React 就够,少一条平台依赖)。
14
+ */
1
15
  import { type Translate } from './copy.js';
2
- import { type OneShotDeps } from './remote.js';
16
+ import type { OneShotDeps } from './remote.js';
3
17
  export type UndoToastDeps = OneShotDeps;
4
18
  /**
5
- * 一次待撤销的事。两形(第十三批②起):单条删除(主语是会话名)/批量删除(主语是条数)。
6
- * 同一个 Toast 宿主、同一停留时长;撤销动作按形走——单条 `restore`,批量**一次** `applyOutcomes`。
19
+ * 一次待撤销的事。两形(2026-10-17 起):单条删除(主语是会话名)/批量删除(主语是条数)。
20
+ * 同一个 Toast 宿主、同一停留时长;撤销动作按形分派(单条 / 批量),两条都走同一个外部恢复座位
21
+ * (`write-seats.ts` 的 `restoreFromUndo`),单条带 `single: true` 的读法(null 是协议错,不冒充幂等成功)。
7
22
  */
8
23
  export type UndoNotice = {
9
24
  /** 每次发布递增:Toast 靠它重挂载,同一段文案连续出现两次也能重新计一次停留。 */
@@ -28,16 +43,25 @@ export declare function publishUndoNotice(input: {
28
43
  entryId: string;
29
44
  title: string;
30
45
  }): void;
31
- /** 批量删除成功后发布一条撤销提示(第十三批②):覆盖上一条,只报这一次移走的条数。 */
46
+ /** 批量删除成功后发布一条撤销提示(2026-10-17):覆盖上一条,只报这一次移走的条数。 */
32
47
  export declare function publishUndoBatchNotice(input: {
33
48
  entryIds: readonly string[];
34
49
  n: number;
35
50
  }): void;
36
- /** 撤销成功后把提示收掉(用户点「撤销」、或横幅自己走完停留时间)。 */
51
+ /** 整条横幅不再留:撤销成了、停留时间走完,或清空回收站把所有落点一起清了。
52
+ * 与下面 `dismissUndoNotice` 的分别是「整条不再留」与「按条目匹配」——清空之后没有条目还能撤销,
53
+ * 所以那一路不需要名单。 */
37
54
  export declare function clearUndoNotice(): void;
55
+ /**
56
+ * 某个条目已经在别处恢复:把指向它的横幅熄掉。
57
+ * 批量里任一命中就清整条——名单少了、计数没变,留着就是一条会骗人的「撤销」动作。
58
+ */
59
+ export declare function dismissUndoNotice(entryIds: readonly string[]): void;
38
60
  /** 供组件订阅。 */
39
61
  export declare function useUndoNotice(): UndoNotice | null;
40
- /** 提示停留时长。设计稿 §7 定死:**6 秒**(Toast 自己的默认是 3 秒)。 */
62
+ /** 测试专用:现场与 seq 归零,用例之间互不串味。 */
63
+ export declare function resetUndoNotice(): void;
64
+ /** 提示停留时长。**6 秒** 定死在本行(比 Toast 默认的 3 秒长)——`docs/ui-spec.md` §2 状态 ③ 只指到这里,不抄这个数。 */
41
65
  export declare const UNDO_HOLD_MS = 6000;
42
66
  /**
43
67
  * 常驻的撤销提示宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,设计稿 §7 状态④)。
2
+ * 清单列与详情列之间那根**可拖的宽度把手**(2026-10-15 加,`docs/ui-spec.md` 的「面板与清单」)。
3
3
  *
4
4
  * 三件事住在这里,因为它们互相咬合、分开测各自都不成立:
5
5
  * ① **宽度夹在两个界之间**。下限由调用方给(回收站那条的来历写在 `trash-panel.tsx`:真 Chromium
6
6
  * 上扫出来的,约束不是"两颗按钮放得下"——它们 `flex:none`,再窄也是文字列缩;而是「删除于 … ·
7
7
  * 大小」那一行不再被省略号截断),上限 = 面板宽 − `reserve`。**`reserve` 的数值与来历都住在调用方**
8
- * (`DETAIL_RESERVE`,那三件东西怎么加出来的见设计稿 2026-10-15 §7.1 与现状稿 §8 的几何读数):
8
+ * (`DETAIL_RESERVE`,那三件东西怎么加出来的见 `trash-panel.tsx` 该常量的注释与几何用例):
9
9
  * 这一层不复制那份算式——上一版在这里钉过一个"480 ⇒ 900 那档正好回 420",调用方改到 489 之后
10
10
  * 它就成了假数,而 900 那档的真实读数是**清单被夹到 411**(比默认窄 9px,还能往里拖)。
11
11
  * ② **夹在"读"的时候也做一次**。窗口变窄不触发任何写入,若只在写入时夹,之前拖出来的超宽
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 观测**面板自己那一块**的宽度(回收站的断点全靠它,设计稿 2026-10-15 §2.1)。
2
+ * 观测**面板自己那一块**的宽度(回收站的断点全靠它,docs/ui-spec.md)。
3
3
  *
4
4
  * 为什么不是 `window.innerWidth`:这块住在宿主的中央列里,**侧栏折叠/展开、拖动分栏**都会改它的宽度
5
5
  * 而 window 一点不动——只听 window 的 resize 会在侧栏折叠后拿着旧宽度画错形态。
@@ -0,0 +1,42 @@
1
+ import type { BusyGate } from './busy-gate.js';
2
+ import { type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
3
+ import type { PendingAction } from './trash-confirm.js';
4
+ import type { TrashDetailState } from './trash-detail.js';
5
+ import type { TrashQuery } from './use-trash-query.js';
6
+ /**
7
+ * 控制器要的面。`query` 取四样:清单(选中从它里面找)、`refreshAll`(挂载与显式重试)、
8
+ * `refreshAfterWrite`(失效与写后)、清空(面板的写动作那一半)。`ordered` 是键盘/自动选中用的
9
+ * "屏幕上的顺序"(只含展开的组),
10
+ * 由面板算好传进来——布局常量不搬进控制器。
11
+ */
12
+ export interface TrashPanelControllerInput {
13
+ deps: Pick<SessionDeleteRemote, 'detail' | 'applyOutcomes'>;
14
+ gate: BusyGate;
15
+ query: Pick<TrashQuery, 'entries' | 'refreshAll' | 'refreshAfterWrite' | 'emptyTrash'>;
16
+ ordered: readonly TrashEntryLike[];
17
+ canAutoSelect: boolean;
18
+ }
19
+ export interface TrashPanelController {
20
+ readonly busy: boolean;
21
+ readonly selected: TrashEntryLike | null;
22
+ readonly selectedId: string | null;
23
+ readonly drilled: boolean;
24
+ readonly detail: TrashDetailState;
25
+ /** 待确认的那一批(确认框的开合);确认落地后由控制器收起。 */
26
+ readonly pending: PendingAction | null;
27
+ /** 恢复/彻底删除/批量 的失败原文,按条目 id 存:切走再回来还在。 */
28
+ readonly actionFailure: Readonly<Record<string, string>>;
29
+ select(entry: TrashEntryLike, options?: {
30
+ drill?: boolean;
31
+ }): Promise<void>;
32
+ retry(): void;
33
+ back(): void;
34
+ setPending(pending: PendingAction | null): void;
35
+ /** 单条恢复(`/apply` 的 N=1):成功后丢缓存,写完无论成败都重读清单,失败原文落回这一行。 */
36
+ restore(entry: TrashEntryLike): Promise<void>;
37
+ /** 确认框上的「确认」:按 `pending.kind` 分派到空清或 `confirmApply`。 */
38
+ confirmPurge(): Promise<void>;
39
+ /** 一组 id 上的同一个动作:逐条成败落成行上原文/缓存失效,整批只重读一次清单。 */
40
+ confirmApply(action: 'purge' | 'restore', ids: readonly string[]): Promise<void>;
41
+ }
42
+ export declare function useTrashPanelController(input: TrashPanelControllerInput): TrashPanelController;
@@ -1,6 +1,11 @@
1
+ import type { BusyGate } from './busy-gate.js';
1
2
  import { type OrphanReportWire, type SessionDeleteRemote, type TrashEntryLike } from './remote.js';
3
+ import type { WriteOutcome } from './write.js';
2
4
  import type { Translate } from './copy.js';
5
+ import type { ApplyAction } from '../shared/wire.js';
3
6
  export type TrashQueryDeps = Pick<SessionDeleteRemote, 'list' | 'scanOrphans' | 'applyOutcomes' | 'emptyTrash'>;
7
+ /** 一次清单读的结局:`stale` = 被后发的读顶掉(什么都没落地,调用方不许拿它当失败清场)。 */
8
+ export type ReloadResult = 'ok' | 'failed' | 'stale';
4
9
  export interface TrashQuery {
5
10
  /** `null` = 还没读到(第一次读或读失败);与"空的"是两件事。 */
6
11
  readonly entries: readonly TrashEntryLike[] | null;
@@ -14,19 +19,25 @@ export interface TrashQuery {
14
19
  setOrphanConfirm(open: boolean): void;
15
20
  /** 「查看并清理」:把上一条失败/中性提示清掉再开预览框(旧提示不该跟新一次预览并排出现)。 */
16
21
  openOrphanPreview(): void;
17
- /** 交回**这一次读数是否真的落地**:孤儿扫描吃的是对账后的清册,清单没读成就不能扫。 */
18
- reload(): Promise<boolean>;
19
- scanOrphans(): Promise<void>;
20
- /** "重读一遍" = 先对账并读清单,再扫残留;清单没读成就不扫。 */
21
- refresh(): Promise<void>;
22
+ /** 只重读清单(对账 + 读)。交回**这一次读的结局**:落地 / 读失败 / 被后发的读顶掉。
23
+ * 残留报告与提示的联动不在这里;组合清场归 `refreshAll` / `refreshAfterWrite`。 */
24
+ reloadRows(): Promise<ReloadResult>;
25
+ /** 只扫残留。调用方负责先让清单对过账(`refreshAll` / `refreshAfterWrite` 已经排好序)。 */
26
+ rescanOrphans(): Promise<void>;
27
+ /** 先读清单、成了再扫残留;挂载与显式「重试」用它。 */
28
+ refreshAll(): Promise<void>;
29
+ /** 写后/失效:只重读清单;是否顺带重扫残留由这次的 `outcome` 从 action / ids / 当前清单自行派生
30
+ * (判据住 `needsOrphanRescan`)。失效通告不带 outcome:报告未知或非空才重扫。输给后发读时由赢家补扫;
31
+ * 清单读失败时清空报告与提示。 */
32
+ refreshAfterWrite(outcome?: WriteOutcome<ApplyAction>): Promise<void>;
22
33
  /** 清理那批无主子会话(一段 `/apply` 的第五个动作),逐条结果落成"已不在了 / N 组未清理"。 */
23
34
  cleanOrphans(): Promise<void>;
24
- /** 清空回收站(聚合回执):成功就重读,失败就把原文摆到清单那块。交回是否成功。 */
35
+ /** 清空回收站(聚合回执):成功就熄掉撤销横幅并重读,失败就把原文摆到清单那块。交回是否成功。 */
25
36
  emptyTrash(): Promise<boolean>;
26
37
  }
27
- export declare function useTrashQuery({ deps, t, around, }: {
38
+ export declare function useTrashQuery({ deps, t, gate, }: {
28
39
  deps: TrashQueryDeps;
29
40
  t: Translate;
30
- /** 把"这一趟还算忙"的一枚全局闸套在一次动作外(面板的 `busy`)。 */
31
- around: <T>(task: () => Promise<T>) => Promise<T>;
41
+ /** 面板的一枚全局闸:清空与无主子会话清理这两条写动作把整趟包在里面(面板的 `busy`)。 */
42
+ gate: BusyGate;
32
43
  }): TrashQuery;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 逐条 `/apply` 的**写入座位**:一次写入发生在哪个界面位置,就在哪个函数里把呈现材质、闸与重读通道绑好。
3
+ *
4
+ * 为什么要有它:六个调用点此前各自拼 `runWrite({ presentation: {…} })`,并各自决定要不要外包一层闸。
5
+ * 收尾顺序(`write-settlement.ts`,ADR-0013)与重读策略(`use-trash-query.ts`,ADR-0016)不动;
6
+ * 座位只拥有「这次是谁在写、交哪些材质、闸在哪」。
7
+ *
8
+ * 边界:不判回执(`receipt.ts`)、不决定收尾顺序、不拥有各面的本地状态(`pending` / 详情缓存 / 行失败)。
9
+ * 面板座位的闸由面板传入:`busy` 是它的 UI 状态,座位不另造第二枚。
10
+ */
11
+ import type { BusyGate } from './busy-gate.js';
12
+ import type { OneShotDeps, TrashEntryLike } from './remote.js';
13
+ import { type WriteOutcome } from './write.js';
14
+ /** 会话行菜单的单删座位:一条会话、`single` 读法、成功发单条撤销横幅。交回这条会话的失败原文。 */
15
+ export declare function deleteSessionFromMenu(deps: OneShotDeps, input: {
16
+ sessionId: string;
17
+ title: string;
18
+ }, gate: BusyGate): Promise<string | null>;
19
+ /** 批量删除座位(宿主会话列表的多选 → 批量确认框):一次 `/apply`,撤销名单只收 ok 支的条目 id(跟着组走的幂等条不单列)。
20
+ * 只收 id:标题是浮条的呈现,不参与这一次写入。 */
21
+ export declare function deleteSessionsFromBatch(deps: OneShotDeps, ids: readonly string[], gate: BusyGate): Promise<ReadonlyMap<string, string>>;
22
+ /** 外部恢复座位(撤销横幅):没有 busy 面,不传闸;主视图重开归 settlement,这里只让清单失效。 */
23
+ export declare function restoreFromUndo(deps: OneShotDeps, input: {
24
+ ids: readonly string[];
25
+ single: boolean;
26
+ }): Promise<WriteOutcome<'restore'>>;
27
+ /** 面板两个 `/apply` 座位共用的那一半:依赖、条目 id、闸、重读与缓存失效。 */
28
+ interface PanelWriteInput<A extends 'restore' | 'purge'> {
29
+ deps: OneShotDeps;
30
+ ids: readonly string[];
31
+ gate: BusyGate;
32
+ refreshAfterWrite(outcome: WriteOutcome<A>): Promise<void>;
33
+ clearDetailCache(ids: readonly string[]): void;
34
+ }
35
+ /** 面板恢复另要两样:单条读法,以及 already-gone 回执缺行时反查 sessionId 的当前清单。 */
36
+ interface PanelRestoreInput extends PanelWriteInput<'restore'> {
37
+ single?: boolean;
38
+ entries: readonly TrashEntryLike[] | null;
39
+ }
40
+ /** 面板恢复座位:面板已替这条完成「回来」的结算 ⇒ `panelSeat`;撤销横幅与详情缓存都按落地条目收。 */
41
+ export declare function restoreFromPanel(input: PanelRestoreInput): Promise<WriteOutcome<'restore'>>;
42
+ /** 面板彻底删除座位:永久删除也让指向这些条目的撤销横幅熄灭(settlement 的 purge 支负责)。 */
43
+ export declare function purgeFromPanel(input: PanelWriteInput<'purge'>): Promise<WriteOutcome<'purge'>>;
44
+ interface OrphanCleanSeatInput {
45
+ deps: OneShotDeps;
46
+ ids: readonly string[];
47
+ gate: BusyGate;
48
+ refreshAfterWrite(outcome: WriteOutcome<'orphan-clean'>): Promise<void>;
49
+ }
50
+ /** 无主子会话清理座位:不碰回收站失效与撤销,回执判读仍归 `use-trash-query.ts`。 */
51
+ export declare function cleanOrphansFromPanel(input: OrphanCleanSeatInput): Promise<WriteOutcome<'orphan-clean'>>;
52
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 一次逐条 `/apply` 写入成功后的**收尾顺序**,按 action 各一条固定序列。
3
+ *
4
+ * 调用方只交呈现材质(`WritePresentation`),顺序与 `anySuccess` 门只住这里;`runWrite` 在拿到
5
+ * `WriteSuccess` 后、`refresh` 前调一次。为什么按 action 收成一处见 ADR-0013。
6
+ *
7
+ * 四条判据:
8
+ * - **跨槽位在信封成功时就跑**:`afterDelete` / `afterRestore` 与 `invalidate` 不管逐条成败都跑
9
+ * (回滚失败可能留下半套,回收站清单不能继续摆旧行);呈现钩子(发布/熄灭撤销、遗忘主视图、
10
+ * 清详情缓存)只在至少一条落地(`anySuccess`)时跑——没有落点的撤销是假动作。delete 的撤销
11
+ * 另要求落地名单非空:全部被同批前一条带走时没有可撤销的条目。
12
+ * - **面板恢复用 `panelSeat: true` 明说**(「缺席」不再承载语义):外部恢复(撤销横幅)由 settlement
13
+ * 调 `afterRestore` 重开;面板恢复自带「回来」的结算,只 `forgetMainView`、不重开、不抢焦点。
14
+ * 判据住 `src/client/main-view.ts` 的 `forgetMainView`。
15
+ * - **撤销名单由 settlement 算、座位发布**:delete 支把 `landedEntryIds` 与 `successIds.length` 算成
16
+ * `{ entryIds, n }` 交给座位,座位只决定发布哪一形(单条带标题、批量带条数)——那句文案与它的键
17
+ * 住 `undo-toast.tsx` 与 `copy.ts`;purge 支在
18
+ * `anySuccess` 时熄灭指向这些条目的横幅——留着就是一条注定失败的「撤销」。
19
+ * - **`count` 不结算**;`orphan-clean` 没有跨槽位收尾(清完由 query 自己的写后刷新收尾)。
20
+ * 不直接读 DOM、不问宿主:主视图收尾只经 `main-view.ts` 的 `afterDelete` / `afterRestore`
21
+ * (服务缺席或抛错都静默退回);重读仍归各调用方的 `refresh`(面板那侧的最新票在 `use-trash-query.ts`)。
22
+ */
23
+ import type { ApplyAction } from '../shared/wire.js';
24
+ import { type Receipt } from './receipt.js';
25
+ /** 一次写入里与渲染无关的那部分:动作、请求 id、回执、成功名单与“至少一条落地”。 */
26
+ export interface SettledFacts<A extends ApplyAction> {
27
+ readonly action: A;
28
+ readonly ids: readonly string[];
29
+ readonly receipt: Receipt<A>;
30
+ /** 成功支(`ok` / `already-gone`)的请求 id,按请求顺序。 */
31
+ readonly successIds: readonly string[];
32
+ /** 至少一条真的落地(`ok` / `already-gone`;N=1 的协议空值不算)。 */
33
+ readonly anySuccess: boolean;
34
+ }
35
+ /**
36
+ * 按 action 切开的一族呈现材质:每个座位只能拿到自己动作那几手,形状对不上的钩子在类型面就不存在。
37
+ * - `delete` 的 `publishUndo` 存在即「这个座位要发撤销横幅」;它拿到的是 settlement 算好的落地名单。
38
+ * - `restore` 的 `panelSeat: true` 才是「面板已替这条完成『回来』的结算」,不再靠缺席某个钩子暗示。
39
+ * - `count` / `orphan-clean` 没有呈现材质(用 `noPresentation`)。
40
+ * 钩子都是同步的:settlement 在 `refresh` 前一次跑完,不 await。
41
+ */
42
+ export type WritePresentationByAction = {
43
+ delete: {
44
+ /** 外部写入座位:让回收站面板重读。面板写不传(自己的 `refreshAfterWrite` 就是重读)。 */
45
+ invalidate?(): void;
46
+ /** 撤销发布:settlement 把落地的条目 id 与请求名单算好后交来;名单**非空**——全被同批前一条带走时
47
+ * 没有落点,settlement 不发布没有可撤销动作的横幅。座位只决定发布哪一形与那一形带的标题。 */
48
+ publishUndo?(landed: {
49
+ readonly entryIds: readonly [string, ...string[]];
50
+ readonly n: number;
51
+ }): void;
52
+ };
53
+ restore: {
54
+ invalidate?(): void;
55
+ /** 面板恢复座位:true = 面板已替这条完成「回来」的结算,只遗忘、不重开、不抢焦点。 */
56
+ panelSeat?: true;
57
+ dismissUndo?(entryIds: readonly string[]): void;
58
+ clearDetailCache?(entryIds: readonly string[]): void;
59
+ /** already-gone 的回执不带行,它的 sessionId 只能由调用方从当前清单里查。 */
60
+ sessionIdOfEntry?(entryId: string): string | undefined;
61
+ };
62
+ purge: {
63
+ dismissUndo?(entryIds: readonly string[]): void;
64
+ clearDetailCache?(entryIds: readonly string[]): void;
65
+ };
66
+ count: Record<string, never>;
67
+ 'orphan-clean': Record<string, never>;
68
+ };
69
+ export type WritePresentation<A extends ApplyAction> = WritePresentationByAction[A];
70
+ /** 没有呈现材质的座位用它:抛掉按 action 的联合,调用点不用各自造一个空对象字面量。 */
71
+ export declare function noPresentation<A extends ApplyAction>(action: A): WritePresentation<A>;
72
+ export declare function settleWrite<A extends ApplyAction>(success: SettledFacts<A>, presentation: WritePresentation<A>): void;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 一次逐条 `/apply` 写入的**唯一生命周期**:apply → 解信封 → `readReceipt` → settlement → refresh。
3
+ *
4
+ * 收尾顺序与 `anySuccess` 门住 `src/client/write-settlement.ts`(为什么按 action 收见 ADR-0013);
5
+ * 这里只管时机、信封判读、整趟的闸与 `refresh` 的交法。清空回收站是聚合回执(`/empty`),收尾仍住
6
+ * `use-trash-query.ts` 的 `emptyTrash`。
7
+ *
8
+ * 五条判据:
9
+ * - `ok` 只说**信封**成不成;逐条成败在 `failures` 里,调用方自己决定怎么上屏。
10
+ * - 顶层失败把同一句原文铺到每个 id,不结算;`refresh` 照样交(成败都在 outcome 里)。
11
+ * - **信封成功就结算**(逐条全失败也跑:批量写入的失效通告不能漏);`anySuccess` 由这里算好,
12
+ * 按 action 的呈现材质由 settlement 按它上闸。调用方只交呈现材质,再按 `outcome` 决定是否真的重读。
13
+ * 结算在 `refresh` 之前:列表重读看得到收尾后的现场。
14
+ * - `single: true` 时把 N=1 模式交给 `receipt.ts` 的读法;单条 null 在那一处折成 failed,
15
+ * 这里不再重复守卫。
16
+ * - **闸包住整趟**:给了 `gate` 就把 apply → settle → refresh 一起包进去——refresh 落在闸外时,
17
+ * 旧清单还在屏上,同一个动作能被再点一次;调用方不再自己 `around`。
18
+ */
19
+ import type { BusyGate } from './busy-gate.js';
20
+ import { type OneShotDeps } from './remote.js';
21
+ import { type SettledFacts, type WritePresentation } from './write-settlement.js';
22
+ import type { ApplyAction, RemoteFailure } from '../shared/wire.js';
23
+ export type WriteSuccess<A extends ApplyAction> = SettledFacts<A> & {
24
+ readonly ok: true;
25
+ /** 逐条失败原文(failed / neutral / missing),按 id。 */
26
+ readonly failures: ReadonlyMap<string, string>;
27
+ };
28
+ export interface WriteFailure<A extends ApplyAction> {
29
+ readonly ok: false;
30
+ readonly action: A;
31
+ readonly ids: readonly string[];
32
+ readonly failure: RemoteFailure;
33
+ /** 顶层失败时同一句原文铺到每个请求 id。 */
34
+ readonly failures: ReadonlyMap<string, string>;
35
+ }
36
+ export type WriteOutcome<A extends ApplyAction> = WriteSuccess<A> | WriteFailure<A>;
37
+ export interface WriteInput<A extends ApplyAction> {
38
+ readonly deps: OneShotDeps;
39
+ readonly action: A;
40
+ readonly ids: readonly string[];
41
+ /** 单条写入面:把 N=1 模式交给 receipt 读法(null 折成协议错)。 */
42
+ readonly single?: boolean;
43
+ /** 信封成功时交给 settlement 的呈现材质(`successIds` 可能为空);按 action 的收尾顺序住 `write-settlement.ts`。 */
44
+ readonly presentation?: WritePresentation<A>;
45
+ /** 这一趟的闸:给了就把 apply → settle → refresh 整趟包进 busy。refresh 不许落在闸外。 */
46
+ readonly gate?: BusyGate;
47
+ /** 写完(顶层失败或信封成功,无论逐条成败)后的重读策略;成败与 `anySuccess` 都在 outcome 里,
48
+ * 是否真的重读由调用方决定。 */
49
+ readonly refresh?: (outcome: WriteOutcome<A>) => void | Promise<void>;
50
+ }
51
+ export declare function runWrite<A extends ApplyAction>(input: WriteInput<A>): Promise<WriteOutcome<A>>;
@@ -0,0 +1,14 @@
1
+ import type { FsPort } from './ports/fs-port.js';
2
+ import type { HostPort } from './ports/host-port.js';
3
+ import type { TrashLedger } from './trash/ledger.js';
4
+ /** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。 */
5
+ export interface CoreDeps {
6
+ fs: FsPort;
7
+ host: HostPort;
8
+ ledger: TrashLedger;
9
+ home: string;
10
+ }
11
+ /** 只有删除读时钟;其余读 / 计划 / 恢复 / 永久删除只用 CoreDeps。 */
12
+ export type DeleteDeps = CoreDeps & {
13
+ now(): number;
14
+ };
@@ -1,34 +1,28 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import { type ManifestPort } from './trash/manifest.js';
3
2
  export declare const name = "@xrn1997/dsh-session-delete";
4
3
  /**
5
4
  * 插件依赖:**只写字符串数组**(隔离数据根上实测:写成 `{ required, optional }` 会让插件停在
6
5
  * `pending (waiting for services: required, optional)`)。
7
6
  *
8
- * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。三个**故意不写**的:
7
+ * 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。**不进 inject 的**:
9
8
  * - `remote`:那是**浏览器半**的服务(asar `dsh-api-gateway/lib/client.js`:`super(ctx, "remote")`),
10
9
  * Node 半挂的是 `ctx.typertGateway`。把它写进 Node 半的 inject 会让插件永远 pending(同 `slots` 那次的形状)。
11
10
  * - `sessions` / `sessionProjectionCache`:两者都按 `ctx.get(...)` **可选读**——`sessions` 只给 `evictLive`
12
- * 用(活跃判据走 waterfall,见设计稿 §5;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
11
+ * 用(活跃判据走 waterfall,见 src/ports/host-port.ts;服务缺席回 `unknown`),`sessionProjectionCache` 是标题的尽力读面。
13
12
  * inject 一个可能没挂的服务 = 整个插件 pending。
14
13
  * 它取不到时 `titleOf` 返回空串,正是契约允许的降级。
14
+ * - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
15
+ * 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
15
16
  *
16
17
  * `webServer` 是**浏览器半取数通道的承载者**(那条薄线):插件用它注册 `/dsh-session-delete-api`
17
18
  * 前缀路由,浏览器半同源 fetch 那六个段名。它是宿主 web 组成里的硬依赖(`dsh-host-webserver` 的
18
19
  * `super(ctx, "webServer")`),服务名与 `register({kind:'prefix', path, handler})` 的形状按
19
20
  * `dsh-host-webserver/lib/index.js` 与 `README.md` 原文读实,不是猜的。
20
- */
21
- export declare const inject: string[];
22
- /**
23
- * 清单的真实绑定:官方 storage domain → `ManifestPort`(`StorageDomainLike` 在动词层保持一种读法)。
24
- * 域**懒开**且成功句柄只开一次(`apply` 是同步的,而 `open()` 是异步的),并挂一条 `ctx.effect` 清理:
25
- * 插件卸载时关掉句柄并把 `close()` 的落地交回框架(句柄归调用方所有,这是 `open()` 的 JSDoc 明写的契约;
26
- * 2026-10-17 起不再 `void` 走掉——没关严的句柄会让下一份同名域撞 `already-open`)。
27
21
  *
28
- * **失败不缓存**:`open()` 被拒是瞬态(域被别的持有者占着、装载竞态),把第一次的 rejected promise
29
- * 缓存起来等于"这个会话永远删不掉"——下一次读会重新走 `open()`,成功才落下活句柄。
22
+ * 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
23
+ * 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
30
24
  */
31
- export declare function createStorageManifest(ctx: Context): ManifestPort;
25
+ export declare const inject: string[];
32
26
  /**
33
27
  * 装配顺序:**清单 → 端口 → 动词依赖 → 前缀路由**(`DeleteDeps` 就是前两者的合一)。
34
28
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 多帧 zstd 流的**帧边界**(设计稿 2026-10-15 §1.1)。
2
+ * 多帧 zstd 流的**帧边界**(2026-10-15 实测)。
3
3
  *
4
4
  * 为什么要有这个文件:会话日志是一次次 append 出来的**帧序列**,而 Node 的 `zstdDecompressSync`
5
5
  * **与** `createZstdDecompress()` 流式**都只给第一帧**(实测:本机一个 3.8 MB 的文件,两个 API 都只回
@@ -0,0 +1,25 @@
1
+ /** 捕获所需的 ctx 窄面:可选服务读法(`get`)与按依赖注入的子上下文(`inject`,晚挂载的 `agents` 靠它补装)。
2
+ * 旧 ctx / 假件可以没有 `inject`——那就只在 apply 时 `get` 一次。 */
3
+ export interface AgentCtxLike {
4
+ get(name: string): unknown;
5
+ inject?(names: readonly string[], callback: (child: {
6
+ get(name: string): unknown;
7
+ }) => void): void;
8
+ }
9
+ /** 一次捕获交回的句柄表:按会话 id `dispose`。没捕到 / 已经 dispose 过都是 no-op;
10
+ * dispose 本身的失败**原样抛出**(调用方 `evictLive` 把它折成 `unknown`,不通告)。 */
11
+ export interface AgentHandleStore {
12
+ dispose(sessionId: string): Promise<void>;
13
+ }
14
+ /** 在 `ctx.agents` 上包一层 `resume` / `create`,把返回的 agent handle 的 dispose 按会话 id 记下。
15
+ * 服务缺席时走 `ctx.inject(['agents'])` **等服务挂上再装**——live profile 的插件激活顺序可能早于
16
+ * `agents` 服务就绪;只在 apply 时 `get` 一次会让捕获静默缺席(真机症状:重启前在回收站的能冷恢复,
17
+ * 刚删除的会话恢复后旧 agent 拦住 prompt)。形状不符 / 属性不可写都只是**不捕获**,不影响插件激活。
18
+ *
19
+ * **安装时机**:先 `get('agents')`,没装成再 `inject(['agents'])`(两者只装一次,符号表兜住)。
20
+ * **symbol 表**:句柄表挂在服务实例的 {@link CAPTURED_AGENT_HANDLES} 上;同一个 agents 服务上的第二次
21
+ * 安装(`get` 与 `inject` 各一次,或第二个 `createHostPort`)看见它就直接共用,不把 wrapper 套第二层。
22
+ * **读回判据**:cordis 的 traceable 服务代理每次读函数都会新包一层(`get` 里的 `createShadowMethod`),
23
+ * 所以只能看 own property descriptor 的身份;拿 `service[name]` 读回比身份是必然的假警报。
24
+ */
25
+ export declare function captureAgentHandles(ctx: AgentCtxLike): AgentHandleStore;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 宿主数据根。官方解析器在 asar `dsh/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js`:
3
+ * `resolveDshHome(configured, env)` 的优先级 = 显式配置 > `$DSH_HOME`(**空白视为未设**,免得非法覆盖把 home
4
+ * 解析成当前工作目录)> `~/.dsh`,并 `resolve()` 规范化、展开 `~/`。
5
+ *
6
+ * **只镜像后两级,且这与平台的同款设施等价**——"显式配置"那一级对插件**不可得**,三条实证:
7
+ * ① 平台自己的 helper `dshHomePath(...segments)` 逐字是 `join(resolveDshHome(), ...segments)`,
8
+ * **同样不传 `configured`**;
9
+ * ② `ctx.profileContext` 按 launcher 原文只装「profile 位置 / 启动 bundle 名 / 调用覆盖 / 遥测开关」,
10
+ * 里面没有数据根;
11
+ * ③ 宿主自己的服务也不是"问出来"的——它们各自声明 `Config.dshHome`,由**装配那一行的人**填。
12
+ * ⇒ 后果与边界(判据在 `src/verbs/delete-session.ts` 与 `src/verbs/restore-session.ts` 的预检):显式配置过的
13
+ * profile 上我们算出的根与宿主不一致 ⇒ 文件探测落空 ⇒ 删除/恢复**响亮拒绝、零副作用**(`not-found` /
14
+ * `trash-empty` / `project-missing`);**不会**动到别的树(sessions 与回收站同源 ⇒ 同卷)。
15
+ * 本机两条路径都因此对得上:桌面版走 `~/.dsh`(实测 profile 在其下),隔离探针靠 `$DSH_HOME`。
16
+ *
17
+ * `homeDir` 只用于测试注入假主目录;默认值仍是 `homedir()`,行为与搬家前一致。
18
+ */
19
+ export declare function resolveHome(env?: NodeJS.ProcessEnv, homeDir?: string): string;