@dsh-workbench/core 0.3.5 → 0.3.7

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 (53) hide show
  1. package/client.js +1372 -1928
  2. package/dist/client/client.body.js +1372 -1928
  3. package/dist/client/index.js +1372 -1928
  4. package/dist/host/index.js +1569 -751
  5. package/dist/types/app/client.d.ts +76 -3355
  6. package/dist/types/app/layouts/env-gate/boot-gate.d.ts +8 -1
  7. package/dist/types/app/layouts/rightbar/market-busy.d.ts +28 -0
  8. package/dist/types/app/layouts/rightbar/market-hint.d.ts +7 -0
  9. package/dist/types/app/layouts/sidebar/DebugFooterAction.d.ts +2 -1
  10. package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +2 -1
  11. package/dist/types/app/layouts/sidebar/SessionList.d.ts +3 -1
  12. package/dist/types/app/layouts/sidebar/session-pick.d.ts +2 -0
  13. package/dist/types/app/layouts/sidebar/use-attention.d.ts +7 -1
  14. package/dist/types/modules/panels/client.d.ts +1 -1
  15. package/dist/types/modules/panels/model/approval.d.ts +8 -0
  16. package/dist/types/modules/panels/model/diy-context.d.ts +9 -0
  17. package/dist/types/modules/panels/model/diy-record-beat.test.d.ts +1 -0
  18. package/dist/types/modules/panels/model/diy-record-close.test.d.ts +1 -0
  19. package/dist/types/modules/panels/model/diy-record-hook.test.d.ts +1 -0
  20. package/dist/types/modules/panels/model/diy-record-open.d.ts +28 -0
  21. package/dist/types/modules/panels/model/diy-record-open.test.d.ts +1 -0
  22. package/dist/types/modules/panels/model/diy-record-shot.test.d.ts +1 -0
  23. package/dist/types/modules/panels/model/diy-record-text.d.ts +16 -1
  24. package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -1
  25. package/dist/types/modules/panels/model/diy-record.d.ts +40 -1
  26. package/dist/types/modules/panels/model/diy-report.d.ts +1 -1
  27. package/dist/types/modules/panels/model/long-task-tools.d.ts +15 -0
  28. package/dist/types/modules/panels/model/long-task-tools.test.d.ts +1 -0
  29. package/dist/types/modules/panels/unit.d.ts +5 -3
  30. package/dist/types/modules/sessions/index.d.ts +1 -1
  31. package/dist/types/modules/sessions/model/attention-long-task.test.d.ts +1 -0
  32. package/dist/types/modules/sessions/model/attention.d.ts +23 -1
  33. package/dist/types/modules/sessions/model/session-node.d.ts +12 -1
  34. package/dist/types/shared/api/host/approval-call.d.ts +16 -0
  35. package/dist/types/shared/api/host/long-task-notice.d.ts +30 -0
  36. package/dist/types/shared/api/host/long-tasks.d.ts +10 -0
  37. package/dist/types/shared/api/host/mount-units.d.ts +3 -1
  38. package/dist/types/shared/api/host/preflight.d.ts +7 -1
  39. package/dist/types/shared/api/host/workshop-root.d.ts +9 -0
  40. package/dist/types/shared/api/long-tasks.d.ts +19 -0
  41. package/dist/types/shared/hooks/panel-state-report.d.ts +1 -1
  42. package/dist/types/shared/hooks/use-diy-debug.d.ts +1 -1
  43. package/dist/types/shared/hooks/use-diy-record.d.ts +1 -1
  44. package/dist/types/shared/hooks/use-long-task.d.ts +8 -2
  45. package/dist/types/shared/hooks/use-shell-long-tasks.d.ts +8 -0
  46. package/package.json +2 -2
  47. package/skills/workbench-module-diy/BUILD.md +1 -1
  48. package/skills/workbench-module-diy/FAMILY.md +1 -1
  49. package/skills/workbench-module-diy/KIT.md +26 -26
  50. package/skills/workbench-module-diy/PANEL.md +1 -1
  51. package/skills/workbench-module-diy/TOOLING.md +5 -1
  52. package/skills/workbench-module-diy/TOOLS.md +22 -2
  53. package/skills/workbench-module-diy/WRITES.md +20 -3
@@ -1,4 +1,4 @@
1
- import type { DebugState } from '@dsh-workbench/shared';
1
+ import type { DebugState } from '@dsh-workbench/shared/host';
2
2
  import type { HostWrite } from './use-host-write';
3
3
  /**
4
4
  * 调试约定的读路由(宿主侧那半同名)。
@@ -1,4 +1,4 @@
1
- import type { RecordState } from '@dsh-workbench/shared';
1
+ import type { RecordState } from '@dsh-workbench/shared/host';
2
2
  import type { HostWrite } from './use-host-write';
3
3
  /**
4
4
  * 录制交接的读路由(宿主侧那半同名)。
@@ -1,14 +1,20 @@
1
- import { type LongTaskStatus } from '@dsh-workbench/shared';
1
+ import { type LongTaskStatus, type WriteOutcome } from '@dsh-workbench/shared';
2
2
  /**
3
- * 这一单元的长任务清单:`runs` 是这次读交回的那一批(读数还没回来时是空数组)。
3
+ * 这一单元的长任务清单与停止口:`runs` 是这次读交回的那一批(读数还没回来时是空数组);
4
+ * `stop(id)` 请外壳停某一趟 —— 回执没成就带回一句人话(调用点自己决定说不说);
5
+ * `stopping` 是正在请求停止的那一趟 id(没有就是 `null`),`stopFailed` 是上一次停止请求失败那句话(没有就是 `null`)。
4
6
  */
