@xrn1997/dsh-session-delete 0.1.1 → 0.2.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 (54) hide show
  1. package/README.md +5 -4
  2. package/icon.svg +18 -18
  3. package/lib/client.js +1407 -427
  4. package/lib/index.js +785 -182
  5. package/lib/types/api/dispatch.d.ts +51 -0
  6. package/lib/types/client/batch-bar.d.ts +24 -0
  7. package/lib/types/client/batch-confirm.d.ts +31 -0
  8. package/lib/types/client/batch-menu-item.d.ts +19 -0
  9. package/lib/types/client/batch-select.d.ts +97 -0
  10. package/lib/types/client/context.d.ts +31 -0
  11. package/lib/types/client/copy.d.ts +167 -0
  12. package/lib/types/client/delete-confirm.d.ts +40 -0
  13. package/lib/types/client/delete-menu-item.d.ts +20 -0
  14. package/lib/types/client/index.d.ts +19 -0
  15. package/lib/types/client/markdown.d.ts +11 -0
  16. package/lib/types/client/remote.d.ts +178 -0
  17. package/lib/types/client/trash-detail.d.ts +27 -0
  18. package/lib/types/client/trash-entry.d.ts +6 -0
  19. package/lib/types/client/trash-panel.d.ts +10 -0
  20. package/lib/types/client/trash-store.d.ts +4 -0
  21. package/lib/types/client/ui/Button.d.ts +21 -0
  22. package/lib/types/client/ui/Checkbox.d.ts +29 -0
  23. package/lib/types/client/ui/MenuItemButton.d.ts +32 -0
  24. package/lib/types/client/ui/Modal.d.ts +35 -0
  25. package/lib/types/client/ui/Tag.d.ts +16 -0
  26. package/lib/types/client/ui/Toast.d.ts +36 -0
  27. package/lib/types/client/ui/Tooltip.d.ts +37 -0
  28. package/lib/types/client/ui/format.d.ts +18 -0
  29. package/lib/types/client/ui/icons.d.ts +33 -0
  30. package/lib/types/client/ui/recipe.d.ts +11 -0
  31. package/lib/types/client/ui/styles.d.ts +2 -0
  32. package/lib/types/client/undo-toast.d.ts +49 -0
  33. package/lib/types/client/use-column-resize.d.ts +62 -0
  34. package/lib/types/client/use-panel-width.d.ts +15 -0
  35. package/lib/{index.d.ts → types/index.d.ts} +9 -36
  36. package/lib/types/log/session-log.d.ts +40 -0
  37. package/lib/types/log/zstd-frames.d.ts +30 -0
  38. package/lib/types/ports/fs-port.d.ts +16 -0
  39. package/lib/types/ports/host-port.d.ts +85 -0
  40. package/lib/types/remote/map-error.d.ts +19 -0
  41. package/lib/types/shared/wire.d.ts +134 -0
  42. package/lib/types/trash/detail.d.ts +40 -0
  43. package/lib/types/trash/entry.d.ts +35 -0
  44. package/lib/types/trash/manifest.d.ts +18 -0
  45. package/lib/types/trash/reconcile.d.ts +35 -0
  46. package/lib/types/verbs/apply.d.ts +3 -0
  47. package/lib/types/verbs/delete-session.d.ts +30 -0
  48. package/lib/types/verbs/errors.d.ts +20 -0
  49. package/lib/types/verbs/purge-trash.d.ts +6 -0
  50. package/lib/types/verbs/restore-session.d.ts +19 -0
  51. package/locale/en.json +6 -6
  52. package/locale/zh.json +6 -6
  53. package/package.json +14 -9
  54. package/lib/client.js.map +0 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `/dsh-session-delete-api` 前缀路由的**内部分发**(薄线的 Node 半)。