5
7
  export interface LongTaskFeed<T> {
6
8
  readonly runs: readonly T[];
9
+ stop(id: string): Promise<WriteOutcome<unknown>>;
10
+ readonly stopping: string | null;
11
+ readonly stopFailed: string | null;
7
12
  }
8
13
  /**
9
14
  * 卡片要轮询长任务那一行时调用:有在跑的(`running` / `needs-login`)按 `longTaskPollMs` 走快档、空闲走慢档;
10
15
  * `workspace` 为 `null`、`mocked: true` 或 `enabled: false` 时不轮。换单元 / 换工作区就是换读数身份,
11
16
  * 旧那批立刻清空、读数回来前 `runs` 是空数组。
17
+ * `stop(id)` 走 actions 通道的 `<单元>/runs/stop`;不在跑的那一趟会带回一句人话,不抛错。
12
18
  */
13
19
  export declare function useLongTask<T extends {
14
20
  readonly id: string;
@@ -0,0 +1,8 @@
1
+ import { type ShellLongTaskRow } from '../api/long-tasks';
2
+ /**
3
+ * 左栏那颗 loading 点读的长任务清单:**全进程还在跑的**那些(不分单元、不分工作区,每行带它属于
4
+ * 哪个工作区、是哪个会话起的)。起 / 走一步 / 收尾宿主都会推一条事实,收到就当场重读;那个定时器
5
+ * 只是兜底 —— 有在跑的按快档问,都闲着按慢档。
6
+ * @returns 还在跑的长任务;读数还没回来时是空数组。
7
+ */
8
+ export declare function useShellLongTasks(): readonly ShellLongTaskRow[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -87,7 +87,7 @@
87
87
  "access": "public"
88
88
  },
89
89
  "dependencies": {
90
- "@dsh-workbench/shared": "0.3.5"
90
+ "@dsh-workbench/shared": "0.3.7"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -166,7 +166,7 @@ export function apply(ctx) {
166
166
  `card` 拿到 `{ t, world, refreshToken, onRefresh, onSummary }`(`world.current.workspace` 就是当前项目),
167
167
  返回一棵元素树。**三态用外壳给的组件,别自己画**:
168
168
  加载 → `kit.components.SkeletonRows`;空 → `kit.components.EmptyState`(不带按钮);
169
- 出错 → `kit.components.BlockerState` + `kit.components.blockerOfError(error, t)`。
169
+ 出错 → `kit.components.BlockerState` + `kit.lib.blockerOfError(error, t)`。
170
170
  ⚠️ **`idle` 是独立的一格**(没工作区、或取数被 `enabled: false` 关掉时),不许画成"空" ——
171
171
  "空"和"还没开始"是两件事,说错了他会去查他根本没配的东西。
172
172
 
@@ -45,7 +45,7 @@
45
45
  | 工具 | 干什么 |
46
46
  |---|---|
47
47
  | `_record_prepare` | 开一扇窗**顶到最前**(**要用户点头一次**)、装浮条、臂上捕获。参数 `unit` / `url` / `doing`,可选 `expect:{method,urlContains}`;**不给 `url` 当场拒**,**跨域也当场拒**(那个模块的 `design.json` 说过读源时,只许同域录;还没说过就不拦)。返回带 `wrote: false` |
48
- | `_record_read` | 取那一段的**清单**:`rows`(每行 `index` / `key` / `at` / `method` / `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`)· `hookCaught`(钩子这一段**原始**条数)· `capturedLines` / `capturedInSegment` / `capturedOutside` · `dropped` · `noBody` · `nav` · `captureRound`(会话捕获**最近一轮**成没成:`ok` / `fail` / 空 = 还没回)。**清单一条都不删、也不回体**:页面钩子(`[t0,t1]` 段)与会话捕获(整场按段切)合并,**同名只配掉一条钩子行**(缺的 key / 入参 / 出参由捕获那条补上);捕获里两条同 method+url 是**两次真调用,都留着**。**`expect` 不再是过滤器**:它只给命中的行标 ⭐,⭐ 排在前面(不然只列最后十几条时它可能不在正文里);不设 expect 时 ⭐ = 非 GET 或带 request body。要哪一条的原文就带它的 `key`(或 `index`)去 `_record_detail`。没按开始 / 还在录都如实说,不假装"没有请求"。可选 `waitMs`(默认 0 = 立刻回,帽 15 分钟):等不到就回 `timedOut` 并挂一条 notice 叫醒你 —— **一场就是一个周期(开始 → 结束)**,结束之后要再录就**重新 `_record_prepare` 一场**(同场没有"再按一次开始"这回事)。**他按「结束录制」不用他再跟你说一声**:录制期间宿主每 3 秒自己看一眼那一页(浮条在目标页里、推不到宿主;宿主能叫醒你的通道只有 `deliverDebugNotice`),一看到 `finished` 就推状态并发一条 notice 叫你来 `read` |
48
+ | `_record_read` | 取那一段的**清单**:`rows`(每行 `index` / `key` / `at` / `method` / `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`)· `hookCaught`(钩子这一段**原始**条数)· `capturedLines` / `capturedInSegment` / `capturedOutside` · `dropped` · `noBody` · `nav` · `captureRound`(会话捕获**最近一轮**成没成:`ok` / `fail` / 空 = 还没回)。**清单一条都不删、也不回体**:页面钩子(`[t0,t1]` 段)与会话捕获(整场按段切)合并,**同名只配掉一条钩子行**(缺的 key / 入参 / 出参由捕获那条补上);捕获里两条同 method+url 是**两次真调用,都留着**。**`expect` 不再是过滤器**:它只给命中的行标 ⭐,⭐ 排在前面(不然只列最后十几条时它可能不在正文里);不设 expect 时 ⭐ = 非 GET 或带 request body。**正文每一行把它要的两样直接摆出来**:`· [0] ⭐ POST 站点/路径(捕获 · 入参 有 · 出参 40 B · at 1792583917392)` —— 开头那串就是 `key` 原文(opencli 给的,可能带 `#2`),`at` 是毫秒时间戳;**没有 `key` 的钩子行**写成 `method url(钩子 · … · 无 key)`,那种行只在还开着时按 `index` 取得到。要哪一条的原文就带它的 `key`(或 `index`)去 `_record_detail`。没按开始 / 还在录都如实说,不假装"没有请求"。可选 `waitMs`(默认 0 = 立刻回,帽 15 分钟):等不到就回 `timedOut` 并挂一条 notice 叫醒你 —— **一场就是一个周期(开始 → 结束)**,结束之后要再录就**重新 `_record_prepare` 一场**(同场没有"再按一次开始"这回事)。**他按「结束录制」不用他再跟你说一声**:录制期间宿主每 3 秒自己看一眼那一页(浮条在目标页里、推不到宿主;宿主能叫醒你的通道只有 `deliverDebugNotice`),一看到 `finished` 就推状态并发一条 notice 叫你来 `read` |
49
49
  | `_record_detail` | 取**某一条**的原文:入参(`request.body`)与出参(`response body`),外加请求头(可能含凭据,别落进 `design.json`)。参数 `key`(清单里那一条,原样给、含 `#2` 这种后缀)或 `index`(清单里的序号,只在那一场还开着时可用);**同一个 key 跨轮会重名**(opencli 的 key 计数器每批重置),所以再带上清单里那行的 `at` 才钉得住是哪一次(不给就取最早那条,并在正文里说重名几次)。可选 `unit` —— 那一场**收工之后**要给 unit,我去读 `shots/rec-<unit>.jsonl.last`(`_record_stop` 会把原文留成 `.last`,下一场 prepare 开录时清掉它)。取不到时如实说"两边都没有",并给兜底:DevTools 复制 curl。可选 `maxBody`(默认 60000 字符,0 = 不限) |
50
50
  | `_record_stop` | 停捕获、收掉那个标签、清状态(**收工前先读一次捕获文件**,与 read 同一套切段 / 合并;**原文另存成 `.last`**,所以收工之后还能 `_record_detail`;没挪成就不说"原文留在…")。回一段能直接落 `design.json` 的原文(`probeCommand`)—— **只认第一条带 body 的**;一条都没有就回空串,并如实说"只有方法 / URL,body 拿不到 —— 补到 body 再落"(没 body 的写命令落下去是一条**会打错的**命令)。**先 read(或先 detail 完)再 stop** |
51
51
 
@@ -37,7 +37,7 @@ h(Card, { t: props.t, title: t('myunit.cardTitle'),
37
37
  | 在读、且你已经有大概形状 | `SkeletonRows`(骨架) |
38
38
  | 一句说明 / 一句报错 | `Note`(`tone: 'problem'` 是报错) |
39
39
 
40
- prop 与默认值看 `.d.ts`;下面是**怎么拼**。
40
+ prop 与默认值看 `.d.ts`。
41
41
 
42
42
  ```tsx
43
43
  h(CenteredState, { icon: null, title: t('myunit.loading'), hint: t('myunit.loadingHint'), compact: true })
@@ -61,8 +61,7 @@ h(Rows, null,
61
61
 
62
62
  **`ActivityBlock` / `ActivityLine` / `ActivityAction`** —— "它做过什么"那一块(prop 见 `.d.ts`)。时间一律用它。
63
63
 
64
- **`LongTaskRow`** —— 长任务那一行(四态 + 步骤 + 原文,prop 见 `.d.ts`);
65
- 跑得久的事(发布、扫描、迁移)都用它,别自己画进度。
64
+ **`LongTaskRow`** —— 长任务那一行(五态,prop 见 `.d.ts`);别自己画进度。传 `stop` 要宿主认 `signal`、peer 抬到带它那版([TOOLS.md](TOOLS.md) §9)。
66
65
 
67
66
  **`ApprovalDetail` / `ApprovalPlan`** —— 审批屏那两格(写操作才要点头);口径与那张 ASCII 见 [DESIGN.md](DESIGN.md) §3 末节。
68
67
 
@@ -72,22 +71,23 @@ h(Rows, null,
72
71
  第一处错误怎么带进视口、操作区怎么排全归它。**一张完整的表单就用它**(口径见 §5,逐件看 `.d.ts`)。
73
72
 
74
73
  ```tsx
75
- h(Form, {
74
+ h(Form<{ branch: string }>, {
76
75
  fields: [
77
- { key: 'branch', label: t('myunit.branch'), value: branch, // 值只读给校验看,读写仍归你
76
+ { key: 'branch', label: t('myunit.branch'), value: draft.branch, onChange: onBranch, // 值与改写同一处
78
77
  validate: (v) => (v === '' ? t('myunit.branchRequired') : undefined),
79
78
  control: (b) => h(TextField, { id: b.id, name: b.name, invalid: b.invalid,
80
- disabled: b.disabled, value: branch, onChange: (e) => setBranch(e.currentTarget.value) }) },
79
+ disabled: b.disabled, value: b.value, onChange: (e) => b.onChange(e.currentTarget.value) }) },
81
80
  ],
82
- submit: { label: t('myunit.save'), busyLabel: t('myunit.saving') },
83
- cancel: { label: t('myunit.cancel'), onClick: onCancel }, // 排在提交钮左边
84
- extra: [{ key: 'retry', label: t('myunit.retry'), onClick: onRetry }], // 其余动作排最左
85
- onSubmit: save, // 有错就不调它
81
+ submit: { label: t('myunit.save'), busyLabel: t('myunit.saving') }, // cancel / extra 见 `.d.ts`
82
+ onSubmit: save, // 有错就不调它;values 就是这个类型
86
83
  })
87
84
  ```
88
85
 
89
- **值仍归调用点**(`Form` 一个值都不存):`binding` 只递 `id` / `name` / `invalid` / `disabled`,
90
- `value` 与 `onChange` 由 `control` 里自己接。**控件永远由 `control` 给** —— 没有"给个 `options` 自己选控件"的 DSL。
86
+ **`Form<Values>` 把每一格的键与值类型钉住**(不写类型参数 = 默认档、不钉,每格要自己收窄);
87
+ **值与它的改写写在同一格**(`value` + `onChange`,`control` 从绑定取 `binding.value` / `binding.onChange`
88
+ —— `Form` 不持有值);**会 `visible: false` 的键在 `Values` 里写可选**(`branch?: string` ⇒ 值是
89
+ `string | undefined`)。`validate` 是同步纯函数、第二参是整张值表(跨字段读得到别的格);**控件永远由
90
+ `control` 给**(没有"给个 `options` 自己选控件"的 DSL)。
91
91
 
92
92
  **控件那几件** —— `Field`(没进 `FormRow` 时兜住标签 + 说明)· `TextField`(`invalid` 标红)·
93
93
  `ChoiceGroup`(`multi` 区分多选)· `SelectField`(默认常规下拉框,`variant: 'ghost'` 是无边框那档,
@@ -103,7 +103,11 @@ h(Form, {
103
103
  **别手写 `<button>` / `<input>` / `<select>`**(dsh 有 `Button`,kit 有上面这几件;手写的判据会红)。
104
104
  `<textarea>` 例外(dsh 与 kit 都没有多行输入)。
105
105
 
106
- 怎么排成一行(标签位置、错误态、尺寸)见 §5。
106
+ **`ConfirmBar`** —— 「先问一句再动手」那一条(标题 / 说明 / 键值 / 失败那句 / 动作区一次收口):
107
+ 键值走 `Rows` / `Row`、错走 `Note tone="problem"`、动作走 `FormActions` —— **别自己拼这四个槽**。
108
+ **要不要问归调用点**(同 `FormActions`:`danger` 只标"破坏性")。口径见 ADR-0073。
109
+
110
+ 怎么排(标签位置、错误态、尺寸)见 §5。
107
111
 
108
112
  ## 4′ hook 怎么搭(逐件看 `.d.ts`)
109
113
 
@@ -116,21 +120,16 @@ kit 的 hook 也分四个场合,**别自己 `useState` + `setTimeout` 造一
116
120
  | "至少转一会儿"的忙态 | `useVisibleBusy`(投影那两件 `busyView` / `heldPhase`) |
117
121
  | 一格**卡片**起步(gate + 忙 + 探针 + 上报 + blocker 一次叠好) | `useUnitBoard`(`read(gated)` 里自己调 `useApiResource`) |
118
122
  | 只要一格的能力门 / 整张能力表 | `useCapabilityGate` / `useCapabilityMap` |
119
- | 写(**别在组件里 `fetch`**) / 轮询长任务 | `useHostWrite` / `useLongTask`(画用 `LongTaskRow`、点用 `longTaskDot`) |
123
+ | 写(**别在组件里 `fetch`**) / 轮询长任务 | `useHostWrite` / `useLongTask`(画用 `LongTaskRow` —— 圆点与阶段它自己带) |
120
124
 
121
125
  取数那条链的图见 [LINKAGE.md](LINKAGE.md) §8,写通道与配置见 [PANEL.md](PANEL.md) §5。
122
126
 
123
127
  ## 5 表单这一层:`Form`(面)→ 行 → 控件
124
128
 
125
- 表单三层:**面**(`Form`)→ **行**(`FormRows` + `FormRow`)→ **控件**(`TextField` / `SelectField` /
126
- `ChoiceGroup`)。**一张表单一个面**。
127
-
128
- **整张表单用 `Form`**(写法见 §4):`fields` 配置进;**值仍归调用点**(`value` / `onChange` 由 `control`
129
- 里自己接,`binding` 只递 `id` / `name` / `invalid` / `disabled`);**控件永远由 `control` 给**(没有"给个
130
- `options` 自己选控件"的 DSL);`validate` 是同步纯函数;`visible: false` 的那格当它不存在(不渲染 /
131
- 不校验 / 不进 `values`)。**别再自己摆操作区** —— 它自己组 `[...extra, ...cancel, submit(primary)]`
132
- (提交钮永远在右;「一会在左一会在右」就是自己摆的结果)。其余口径(时机 / 第一处错误 / `busy` /
133
- `disabled` / `key`)看它的 JSDoc 与 ADR-0068。
129
+ 表单三层:**面**(`Form`,写法见 §4:值 / 校验 / 操作区全归它)→ **行**(`FormRows` + `FormRow`)→
130
+ **控件**(`TextField` / `SelectField` / `ChoiceGroup`)。**一张表单一个面**。**别再自己摆操作区** ——
131
+ 它自己组 `[...extra, ...cancel, submit(primary)]`(提交钮永远在右;「一会在左一会在右」就是自己摆的结果);
132
+ 其余口径(时机 / 第一处错误 / `busy` / `disabled`)看它的 JSDoc 与 ADR-0068 / ADR-0074。
134
133
 
135
134
  `FormRows` + `FormRow` + `FormActions` 留给**不是整张表单**的地方:单行(一格字段嵌在别处,比如卡片里
136
135
  那一条设定)、非表单的一组动作(工具栏、卡头那几颗钮)。那里才自己拼这三件:
@@ -153,7 +152,8 @@ h(FormRows, null,
153
152
  `ChoiceGroup`(`loading` / `emptyText` / `variant` 两件一个意思)。**loading 那颗一律是 `Spinner`**。
154
153
 
155
154
  **尺寸口径**:字段控件默认 **28**,别自己调高;行距只有一档、标签列宽也归 `FormRow(s)`
156
- (长标签不折行、同一组共用一个标签列,别自己给标签写宽度)。非行的子项(如分组标题)自动通栏。
155
+ (长标签不折行、同一组共用一个标签列,别自己给标签写宽度);**控件的宽度也别自己写**
156
+ (字段控件自己吃满控件槽那一格,ADR-0052 §13)。非行的子项(如分组标题)自动通栏。
157
157
 
158
158
  **反例(别这么写)**:
159
159
 
@@ -164,8 +164,8 @@ h('form', …) // ← 裸 <form> / 手写行槽
164
164
  ```
165
165
 
166
166
  **时点**:这一层随外壳发版后模块才能用(`check --only kit` 拿 peer 下限那份外壳比,不抬必红)。
167
- **`Form` 是新成员**:要用它得等外壳**发一版含它的版本**,再把 `@dsh-workbench/core` 的 peer
168
- 下限抬到**那一版**(版本号随发版填,别照抄别的件);`FormRow(s)` / `FormActions` 的下限是另一件事。
167
+ **`Form` / `ConfirmBar` 是新成员**:要用得等外壳**发一版含它的版本**,再把 peer 下限抬到**那一版**
168
+ (版本随发版填,别照抄别的件);`FormRow(s)` / `FormActions` 的下限是另一件事。
169
169
 
170
170
  ## 6 别的
171
171
 
@@ -119,7 +119,7 @@ agent 那一侧也要能通过模块自己的能力读到同一份偏好。
119
119
  - **默认收起**:历史、明细折叠,露出最近一条;只有一条时不给折叠三角(没有可展开的东西)。
120
120
  - **屏幕上的字越少越好**:他能看懂的(红点绿点、数字)就不用再写一段规则解释;删掉的东西不要留一行说明。
121
121
  - **默认状态点**只有四种意思:在做 / 做完了 / 出错 / 没在跑。
122
- - **外链一律用 kit 的 `Link`**(`h(Link, { href: … }, 文案)`):新窗口 + `rel` + 自带外链箭头,
122
+ - **外链一律用 kit 的 `Link`**(`h(Link, { href: … }, 文案)` —— `h` 从 `react` 引,**不在 kit 面里**):新窗口 + `rel` + 自带外链箭头,
123
123
  **它自带的样子就是唯一一份,别去掉箭头、别换箭头、别自己调**(没有 `icon` 这个口子)——
124
124
  全仓的链接必须长得一样。
125
125
  模块源码里手写 `<a>` / `h('a', …)` 会被「样式归属」判据**直接判红**。
@@ -27,7 +27,8 @@
27
27
  | 发货前的清单([WIRING.md](WIRING.md) §5) | `dsh-wb-module publish`(门禁 + 打包清单 `files` 漏项,只 dry run) |
28
28
  | 干净机器上的第一脚([SKILL.md](SKILL.md) 第 4 步) | 启动器自愈骨架 + PATH 上的 `dsh-wb-module` |
29
29
  | 会写的工具要点几次头([TOOLS.md](TOOLS.md) §6) | `writes: true` + `needsApproval` + `approvalDetail`,`check --only contracts` 查 |
30
- | 跑一会儿的事怎么让用户看见([TOOLS.md](TOOLS.md) §9) | `longTasks` + kit 的 `useLongTask` / `LongTaskRow`,`check --only contracts` 查 |
30
+ | 跑一会儿的事怎么让用户看见([TOOLS.md](TOOLS.md) §9) | `longTasks`(注册表含 `stop`)+ kit 的 `useLongTask` / `LongTaskRow`,`check --only contracts` 查**形状** |
31
+ | agent 要能看 / 停长任务([TOOLS.md](TOOLS.md) §9) | **外壳自带**的 `workbench_long_tasks_list` / `workbench_long_tasks_stop`(自己起的免审批、别人的 / 用户按的要审批、外地工作区直接拒绝)—— 模块不用自己写;"谁起的"靠 `runs.start({ …, session: sessionIdOf(exec) ?? undefined })` 记(**不记 = 用户按的**) |
31
32
  | 这个能力要靠什么才跑得起来([TOOLS.md](TOOLS.md) §8) | `manifest.env` + `libInstallCommand`,`check --only contracts` 查 |
32
33
  | 配置存不存([PANEL.md](PANEL.md) §5.1) | `config[].persistence`(`prefs` / `view`)+ `check --only contracts` |
33
34
  | agent 动完面板哪一格变([LINKAGE.md](LINKAGE.md) §8) | `factsOf` + `factOfRoute` / `context` / `warmReads` / `values`,`check --only contracts` 只查形状 |
@@ -47,5 +48,8 @@
47
48
  - **安全取舍**:"说不清这次会发生什么就不许问" —— [TOOLS.md](TOOLS.md) §6
48
49
  - **联动颗粒度**:动的是整卡、某一行、还是某一列 —— [LINKAGE.md](LINKAGE.md) §1
49
50
  (工具只能保证"形状对","该多窄"是判断)
51
+ - **停不停得了**:那颗停止钮画不画(`LongTaskRow` 的 `stop`)、`run` 收到的 `signal` 认不认
52
+ ([TOOLS.md](TOOLS.md) §9)—— 判据只看形状;"外壳把这一趟标成 `stopped`、你的活儿还在跑"这件事
53
+ 机器看不出来,是模块自己的取舍
50
54
 
51
55
  工具只负责把**事实**摆出来(状态、报错原文、预览结果),判断仍然是人 / agent 的事。
@@ -87,9 +87,29 @@
87
87
 
88
88
  ## 9 长任务:跑一会儿的事
89
89
 
90
- 跑几十秒的事(构建、发布、批量删)做成**长任务**。宿主半侧用 `mount` deps 里的 `longTasks` 建注册表并挂回返回值:`longTasks.create({ key, run, step, outcome, view })` —— `run({ payload, onStep })` 里用 `onStep({ kind: 'start', text })` 报步骤、`return` 结果;`step`/`outcome`/`view` 各映射一段给界面;返回里带上 `longTasks: runs`。
90
+ 跑几十秒的事(构建、发布、批量删)做成**长任务**。宿主半侧用 `mount` deps 里的 `longTasks` 建注册表并挂回返回值:`longTasks.create({ key, run, step, outcome, view, stopped })` —— `run({ payload, onStep, signal })` 里用 `onStep({ kind: 'start', text })` 报步骤、`return` 结果;`step`/`outcome`/`view` 各映射一段给界面;返回里带上 `longTasks: runs`。
91
91
 
92
- 浏览器半侧用 kit 现成的 `useLongTask`(→ `feed.runs`),画用 `LongTaskRow` 或 `longTaskDot`(每条的字段与四种状态见 `.d.ts`);**契约里没有"停止"**,用户要中途停得自己在 `run` 里看信号。判据(`check --only contracts`):注册表五个方法要全、`create` 的 `key`/`run`/`view` 不能缺、产物里必须有 `useLongTask`/`LongTaskRow`/`longTaskDot` 之一。
92
+ **来源要记**(起这一趟的是谁):在起它的那把工具里用 `execute(args, exec)` 给的现场,`runs.start({ …, session: sessionIdOf(exec) ?? undefined })`(`sessionIdOf` 从 `@dsh-workbench/shared/host` 取)。**不记 = 用户在界面上按的**(或这一版之前起的)—— 保守的那一侧:停它要走审批。
93
+
94
+ 浏览器半侧用 kit 现成的 `useLongTask`(→ `feed.runs` / `feed.stop(id)` / `feed.stopping` / `feed.stopFailed`),画用 `LongTaskRow` 或 `longTaskDot`(每条的字段与**五种**状态 —— `running` 进行中 / `needs-login` 要登录 / `stopped` 停下的(灰点)/ `ok` 完成 / `failed` 出错 —— 见 `.d.ts`)。`stopped` 是后加的第五态:自己穷举过四态的地方(`switch` 没 `default`)会 tsc 报错,那是提醒你把它补上。
95
+
96
+ **要给用户那颗「停止」钮,两半都得做**:
97
+
98
+ - **认信号**:`run` 收到的 `signal` 被 abort = **有人按了停止**(用户在卡片上按的,或 agent 用 `workbench_long_tasks_stop` 停的)。在你能收手的地方(每轮循环、每条子命令前后)看一眼 `signal.aborted` 就收工。**不认就别画那颗钮** —— 画了就是骗人:外壳只会把这一趟标成 `stopped`,你的活儿还在跑(内部是 opencli 子进程那类编排,今天最多做到"我们不再等它",平台侧那趟照旧跑完)。
99
+ - **画钮**:给那一行的 `LongTaskRow` 传 `stop`(`onStop: () => feed.stop(run.id)`、`busy: feed.stopping === run.id`、`failed: feed.stopFailed`、`label` 用 dsh 那句 `停止任务 {label}`(`{label}` = 这一趟的名字)、`askLabel: '确认停止'`)。**词与取值照 dsh 任务列表那行来**:没上膛的 aria `停止任务 {label}` → 上膛后可见字 `确认停止`、aria `再次点击确认停止`,3 秒自己落膛。**失败那半是我们自己的口径**:不往钮里写"停止失败"四个字(那是 dsh 词表里的一项),而是把这次请求失败的**原话**画在那一行下面 —— `failed: feed.stopFailed` 给什么就画什么(例如 `网络不通,停止请求没发出去`),不给就不画。两按确认(第一次只上膛、再按才真发、请求在飞时按不动)归组件,调用点只管上面这几样;**没传 `stop` 的行一个像素都不变**。
100
+ - **多行同时在跑要留意**:`feed.stopping` 与 `feed.stopFailed` 都只有**一格**("最后一次"),不分行 —— 停 A 再停 B,`stopping` 变成 B,A 那行**提前解禁**(再按一下只是再发一发,外壳回 `{ ok: true, stopped: false }`:无害但白费);某一次停止失败,那句原文会画在**每一个**你传了 `failed: feed.stopFailed` 的行下面(`LongTaskRow` 只认 `failed` 这个字符串,不认 id)。**要分行报成败、要精确禁用就自己按 id 记**(比如 `useState` 记"在飞的那趟 id"与"那趟失败的话");单行卡片用现成这一格就够,嫌它糊就别把 `stopFailed` 传给所有行。
101
+
102
+ **外壳自带两把通用工具,你不用自己写**:`workbench_long_tasks_list`(读)看**当前工作区**跑着什么、**谁起的**(`我起的` / `用户按的` / `别的会话起的`),`workbench_long_tasks_stop`(写)停一趟。审批口径:**自己起的免审批**(这一趟记的 `session` 就是调用方那个会话);**别人的 / 用户按的 / 没记来源的要审批**(那一屏外壳自己认领,画"要停哪一趟"+ 一句"停了只是让外壳发出中断信号");**不在你当前工作区的直接拒绝**(工作区是边界,仓内 ADR-0018)。
103
+
104
+ **被停时会通知发起那次运行的会话**:用户(或别的会话)停下你起的这一趟,外壳给**起它的那个会话**发一条 notice —— 那会话正在跑就 inject 一条,闲着就跳过;**跑完 / 失败不发**(那是你自己那把工具的活,它起的那趟它自己报)。你不用在模块里自己发。
105
+
106
+ **为什么不走 `ctx.jobs`**:dsh 的 job 归**一次 agent 会话**(teardown 主动取消、读口按会话围栏),我们的长跑归**工作区的一个目标**(`dispose` 刻意不取消)—— 归属轴不同;换过去要改归属口径,而它的 `JobStatus` 里没有 `needs-login`、`JobView` 里也没有工作区字段。理由、代价与"只借形状"那四件见仓内 ADR-0069(这里不链仓内路径)。
107
+
108
+ 判据(`check --only contracts`):注册表**七个**方法要全(含 `stop` / `live`)、`create` 的 `key`/`run`/`view` 不能缺、产物里必须有 `useLongTask`/`LongTaskRow`/`longTaskDot` 之一。**它只查形状与"画得出来"** —— "你真认了 signal 吗、真收得住手吗"不在它的射程里(那只有你自己知道,见上面第一条)。
109
+
110
+ **左栏那颗 loading 点不用你做**:有长任务在跑时,起它的那条会话行、以及项目下拉里有它的那个工作区那一行,各自那颗状态点会自己转(外壳按 `live()` 读,不看是谁起的;用户在面板上按的那趟也会点亮它所在工作区那一行)。
111
+
112
+ **要用 `stop` 这个新 prop / 新返回字段,就得装带这次改动的那份外壳**:把自己 `peerDependencies['@dsh-workbench/core']` 的下限抬到那一版 —— `check --only kit` 盯着这条(产物实际取的 kit 成员 ⊆ 你 peer 下限那份外壳的 kit)。**同理**:`start` 收 `session`、外壳那两把 `workbench_long_tasks_*` 工具,也都是这一版才有的。
93
113
 
94
114
  ## 10 用户自己按的那个入口 / 范例
95
115
 
@@ -43,10 +43,18 @@
43
43
  站点、录哪一下、落在哪、录完不替他切回来),窗里有一根浮条。请他 ① 按「开始录制」 ② 做**一次**那动作 ③ 按「结束录制」;
44
44
  **别让他点第二次**。`url` 必须是**这个模块读源同域**的页面 —— 模块的 `design.json` 说过读源时,跨域会**当场拒**
45
45
  (没说过就不拦;那一条比的是"别把别处的请求原文带进这个模块")。
46
+ - **再让他按一下浮条上的「截图」**:时机是**把表填对之后、点提交之前**(提交完表单就没了)——
47
+ 这一下**不另外审批**(屏幕在 prepare 那一拍已经借过)。它同时落下两样东西:一张整页图(落 `shots/rec-shot-<unit>-<n>.png`)
48
+ 与一份**页面快照**(每个控件的 `name`/`id`、标签、`required`/`aria-required`/容器 class/`*` 这类必填痕迹、当时的填值、是否空、
49
+ 有没有被改过)。**提交那一刻我们会自动再抓一份**,所以他忘了按也不至于空手。
50
+ 他按了截图之后,浮条上会多一个「已拍 N 张」,左栏底部那句也跟着带张数。
46
51
  - `workbench_diy_record_read` 取那一段(他按完结束再取):回的是**清单** `rows`(每条 `index` / `key` / `method` /
47
52
  `url` / `status` / `ct` / `size` / `source` / `hasRequest` / `hasResponse` / `likelyWrite` / `marked`),
48
- **一条都不删、也不回体**;`expect` 只用来标 ⭐ 并把 ⭐ 排到前面。要哪一条的原文再调
49
- **`workbench_diy_record_detail`**(给清单里那条的 `key`,或序号 `index`;**同一个 key 跨轮会重名**,
53
+ **一条都不删、也不回体**;`expect` 只用来标 ⭐ 并把 ⭐ 排到前面。**正文每一行就把它要的两样摆出来了**:
54
+ `· [0] ⭐ POST 站点/路径(捕获 · 入参 有 · 出参 40 B · at 1792583917392)` —— 开头那串就是 `key` 原文(opencli 给的,
55
+ 可能带 `#2` 这种后缀),`at` 是毫秒时间戳;**没有 `key` 的钩子行**(页面钩子只记 `method`/`url`,没给 key)会写成
56
+ `method url(钩子 · … · 无 key)`,那种行只有还开着时按 `index` 才取得到。要哪一条的原文再调
57
+ **`workbench_diy_record_detail`**(给那一行的 `key`,或序号 `index`;**同一个 key 跨轮会重名**,
50
58
  再带上那一行的 `at` 才钉得住是哪一次)——它回**入参(`request.body`)与
51
59
  出参(`response body`)原文**,外加请求头(可能含凭据,别落进 `design.json`)。他还没按结束时 read 会说清"还在录"
52
60
  (要等就带 `waitMs`,帽 15 分钟;等不到会挂一条 notice 叫你);浮条被导航干掉过会顺手重新装上。
@@ -115,7 +123,7 @@
115
123
  | 字段 | 渲染出来的行 |
116
124
  |---|---|
117
125
  | `command` | 哪条写(命令 / URL + 方法) |
118
- | `params` | 参数(必填 / 取值枚举 / 上限) |
126
+ | `params` | 参数(必填 / 取值枚举 / 上限)—— 写"必填"就得带来源括号,见下 |
119
127
  | `credentials` | 凭据从哪来 |
120
128
  | `preview` | 有没有预览档(没有就写"没有,一次就是真的") |
121
129
  | `readback` | 写完之后怎么回读确认(哪条读 / 出参哪个字段) |
@@ -129,3 +137,12 @@
129
137
 
130
138
  `undo` 是 2026-10-19 加的(照 opencli 的 `write-without-delete-pair`):**旧模块**跑 `design check`
131
139
  会多出一格 `write.undo` 的空格 —— 补一句"没有配对,一次就是真的"(或真配对那条命令)就过,别的不用动。
140
+
141
+ `params` **带证据**是 2026-10-21 加的(照 `undo` 那一格同一套做法):录完一次之后,
142
+ `workbench_diy_record_read` 会给到这一场落下的**图**(他按「截图」那几张)与 `workbench_diy_record_detail` 里的**捕获原文** ——
143
+ 照它们写:哪一格必填,就看**图上有没有那个红 `*`**,或者看**契约**(`opencli <命令> --help -f json` 里的 `required`)。
144
+ 那一格**没有 DOM 快照这条路**(2026-10-21 撤了:页面的弹层/抽屉在真机上是哈希类名,通用规则圈不住他正在填的那一层)。
145
+ 判据只咬**来源有没有写出来**:`params` 里出现"必填"时,同一格必须带来源关键词之一
146
+ (`已证` / `声明` / `人证` / `契约` / `未定`),否则 `design check` 点名 `write.params.source`。
147
+ **旧模块**补一个括号就过 —— `(前端已证)` / `(服务端已证)` / `(人证)` / `(契约)` / `(未定)`;别的不用动。
148
+ 写成 `非必填` / `不必填` 的不需要来源(那一格是"可选")。这一条**不校验来源是不是真的** —— 它逼你想过,不逼你造假。