3
+ *
4
+ * 为什么是这一条路(裁决,不是偏好):浏览器侧 `remote.<namespace>` 只能由**生成的 Typert 远程
5
+ * 描述符贡献**经 `ctx.remote.$mount(...)` 挂上,而盒内那份装配清单是**写死的 25 条官方包**、
6
+ * 全宿主没有任何"按包发现再挂载"的动态机制 ⇒ 第三方要那个命名空间,
7
+ * 只能自挂自(要生成物 + `zod` + 改包布局),代价与收益完全不成比例。改用宿主**自己的 web server**
8
+ * 开一条插件专属前缀路由:真相/账本/判据全在宿主那边,这一层只搬运。
9
+ *
10
+ * 四条口径:
11
+ * ① **不依赖 Cordis**:`createApiHandler(deps)` 是纯构造器,返回一个 `(req, res) => Promise<void>`
12
+ * ——所以它能用真 `node:http` server 直接单测(`tests/api.test.ts`),与宿主怎么挂它无关。
13
+ * ② **信封就是浏览器半消费的那个形状**:`{ok:true,value}` / `{ok:false,error:{code,message,details?}}`。
14
+ * `error.code` 走 `src/remote/map-error.ts` 的**同一张表**(设计稿 §6 的五个码),原文进
15
+ * `error.message`,结构化附带物进可选的 `error.details`——浏览器半的 `unwrapRemoteResult` /
16
+ * `failureOf` 因此一行都不用改。
17
+ * ③ **HTTP 状态不是第二套语义**:**操作失败**时状态一律 200(含 `ok:false`)——我们与浏览器半之间的
18
+ * 契约是信封本身,判断成败只看 `ok`(同宿主网关"每次 unary 调用 resolve 成 RemoteResult、
19
+ * 不为载体问题 reject"的那条规矩)。**请求本身不对**才用 4xx(400 请求体 / 405 方法 / 404
20
+ * 未知路由),同源栅栏拒的是**请求**而不是操作,回 403;浏览器半先解信封再看状态。
21
+ * ④ **同源栅栏**:本机回环上一条能删会话的写口不能被任意网页用 simple request CSRF 打——没有栅栏时
22
+ * `content-type: text/plain` 的跨源 POST 不触发预检、直接生效。放行的形状见 {@link isTrustedRequest}:
23
+ * 同源(浏览器直接打开宿主那行 URL)、桌面壳自己的页面(`dsh-app://app`,真机那条路,见
24
+ * {@link isDesktopShellUrl})、或两者皆缺席(curl / 本机工具)。本机既有插件
25
+ * (`dsh-novel` 的 `api/wire.ts`)只认前两条里的"同源",本文件多的是桌面壳那一条——**它必须有**:
26
+ * 桌面版的请求被壳摘掉了 `Origin`,漏掉它等于真机上四个入口一个都不出现(而直连 http 的过门
27
+ * 读数看不出来)。
28
+ * ⑤ **`list` 是"对账 + 读",且它与写动作按到达序串行**({@link createApiHandler} 里那条进程内链):
29
+ * 对账(`reconcileTrash`)与删除/恢复/彻底删除都是"读清单 + 动文件 + 写清单"的多步,交错跑会互相踩
30
+ * ——删除刚把目录搬进回收站、行还没写上时,对账抢先把这行按目录"重建"出来,再被真实行覆盖成降级
31
+ * 元数据。串行的代价只是排队(单进程单用户,毫秒级);纯读(`detail`、`ping`、`apply/count`)
32
+ * 不占这条链。
33
+ * ⑥ **一段 `POST /apply` 管所有写动作**(2026-10-17 批量重构):旧形状是「一条一段、一次一发」,
34
+ * 批量只能由调用方循环拼出来——100 条 = 100 次往返 ×(一次全量清册读 + 每条一次宿主会话表)。
35
+ * 现在一组 id 一次进、逐条 `ApplyOutcome` 出;单条就是 N=1,仓里不留第二条执行路径。
36
+ * 清空是聚合回执(不按条回),所以单独一段 `POST /empty`。
37
+ */
38
+ import type { IncomingMessage, ServerResponse } from 'node:http';
39
+ import type { TrashEntry } from '../trash/entry.js';
40
+ import type { DeleteDeps } from '../verbs/delete-session.js';
41
+ /** 清单交回浏览器的那一行:清册那一行 + 组视图的三个只读数。 */
42
+ export interface TrashRow extends TrashEntry {
43
+ /** 跟着这条父会话一起进回收站的孩子数(来源:清册里 `parentEntryId` 指向它的行)。 */
44
+ readonly subagentCount?: number;
45
+ /** 那些孩子的字节合计(页脚的合计要算得上它们,不能只加父自己那一格)。 */
46
+ readonly subagentBytes?: number;
47
+ /** **盘上还留着、没被带走**的孩子数(来源:宿主 `sessionQuery`)。级联落地前删掉的那些存量才非零;
48
+ * 与 `subagentCount` 是两件事,界面上必须各说各的(不许用一个冒充另一个)。 */
49
+ readonly strandedSubagents?: number;
50
+ }
51
+ export declare function createApiHandler(deps: DeleteDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 批量删除的浮条(第十三批①):**多选模式内常驻**(0 条也渲染,既显示模式又给退出入口),
3
+ * 汇总「已选 N 条」+(读得到时)「含 K 个子会话」;「查看已选」展开限高可滚动的标题清单
4
+ * (逐条可移除),回答"到底选了哪些"——清单默认收起,不把底部输入区盖住。
5
+ * 模式内的整行点击守卫也挂在这棵常驻宿主上(见 `batch-select.tsx` 的 `useSelectionRowClickGuard`)。
6
+ *
7
+ * 为什么单独一个文件而不是并进 `batch-select.tsx`:那份是"选中集 + 行开关"(被确认框 import),
8
+ * 而浮条要点开确认框(要 import 它的现场)——两个方向合到一个文件上就是一圈。浮条住在
9
+ * `shell.overlay` 上,与单删的撤销提示/确认框同类(槽位原文:"a toast stack or a status pill
10
+ * all belong here")。
11
+ *
12
+ * 那个 K 是**勾选变化后防抖 300ms 的一次批量 count**(`/apply` 的 `action:'count'`,一组 id 一次),
13
+ * 并且带**序号守卫**:在飞的旧响应不许盖掉新名单的读数(与第十二批那条 `reloadSeq` 同一个道理)。
14
+ * 读不到就整句不说——K 是合计,缺一条就是假数(10-15 稿 §10「不许拿 0 冒充没有」)。
15
+ */
16
+ import { type ReactElement } from 'react';
17
+ import { type Translate } from './copy.js';
18
+ import type { SessionDeleteRemote } from './remote.js';
19
+ /** 浮条只需要那一个批量口(计数);执行归确认框。 */
20
+ export type BatchBarDeps = Pick<SessionDeleteRemote, 'applyOutcomes'>;
21
+ export declare function BatchBar({ deps, t, }: {
22
+ deps: BatchBarDeps;
23
+ t?: Translate;
24
+ }): ReactElement | null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 批量删除的确认框(第十三批②)——挂在 `shell.overlay` 上的宿主,加它读的那个模块级现场。
3
+ *
4
+ * 为什么又开一个文件而不是把 `delete-confirm.tsx` 撑大:单删那张框是**已交付的面**(设计稿 §7 状态 ②,
5
+ * 用例钉着),批量形与它只有三件不同——标题随条数、目标卡逐条列、执行是一次批量 `apply`。共用一个壳
6
+ * 意味着单删的 `busy` / 失败面要跟着学批量形,那是拿已交付的行为去换省一个文件。卡片样式(`TARGET_CARD`)
7
+ * 与 hint、子会话那两句**复用**单删的,一句不造第二个键。
8
+ *
9
+ * 现场是模块级的(同 `delete-confirm.tsx` / `undo-toast.tsx`):请求从浮条那颗按钮发来,确认框住在
10
+ * `shell.overlay` 上——它必须活在行开关那棵子树之外(槽位行随宿主重排而卸载,见 `delete-confirm.tsx` 头注)。
11
+ */
12
+ import { type ReactElement } from 'react';
13
+ import { type PickItem } from './batch-select.js';
14
+ import { type Translate } from './copy.js';
15
+ import { type SessionDeleteRemote } from './remote.js';
16
+ export interface BatchDeleteRequest {
17
+ /** 每次发布递增:换一批就重挂载,busy / 失败面不带着上一批的记忆。 */
18
+ seq: number;
19
+ items: readonly PickItem[];
20
+ }
21
+ /** 浮条那颗按钮调用:请求确认这一批(缺省 = 当前选中集)。空名单不发框。 */
22
+ export declare function requestDeleteBatch(items?: readonly PickItem[]): void;
23
+ export declare function clearDeleteBatchRequest(): void;
24
+ /** 供浮条与装配面订阅。 */
25
+ export declare function useDeleteBatchRequest(): BatchDeleteRequest | null;
26
+ export type BatchConfirmDeps = Pick<SessionDeleteRemote, 'applyOutcomes'>;
27
+ /** 常驻的批量确认框宿主。没有待确认的批量删除时什么都不渲染。 */
28
+ export declare function BatchConfirmHost({ deps, t, }: {
29
+ deps: BatchConfirmDeps;
30
+ t?: Translate;
31
+ }): ReactElement | null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 会话「⋯」菜单里的「多选」入口(第十三批①改版)。
3
+ *
4
+ * 常驻勾选框被用户打回后,入口改成显式模式:按下这一行才进入多选态(再按一次退出),
5
+ * 模式内才渲染勾选框、才把整行点击变成"勾/取消"(守卫在 `batch-select.tsx`)。
6
+ * 菜单的开关归 owner(与 `delete-menu-item.tsx` 同一条契约):按下后先切模式、再收菜单。
7
+ *
8
+ * 文案复用回收站工具栏那两句(`trash.toolbar.select` / `selectExit`)——同一个"多选/退出多选"
9
+ * 不造第二个键。
10
+ */
11
+ import type { ReactElement } from 'react';
12
+ import { type Translate } from './copy.js';
13
+ export interface BatchSelectMenuItemProps {
14
+ /** 菜单 owner 交下来的「收起菜单」;缺省不动,方便单独测这一行。 */
15
+ dismissMenu?: () => void;
16
+ /** 文案(缺省 = 内置 zh,见 `copy.ts`)。 */
17
+ t?: Translate;
18
+ }
19
+ export declare function BatchSelectMenuItem({ dismissMenu, t, }: BatchSelectMenuItemProps): ReactElement;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 宿主列表批量删除的选择面(第十三批①):多选模式 + 选中集 + 行首勾选 + 行尾兜底 + 整行点击守卫。
3
+ *
4
+ * 入口在每行的「⋯」菜单里(`batch-menu-item.tsx`):**按下才进入多选模式**,模式内才渲染勾选框、
5
+ * 才把整行点击变成"勾/取消";退出模式清空名单。选中集是**模块级现场**(本仓既有的客户端模式,见
6
+ * `delete-confirm.tsx` / `undo-toast.tsx`):主勾选框住在每行的 `sidebar.session.row.leading` 槽位里,
7
+ * 行尾兜底住在 `sidebar.workspaces.session.row.action`,浮条与批量确认框住在 `shell.overlay` 上——
8
+ * 四处隔着槽位边界,只有模块级 store 能让他们看见同一份名单。状态源用 React 自带的
9
+ * `useSyncExternalStore`,不为此引新的状态库。
10
+ *
11
+ * **为什么主勾选在行首**:宿主对行尾 `.rowActions` 是 `display:none`,只有 `row:hover` / 菜单打开
12
+ * 才出现(app.asar 0.2.0-rc.2 的 Rows 渲染);行首 16px 格在无状态点时是空的,模式内可见。
13
+ * 宿主在「有状态点(运行中/待确认/未读完成)」与归档行不渲染行首格 ⇒ 这些行走行尾悬停兜底,
14
+ * 但模式内**点整行**也能选(`useSelectionRowClickGuard`),所以不依赖那颗小框。
15
+ * 两条勾选不会同时出现(`ui.css` 的 `:has` 兜底隐藏规则,靠宿主行的 `data-row-key`)。
16
+ *
17
+ * 名单只收槽位投影原样给的 `sessionId` / `displayTitle`(浏览器半不自己拼 id),**顺序 = 勾选顺序**:
18
+ * 宿主列表的 DOM 序不归我们读,`/apply` 那条「保持输入顺序」吃到的因此就是勾选序。
19
+ */
20
+ import { type ReactElement } from 'react';
21
+ import { type Translate } from './copy.js';
22
+ import { type SessionDeleteRemote } from './remote.js';
23
+ export interface PickItem {
24
+ /** 宿主槽位投影的会话 id,原样透给 `applyOutcomes('delete', …)`。 */
25
+ sessionId: string;
26
+ /** 删除那一刻的会话名(投影的 `displayTitle`):只用于文案与确认框,不参与请求。 */
27
+ title: string;
28
+ }
29
+ export declare function getPicks(): readonly PickItem[];
30
+ export declare function isSelecting(): boolean;
31
+ /** 「⋯ → 多选」按下时进入;重复进入是幂等的。 */
32
+ export declare function enterSelection(): void;
33
+ /** 退出模式并清空名单:多选态和选中集是同一次交互的两半,退出后不该留下看不见的名单。 */
34
+ export declare function exitSelection(): void;
35
+ export declare function useSelecting(): boolean;
36
+ /** 翻转一条:没选过就排队尾,选过就摘掉(列表里永远不会有重复 id)。 */
37
+ export declare function togglePick(item: PickItem): void;
38
+ export declare function clearPicks(): void;
39
+ /** 全选:把当前可见可选行里还没选的按 DOM 序追加;原有选中保持在前。 */
40
+ export declare function selectAllVisible(): void;
41
+ /**
42
+ * 同一个切换口该显示「全选」还是「取消全选」:
43
+ * - 有选中、且当前可见可选行全都选中 ⇒ 取消全选;
44
+ * - 有选中、但当前没有可见可选行(全折叠/过滤掉)⇒ 只能清空,也显示取消全选;
45
+ * - 其余(含 0 条)⇒ 全选。
46
+ */
47
+ export declare function allVisibleSelected(): boolean;
48
+ /** 执行后收缩:只留失败名单(空数组 = 全清)。成功的随通告从侧栏消失,不该留在名单里。 */
49
+ export declare function shrinkPicksToFailures(failedSessionIds: readonly string[]): void;
50
+ export declare function usePicks(): readonly PickItem[];
51
+ /**
52
+ * 多选态内的整行点击守卫(document 捕获阶段)。
53
+ *
54
+ * 为什么必须在这里拦:宿主的会话行自己带 `onClick = open(sessionId)`,第三方没有官方的
55
+ * "别切会话"开关;模式内点行要变成"勾/取消",只能抢在 React 根监听之前把事件吃掉。
56
+ * 边界:行里的按钮 / 链接 / 输入 / 菜单项一律放行(否则 ⋯ 菜单、归档、置顶、单删都点不动);
57
+ * 双击的第二下与 dblclick 不翻转(第一下已经选中,第二下不能又反掉,也不能打开宿主的重命名)。
58
+ * 认行靠 `data-row-key="session:…"`——宿主将来改掉这个属性,最差是模式内点行不生效,
59
+ * 勾选框本身仍在。
60
+ */
61
+ export declare function useSelectionRowClickGuard(): void;
62
+ /**
63
+ * 一组会话的子会话计数(`/apply` 的 `action:'count'`,纯读、不进串行链)。
64
+ *
65
+ * **一条读不到就整份返回 null** ——界面上那两个数(浮条的 K、确认框的 K 与逐条 M)都是合计,
66
+ * 缺一条就是假数;「不许拿 0 冒充没有」(10-15 稿 §10)。
67
+ */
68
+ export declare function fetchCounts(deps: Pick<SessionDeleteRemote, 'applyOutcomes'>, ids: readonly string[]): Promise<Map<string, number> | null>;
69
+ export interface BatchPickCheckboxProps {
70
+ sessionId: string;
71
+ /** 槽位只投 `sessionId`;标题由宿主侧的 root hook 读出来再传进来(见 `index.tsx` 的 `PickLeadingRow`)。 */
72
+ title: string;
73
+ /** 文案(缺省 = 内置 zh,见 `copy.ts`)。 */
74
+ t?: Translate;
75
+ }
76
+ /**
77
+ * 行首 16px 格里的主勾选框:**只在多选模式内渲染**(模式入口在「⋯ → 多选」,不常驻)。
78
+ *
79
+ * 用的是回收站多选同款原生 `Checkbox`:它自己会 `stopPropagation`,所以点框不会顺带打开会话
80
+ * (行首格在宿主行那条 `onClick` 里面)。宿主对「有状态点」和归档行不渲染这一格,
81
+ * 那些行走 `BatchPickButton` 兜底;两条不会同时出现(见 `ui.css` 的兜底隐藏规则)。
82
+ */
83
+ export declare function BatchPickCheckbox({ sessionId, title, t, }: BatchPickCheckboxProps): ReactElement | null;
84
+ export interface BatchPickButtonProps {
85
+ sessionId: string;
86
+ title: string;
87
+ /** 会话在跑:置灰 + 悬停原因(与单删菜单行同款——级联遇在跑成员整组拒绝,先说清再让人点不动)。 */
88
+ live?: boolean;
89
+ /** 文案(缺省 = 内置 zh,见 `copy.ts`:这不是"忘了传",而是"这次没有 locale 服务")。 */
90
+ t?: Translate;
91
+ }
92
+ /**
93
+ * 多选态里行尾悬停排的**兜底**勾选开关:只在宿主不渲染行首格的行上露头(状态点行 / 归档行)。
94
+ * 座位原文保证「排内点击不误开会话」(`rowActions` 那层自己挡住冒泡),所以这颗按钮不需要
95
+ * 自己 `stopPropagation`。行首那颗与这颗共用同一份选中集和同一句 aria 名字;模式内点整行也能选。
96
+ */
97
+ export declare function BatchPickButton({ sessionId, title, live, t, }: BatchPickButtonProps): ReactElement | null;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 宿主**浏览器侧** ctx 的本地窄镜像(AGENTS.md 的规矩:只声明用到的成员 + 一次强转;
3
+ * 不装宿主类型包、不做 `declare module` 增强)。
4
+ *
5
+ * 单独一个文件是因为它现在有两个使用者:入口 `index.tsx`(槽位注册)与 `copy.ts`(locale 服务),
6
+ * 放在入口里会绕出一个循环导入。
7
+ */
8
+ /** 一个槽位注册项。`label` 允许是 thunk——宿主 `resolveSlotLabel` 的 JSDoc 原文即
9
+ * "thunks follow the active locale",面板行名要随语言走就得走这条(见 `copy.ts` 头注)。 */
10
+ export interface SlotRegistration {
11
+ name: string;
12
+ /** list 座位的占用者 id。 */
13
+ id?: string;
14
+ /** keyed 座位(`main`)的键;与 `id` 二者按座位取一。 */
15
+ key?: string;
16
+ order?: number;
17
+ /** 宿主的行名字(`resolveSlotLabel(options.label)`;`sidebar.panellist` 用它画 `.panelTitle`)。 */
18
+ label?: string | (() => string);
19
+ inject?: () => unknown;
20
+ }
21
+ export interface SlotsLike {
22
+ inject(key: string, contribute: () => unknown): unknown;
23
+ register(options: SlotRegistration, component: unknown): unknown;
24
+ }
25
+ export interface ClientContextLike {
26
+ slots: SlotsLike;
27
+ /** 可选服务的读面(宿主自己的写法:`ctx.get('…')?.…`)。**缺省调用**:老宿主上没有这个方法。 */
28
+ get?(name: string): unknown;
29
+ /** 挂一个随本插件卸载而撤销的副作用(返回的清理函数由框架接管)。 */
30
+ effect(execute: () => unknown, label?: string): unknown;
31
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 可见文案(**全部**住在这里)+接上宿主 Client locale 服务的那一步。
3
+ *
4
+ * ## 为什么不是散在各组件里的中文字面量
5
+ *
6
+ * 官方给第三方 UI 插件的规则是「Route visible UI text through the Client locale service」
7
+ * (宿主技能 `cordis-plugin-development/references/ui-plugin.md`)。宿主自己那套就是这么做的:
8
+ * `ui-sidebar` 声明 `inject = ['slots','layout','uiWorkspace','locale','shortcuts']`,并在
9
+ * `ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-sidebar: dictionaries')` 里注册字典
10
+ * (真产物原文)。所以本插件也把文案收成一份带**命名空间**的字典,`zh` 是完整真源、`en` 逐键镜像
11
+ * ——宿主 `register` 的**对象形式要求两套内置语言都齐**(缺一个键就是运行期报错)。
12
+ *
13
+ * ## 服务是**可选读**的,不进 `inject`
14
+ *
15
+ * `ctx.get('locale')` 而不是把 `locale` 写进 `inject`。理由是本仓那条硬约束的另一面:**客户端半的
16
+ * 条目激活失败 = 宿主进程死**(前端启动自检 `web boot: N entry did not activate` ⇒ 桌面壳
17
+ * `reportFatal('web-boot')`)。宿主自己的 `ui-sidebar` 敢硬注入 `locale`(它是宿主的一部分,知道
18
+ * 那一行一定在),而本插件要跨宿主代数活着 ⇒ 服务缺席时**降级到内置 zh 文案继续工作**,
19
+ * 好过整个进程死。这与宿主对可选服务的官方写法同形(`ctx.get('…')?.…`)。
20
+ *
21
+ * 取数面:[`dsh-client-locale`] 的 `LocaleRuntime.register(ns, { zh, en })` 返回一个 disposer
22
+ * (必须挂进 `ctx.effect`,否则卸载后字典留在注册表里,下次挂载会撞 "already has locale"),
23
+ * `bind(ns)` 交回一个**按命名空间记忆化**的 `t(key, params)`(官方明说它"retain stable identity …
24
+ * so they can ride inject surfaces without breaking memoization"——所以我们把它塞进槽位的
25
+ * `inject` 载荷是正当用法)。
26
+ *
27
+ * ## 面板行那一行字为什么要用 thunk
28
+ *
29
+ * `sidebar.panellist` 的行名字由宿主 `syncPanels` 逐字 `label: resolveSlotLabel(options.label) ?? id`
30
+ * 解析,而 `resolveSlotLabel` 的实现是 `typeof label === 'function' ? label() : label`,
31
+ * 其 JSDoc 原文即 "thunks follow the active locale";宿主自己还挂了
32
+ * `ctx.effect(() => ctx.locale.subscribe(syncPanels), 'ui-sidebar: panel labels')` 在语言切换时重读。
33
+ * ⇒ 面板名传 `() => t('trash.title')` 就随语言走;**换成活计数就会 stale**(只在条目或语言变化时重读)。
34
+ */
35
+ import { type RelativeTimeUnit } from './ui/format.js';
36
+ import type { ClientContextLike } from './context.js';
37
+ /** zh 是真源;`en` 必须逐键齐全(宿主的对象形式注册会整体校验)。 */
38
+ declare const ZH: {
39
+ readonly 'common.untitled': "(无标题会话)";
40
+ readonly 'common.cancel': "取消";
41
+ readonly 'common.close': "关闭";
42
+ readonly 'common.justNow': "刚刚";
43
+ readonly 'common.ago': "{n}{unit}前";
44
+ readonly 'common.retry': "重试";
45
+ readonly 'trash.title': "回收站";
46
+ readonly 'trash.loading': "正在读回收站…";
47
+ readonly 'trash.readFailed': "回收站读不到";
48
+ readonly 'trash.empty.title': "回收站是空的";
49
+ readonly 'trash.empty.hint': "在会话条目的「…」菜单里选「删除」,会话会先进到这里,随时可以恢复。";
50
+ readonly 'trash.summary': "{count} 个会话 · 共 {size}";
51
+ readonly 'trash.group.count': "{count} 个会话";
52
+ readonly 'trash.project.unknown': "未记录项目";
53
+ readonly 'trash.row.deletedNow': "刚刚删除";
54
+ readonly 'trash.row.deletedAgo': "删除于 {n}{unit}前";
55
+ readonly 'trash.row.subagents': "含 {n} 个子会话";
56
+ readonly 'trash.row.strandedSubagents': "盘上还剩 {n} 个子会话";
57
+ readonly 'trash.unit.minutes': "分钟";
58
+ readonly 'trash.unit.hours': "小时";
59
+ readonly 'trash.unit.days': "天";
60
+ readonly 'trash.unit.months': "个月";
61
+ readonly 'trash.unit.years': "年";
62
+ readonly 'trash.row.restore': "恢复";
63
+ readonly 'trash.row.purge': "彻底删除…";
64
+ readonly 'trash.footer.note': "回收站本体在宿主数据根下的 {dir} 里,不自动过期;清空后不可恢复。";
65
+ readonly 'trash.emptyAll': "清空回收站…";
66
+ readonly 'trash.confirm.all': "清空回收站?";
67
+ readonly 'trash.confirm.one': "彻底删除 1 个会话?";
68
+ readonly 'trash.confirm.one.meta': "{project} · 删除于 {at} · {size}";
69
+ readonly 'trash.confirm.body': "这会从回收站移除文件,{strong}。";
70
+ readonly 'trash.confirm.body.strong': "之后没有任何副本可以恢复";
71
+ readonly 'trash.confirm.purge': "彻底删除";
72
+ readonly 'trash.toolbar.collapseAll': "全部折叠";
73
+ readonly 'trash.toolbar.expandAll': "全部展开";
74
+ readonly 'trash.divider.label': "调整左右分栏宽度";
75
+ readonly 'trash.select.group': "勾选「{project}」这 {count} 条会话";
76
+ readonly 'trash.toolbar.select': "多选";
77
+ readonly 'trash.toolbar.selectExit': "退出多选";
78
+ readonly 'trash.select.count': "已选 {count} 条";
79
+ readonly 'trash.select.purge': "彻底删除 {count} 条";
80
+ readonly 'trash.select.restore': "恢复 {count} 条";
81
+ readonly 'trash.select.needOne': "先勾选要处理的会话";
82
+ readonly 'trash.select.row': "勾选「{title}」";
83
+ readonly 'trash.confirm.bulk': "彻底删除 {count} 个会话?";
84
+ readonly 'trash.confirm.restore': "恢复 {count} 个会话?";
85
+ readonly 'trash.confirm.restore.body': "这一批会回到它们各自的原项目目录,恢复到会话列表里。";
86
+ readonly 'trash.confirm.restoreNow': "恢复";
87
+ readonly 'detail.notSelected': "选中一条会话,看它讲了什么";
88
+ readonly 'detail.loading': "正在读这条会话…";
89
+ readonly 'detail.readFailed': "这条会话的正文读不出来";
90
+ readonly 'detail.noBody': "这条会话没有可显示的文字";
91
+ readonly 'detail.field.created': "创建";
92
+ readonly 'detail.field.lastActivity': "最后活动";
93
+ readonly 'detail.field.deletedAt': "删除于";
94
+ readonly 'detail.field.span': "活跃跨度";
95
+ readonly 'detail.field.size': "大小";
96
+ readonly 'detail.field.turns': "轮数";
97
+ readonly 'detail.field.userMessages': "提问";
98
+ readonly 'detail.field.assistantMessages': "回复";
99
+ readonly 'detail.field.toolCalls': "工具调用";
100
+ readonly 'detail.field.models': "模型";
101
+ readonly 'detail.field.project': "项目";
102
+ readonly 'detail.field.sessionId': "会话 id";
103
+ readonly 'detail.field.trashDir': "回收站目录";
104
+ readonly 'detail.field.wasArchived': "原本归档";
105
+ readonly 'detail.value.yes': "是";
106
+ readonly 'detail.value.no': "否";
107
+ readonly 'detail.value.spanHours': "{n} 小时";
108
+ readonly 'detail.value.spanDays': "{n} 天";
109
+ readonly 'detail.messages.hidden': "另有 {count} 条宿主注入的上下文未显示";
110
+ readonly 'detail.messages.omitted': "还有 {count} 条未显示";
111
+ readonly 'detail.back': "← 返回清单";
112
+ readonly 'detail.list.label': "回收站清单";
113
+ readonly 'detail.role.user': "你";
114
+ readonly 'detail.role.assistant': "助手";
115
+ readonly 'menu.delete': "删除";
116
+ readonly 'menu.live': "会话正在运行,先停止再删除";
117
+ readonly 'dialog.title': "删除这个会话?";
118
+ readonly 'dialog.subagents': "含 {n} 个子会话,将一起移入回收站。";
119
+ readonly 'dialog.hint': "会话会移入回收站,之后可以随时恢复。彻底删除是回收站里的另一个动作。";
120
+ readonly 'dialog.confirm': "移入回收站";
121
+ readonly 'batch.row.pick': "勾选「{title}」以批量删除";
122
+ readonly 'batch.bar.count': "已选 {n} 条";
123
+ readonly 'batch.bar.pickHint': "点击会话行选择";
124
+ readonly 'batch.bar.kids': "含 {k} 个子会话,跟着整组走";
125
+ readonly 'batch.bar.review': "查看已选";
126
+ readonly 'batch.bar.reviewClose': "收起已选";
127
+ readonly 'batch.bar.remove': "取消勾选「{title}」";
128
+ readonly 'batch.bar.selectAll': "全选";
129
+ readonly 'batch.bar.selectNone': "取消全选";
130
+ readonly 'batch.run': "移入回收站 {n} 条";
131
+ readonly 'dialog.batch.title': "删除这些 {n} 个会话?";
132
+ readonly 'dialog.batch.retry': "重试这 {n} 条";
133
+ readonly 'undo.movedBatch': "{n} 条会话已移入回收站";
134
+ readonly 'undo.moved': "{title} 已移入回收站";
135
+ readonly 'undo.session': "会话";
136
+ readonly 'undo.undo': "撤销";
137
+ };
138
+ export type CopyKey = keyof typeof ZH;
139
+ export type CopyParams = Readonly<Record<string, string | number>>;
140
+ export type Translate = (key: CopyKey, params?: CopyParams) => string;
141
+ /** 服务缺席时的缺省:字典里那套 zh。**也是组件 prop 的缺省值**——它表达的是"这次没有 locale 服务",
142
+ * 与运行期 `createTranslate` 的降级分支是同一件事,不是"忘了传"。 */
143
+ export declare const zhTranslate: Translate;
144
+ /**
145
+ * 注册字典并交回本插件的 `t`。服务不在时返回 `zhTranslate`(不抛、不 pending)。
146
+ *
147
+ * 注册必须挂在 `ctx.effect` 上:`register` 返回的 disposer 是**官方契约**(卸载时要把字典摘掉),
148
+ * 而且同一命名空间的同一语言**只能注册一次**——effect 没挂,HMR 重挂就撞 "already has locale"。
149
+ */
150
+ export declare function createTranslate(ctx: ClientContextLike): Translate;
151
+ /**
152
+ * 把一句话里内嵌的那一段拆出来:`'…文件,之后没有任何副本可以恢复。'` 交给调用面用 `<b>` 包中间那段。
153
+ *
154
+ * 为什么不留成"前段 / 内嵌段 / 后段"三个键:那等于把英文和中文的语序各钉一次,两段翻译各自
155
+ * 成立、拼起来未必成立。整句一个键 + 一个占位符,语言自己决定那一段落在哪里。
156
+ */
157
+ export declare function splitOn(sentence: string, inner: string): [string, string, string];
158
+ /**
159
+ * 绝对时间那一句串。清单行的 `title`、确认框里的meta、详情的元信息三处要的是同一个写法,
160
+ * 各自 `new Date(...).toLocaleString()` 一遍迟早会漂成两种样子。
161
+ */
162
+ export declare function absoluteText(at: number): string;
163
+ /** 单位词本身(调用方把它填进各自语言的整句模板里,语序由各语言自己钉)。 */
164
+ export declare function unitWord(unit: RelativeTimeUnit, t: Translate): string;
165
+ /** 「7分钟前」这类**不带谓语**的相对短语;`now` 桶不套模板——"刚刚前"不是话。 */
166
+ export declare function agoText(at: number, now: number, t: Translate): string;
167
+ export {};
@@ -0,0 +1,40 @@
1
+ import { type Translate } from './copy.js';
2
+ import { type DeleteConfirmDeps } from './remote.js';
3
+ /**
4
+ * 被操作对象那张卡。`Modal`(`ui/Modal.tsx`,照官方抄的)的 `.sd-modal-body` 不设字号也不给层级 ⇒ 裸摆一行会话标题会和下面那句
5
+ * 提示**同字同色**,读不出"这才是要被删的东西"。配方与「彻底删除」框里那张摘要卡同一套(宿主设置面
6
+ * 的卡片 token:`bg-layer-2` + `border-l4` + `radius-lg`),两个对话框因此同形。
7
+ * 浅色下 `bg-layer-2` 与对话框自身同色,分隔全靠那根描边——宿主自己的卡片在浅色下也是这样。
8
+ */
9
+ export declare const TARGET_CARD: {
10
+ readonly padding: "10px 12px";
11
+ readonly background: "var(--dsw-alias-bg-layer-2)";
12
+ readonly border: "0.5px solid var(--dsw-alias-border-l4)";
13
+ readonly borderRadius: "var(--dsw-radius-lg)";
14
+ };
15
+ /** 菜单行写进现场的一条删除请求(槽位投影原样带过来,浏览器半不自己拼 id)。 */
16
+ export interface DeleteRequest {
17
+ /** 每次发布递增:换一个会话就重挂载确认框,busy / failure 不带着上一条请求的记忆。 */
18
+ seq: number;
19
+ /** 要删的会话 id——原样透给 `sessiondelete/delete`。 */
20
+ sessionId: string;
21
+ /** 删除那一刻的会话名(槽位投影的 `displayTitle`):只用于确认框文案与撤销提示。 */
22
+ title: string;
23
+ }
24
+ /** 菜单行调用:请求删除这个会话(确认框由 overlay 上的宿主渲染)。 */
25
+ export declare function requestDelete(input: {
26
+ sessionId: string;
27
+ title: string;
28
+ }): void;
29
+ /** 收起确认框(用户点「取消」/「关闭」、或删除成功后收工)。 */
30
+ export declare function clearDeleteRequest(): void;
31
+ /** 供宿主组件订阅。 */
32
+ export declare function useDeleteRequest(): DeleteRequest | null;
33
+ /**
34
+ * 常驻的确认框宿主。挂 `shell.overlay`(框架级浮层;槽位说明原文 "a toast stack or a status
35
+ * pill all belong here"):没有待确认的删除时什么都不渲染。
36
+ */
37
+ export declare function DeleteConfirmHost({ deps, t }: {
38
+ deps: DeleteConfirmDeps;
39
+ t?: Translate;
40
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,20 @@
1
+ import { type Translate } from './copy.js';
2
+ export interface DeleteMenuItemProps {
3
+ /** 宿主菜单槽投影的会话 id,原样透给 `sessiondelete/delete`(浏览器半不自己拼 id)。 */
4
+ sessionId: string;
5
+ /** 宿主菜单槽投影的 `displayTitle`:只用于确认框与提示文案,不参与任何请求。 */
6
+ title: string;
7
+ /** 会话在跑:这一行置灰,并把原因摆在行上。 */
8
+ live?: boolean;
9
+ /** 上方那条发丝线;缺省 true。多选入口排在删除前面时由它带线,删除行改成 false(`index.tsx`)。 */
10
+ separatorBefore?: boolean;
11
+ /**
12
+ * 菜单 owner 交下来的「收起菜单」——槽位注入的 `useMenuOpenState`(`index.tsx` 的 `DeleteRow` 接住)。
13
+ * 「关不关菜单是 owner 的决定」是官方对该槽位的契约;按下这一行时**先发请求、再收菜单**。
14
+ * 缺省不动,方便单独测这一行。
15
+ */
16
+ dismissMenu?: () => void;
17
+ /** 文案(缺省 = 内置 zh,见 `copy.ts`:这不是"忘了传",而是"这次没有 locale 服务")。 */
18
+ t?: Translate;
19
+ }
20
+ export declare function DeleteMenuItem({ sessionId, title, live, separatorBefore, dismissMenu, t, }: DeleteMenuItemProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ export declare const name = "@xrn1997/dsh-session-delete/client";
2
+ /**
3
+ * 只列**宿主公开面保证存在**的成员:`slots` 是浏览器侧槽位注册表
4
+ * (宿主外壳的冻结平台模块表里的静态键 `@deepseek-ai/dsh-client-ui-slots`),一定在。
5
+ * 取数通道不在这里——理由见文件头注(它不是 inject 能表达的事,写错了代价是进程死)。
6
+ * **`locale` 也不在这里**:宿主自己的 `ui-sidebar` 是硬注入它的,但那是"宿主知道自己那一代"的前提;
7
+ * 本插件要跨代活着 ⇒ 走 `ctx.get('locale')` 可选读,缺席时降级到内置 zh 文案(见 `copy.ts` 头注)。
8
+ * 写进 inject 就等于"这一代没有 locale 就不让整个客户端条目激活",而那等于宿主进程死。
9
+ */
10
+ export declare const inject: string[];
11
+ /**
12
+ * 入口:**同步返回、绝不抛**(见文件头注)。
13
+ *
14
+ * 注册挂在探测的续拍上,两条理由:① 前端启动自检只看 entry 声明的 `inject` 服务齐不齐(`slots`
15
+ * 一定在),不看它注册了几行;② 槽位注册是注册表的一次 mutation,渲染层按微任务批量订阅
16
+ * (`SlotCore` 的变更传播契约),晚一拍注册照常上屏——宿主侧栏对 `sidebar.panellist` 正是
17
+ * `ctx.slots.subscribe("sidebar.panellist", syncPanels)`(asar @44795361)。
18
+ */
19
+ export declare function apply(ctx: unknown): void;
@@ -0,0 +1,11 @@
1
+ import './ui/styles.js';
2
+ /**
3
+ * `memo` 是必需的而不是优化:`react-markdown@10.1.0` 里**一处 memoization 都没有**(读实
4
+ * `node_modules/react-markdown/lib/index.js:175-178`——`Markdown()` 每次调用都 `createProcessor(options)`
5
+ * 再 `runSync(parse(file))`,`MarkdownHooks` 那一支也一样),而面板任何一次重渲(悬停、↑↓、
6
+ * 「刚刚删除」那句要读 `Date.now()`)都会走到这里 ⇒ 不按住就是每条助手回复每拍重解析一遍。
7
+ * 按住的条件就只有 `text`:它是这里唯一的输入,样式挂在 `.sd-md` 上、不随渲染变。
8
+ */
9
+ export declare const MarkdownBody: import("react").NamedExoticComponent<{
10
+ text: string;
11
+ }>;