dsh-taskboard 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,169 +1,66 @@
1
+ [![npm version](https://img.shields.io/npm/v/dsh-taskboard.svg)](https://www.npmjs.com/package/dsh-taskboard)
2
+ [![License](https://img.shields.io/npm/l/dsh-taskboard.svg)](https://github.com/cloader/dsh-taskboard/blob/main/LICENSE)
3
+
1
4
  # dsh-taskboard
2
5
 
3
- DeepSeek Harness(DSH)的 **Agent 任务看板插件**:人把工作拆成卡片挂上看板,agent 会话通过 `taskboard_*` 工具认领、推进、评论、移交,人最终验收。双向协作,不是单向 todo 列表。
6
+ DeepSeek Harness 的**任务看板插件**:人建卡、agent 认领执行、人验收。任务挂项目(workspace)、可指定模型、支持手动与定时执行,全流程双向协作。
4
7
 
5
- ```
6
- 人在 GUI 建卡(选项目/紧急度/模型/执行方式)
7
-
8
-
9
- 看板 ◄──SSE 实时刷新──┐
10
- │ │ agent 会话内:
11
- [手动执行/定时触发] │ taskboard_get / comment_add / move
12
- │ ▼
13
- ▼ 卡片 → in_progress →(实现+自验)→ in_review
14
- 在任务项目里新建会话 agent 到此为止,永远到不了 done
15
- (指定模型,干净上下文)
16
-
17
-
18
- 人在看板上确认 → done(唯一入口)
19
- ```
8
+ ## 界面
20
9
 
21
- ## 功能特性
22
-
23
- **Host 侧(agent 工具 + 台账 + 调度)**
24
-
25
- - **8 个 `taskboard_*` 工具**:任何会话中的 agent 都能查板、建卡、认领、移卡、评论、软删除
26
- - **任务挂项目**:项目直接映射 DSH workspace,零新实体;认领(todo→in_progress)校验调用会话归属,跨项目认领被拒
27
- - **紧急度三色**:urgent 红 / normal 紫 / relaxed 蓝,贯穿卡片、筛选、详情
28
- - **每任务可固定模型**:执行时创建会话即生效;未指定走部署默认模型
29
- - **两种执行方式**:认领制(项目内会话认领)或定时执行(host 侧 cron,关掉浏览器照样跑,错过窗口跳过不补跑)
30
- - **一键真实执行**:手动「执行」在任务项目内新建全新会话(干净上下文)提交任务 prompt,回合结算记入执行记录
31
- - **乐观并发**:所有写操作带 `ifVersion`,冲突返回 `version_conflict`
32
- - **完整归因**:每次变更记录 actor(user / agent+sessionId),评论区分作者
33
- - **host 权威台账**:`~/.dsh/dsh-taskboard.json` 原子持久化,全局 revision 单调递增,SSE 推送 + 断线全量对账
34
-
35
- **协议硬闸(代码级强制,不靠 prompt 自觉)**
36
-
37
- - agent 移卡到 `done` 一律 `forbidden` —— 完成只能由人在 GUI 确认
38
- - 认领要求会话 cwd 解析到任务所属 workspace(跨项目 = `workspace_mismatch`)
39
- - 任务被某 agent 会话持有期间,其他会话不可抢占(任何移动 `forbidden`)
40
- - agent 不可篡改任务的 model / execution 配置(归创建者/用户所有)
41
-
42
- **Client 侧(看板 UI)**
43
-
44
- - **五列看板**:待规划 / 待办 / 进行中 / 待验收 / 已完成;blocked 角标不占列;canceled / archived / 已删除收进「其它任务」页
45
- - **筛选栏**:项目下拉 + 紧急度三色 chip 多选
46
- - **任务卡片**:紧急度左缘色条、模型 / ⏰ 定时(下次运行时间)/ 受阻 / 最近执行结果角标、评论计数;backlog ↔ todo 拖拽
47
- - **新建 / 编辑任务弹窗**(`TaskFormModal`,新建与编辑共用):
48
- - 头部图标 + 副标题、双列网格表单、底部实时校验提示 + 操作按钮
49
- - 紧急度色点卡片三选(各带副提示)、执行方式分段选择(认领制 / 定时)
50
- - Cron 预设 chip 高亮选中,**实时校验**(非法表达式红框)+ **下次运行时间预览**(与 host 调度器同一套纯函数)
51
- - 模型下拉读运行时已配置目录,「默认模型」为第一项
52
- - Esc 关闭、自动聚焦标题、保存提示版本变化
53
- - **任务编辑**:详情面板「✎ 编辑」回填全部字段;GUI 作为 owner surface 可改标题 / 描述 / Prompt / 紧急度 / **项目改绑** / 执行方式 / 模型(含清除回默认)
54
- - **详情面板**:状态流转(done 二次确认)、受阻标记、用户/agent 气泡评论流、执行记录时间线(结果/耗时/会话短 id/错误摘要)、危险区(软删除→物理清除需确认)
55
- - **主题适配**:全部颜色走外壳 `--dsw-alias-*` 设计令牌,浅色 / 深色主题与皮肤自动跟随
56
-
57
- ## 核心概念
58
-
59
- | 概念 | 说明 |
60
- |---|---|
61
- | 项目 | 直接映射 DSH workspace(无新实体)。任务创建时必须选一个 |
62
- | 紧急度 | `urgent` 红 / `normal` 紫 / `relaxed` 蓝,看板列内以色条区分,支持筛选 |
63
- | 状态机 | `backlog → todo → in_progress → in_review → done`,旁路 `canceled` / `archived`;`blocked` 是布尔标记不是状态 |
64
- | 执行方式 | `claim`(项目内会话认领)或 `scheduled`(host 侧 cron 定时触发) |
65
- | 模型 | 每任务可固定 `{provider, model}`;执行时应用到新建会话,未指定用部署默认模型 |
66
- | 乐观并发 | 所有写操作带 `ifVersion`,冲突返回 `version_conflict`(重读再试一次即可) |
67
- | 归因 | 每次变更记录 actor(user / agent+sessionId),评论挂 threadId |
68
-
69
- ## Agent 工具(8 个)
70
-
71
- | 工具 | 作用 |
72
- |---|---|
73
- | `taskboard_list` | 按项目/状态/紧急度过滤列卡(开始工作前先看) |
74
- | `taskboard_get` | 读单卡全文(描述/prompt/评论/版本) |
75
- | `taskboard_create` | 建卡(需要 agent 身份) |
76
- | `taskboard_update` | 改标题/描述/prompt/紧急度/blocked(带 ifVersion) |
77
- | `taskboard_move` | 移动状态(带 ifVersion;**代码级拒绝 agent 移到 done**) |
78
- | `taskboard_comment_add` | 追加进度/报告评论 |
79
- | `taskboard_comments` | 列评论(先读再决定开工) |
80
- | `taskboard_delete` | 软删除(agent 只能标记,人在 GUI 清除) |
81
-
82
- Agent 工作流协议(随插件注入 system prompt):开工先查板 → 先读后动 → 先认领再干活 → 冲突只重试一次 → 自验后移 in_review 留评论 → `done` 永远由人确认 → backlog 未授权不执行 → model/execution 只读。
83
-
84
- ## 执行链路
85
-
86
- 手动「执行」按钮或 cron 到期都走同一条路:
87
-
88
- 1. 卡片置 `in_progress`,台账追加 execution 记录(`running`);
89
- 2. 在任务项目目录里新建全新会话(干净上下文),带任务固定模型或部署默认模型;
90
- 3. 会话 attach 到 workspace(GUI 项目会话列表可见、可回链);
91
- 4. 任务 prompt 作为普通用户消息提交,消息头带任务 ID 与状态说明;
92
- 5. 回合结算:成功 → `succeeded`;回合错误 → `failed`(错误入台账)。
93
-
94
- 调度器每分钟 tick;到期任务先顺延 `nextRunAt` 再触发;错过超过 5 分钟的窗口跳过不补跑。
10
+ <p align="center"><img src="https://raw.githubusercontent.com/cloader/dsh-taskboard/main/img/board.png" alt="任务看板" width="880"></p>
95
11
 
96
- ## 安装
12
+ <p align="center"><img src="https://raw.githubusercontent.com/cloader/dsh-taskboard/main/img/modal.png" alt="新建任务" width="440"></p>
97
13
 
98
- **GitHub 源安装**(收录 [awesome-dsh-plugin 精选列表](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)后,dshmarket 市场里的一键安装走的也是这条路):
14
+ ## 核心功能
99
15
 
100
- ```bash
101
- dsh plugin --profile <name> add github:cloader/dsh-taskboard
102
- ```
16
+ **看板协作**
17
+ - 五列看板(待规划 / 待办 / 进行中 / 待验收 / 已完成)+ 受阻标记,SSE 实时刷新
18
+ - 任务挂项目:认领校验会话归属,跨项目不可抢
19
+ - 紧急度三色(紧急/一般/不急)筛选与色条
20
+ - 新建/编辑弹窗:项目、模型、紧急度、执行方式、cron 实时校验与下次运行预览
21
+ - 详情面板:状态流转(done 仅限人工)、agent/用户评论流、执行记录回链会话
103
22
 
104
- > 构建产物 `lib/` 已随仓库提交:安装即用,无本地构建、无构建脚本授权(`allowBuilds`)。
23
+ **Agent 工具(taskboard_\*)**
24
+ - 8 个工具:查板 / 建卡 / 改卡 / 移卡 / 评论 / 软删除,任何会话可用
25
+ - 代码级协议闸:agent 永远移不到 done;任务被持有时不可抢占;model/execution 对 agent 只读
105
26
 
106
- **npm 裸名安装**(需先将包发布到 npm;同样是预构建,免构建授权):
27
+ **执行**
28
+ - 手动执行或 cron 定时:每次执行在任务项目内新建全新会话(干净上下文、可指定模型)
29
+ - host 侧调度:关掉浏览器照常触发;错过窗口跳过不补跑
30
+ - 乐观并发(ifVersion)+ 完整归因(谁改的、哪个会话执行的)
107
31
 
108
- ```bash
109
- dsh plugin --profile <name> add dsh-taskboard
110
- ```
32
+ ## 安装
111
33
 
112
- 本地 link 开发:profile 的 `package.json`:
113
-
114
- ```json
115
- {
116
- "dependencies": {
117
- "dsh-taskboard": "link:D:/path/to/dsh-taskboard"
118
- },
119
- "dsh": {
120
- "profile": {
121
- "bundles": [
122
- "@deepseek-ai/dsh-base",
123
- "@deepseek-ai/dsh-web-app",
124
- "dsh-taskboard"
125
- ]
126
- }
127
- }
128
- }
34
+ ```bash
35
+ dsh plugin --profile <name> add dsh-taskboard # npm(推荐,预构建免授权)
36
+ dsh plugin --profile <name> add github:cloader/dsh-taskboard # GitHub 源
129
37
  ```
130
38
 
131
- 然后 `dsh plugin --profile <name> add link:D:/path/to/dsh-taskboard`。
132
-
133
- > ⚠️ 官方 `@deepseek-ai/dsh-*` 包只写进 `bundles` 列表,**不要** `plugin add` 进 profile dependencies(会引发 SDK 双实例遮蔽,详见项目根 README 的踩坑记录)。
134
-
135
- host 侧改动需重启 `dsh web`;client 侧改动 rebuild 后刷新页面。
39
+ > 官方 `@deepseek-ai/dsh-*` 包只写进 profile `bundles` 列表,不要 `plugin add` 进 dependencies(避免 SDK 双实例遮蔽)。
136
40
 
137
41
  ## 开发
138
42
 
139
43
  ```bash
140
- npm install
141
- npm run build # tsdown 双构建:host ESM + client CJS(wrap-client.mjs 包装)
142
- npm test # vitest 41 项(protocol/tools/execution/routes/client
143
- npm run typecheck
44
+ npm install && npm run build # host ESM + client CJS 双构建
45
+ npm test # vitest 41
46
+ node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge
144
47
  ```
145
48
 
146
- ## 架构
49
+ ## 升级日志
147
50
 
148
- ```
149
- src/
150
- shared/protocol.ts # 状态机/紧急度/cron/记录类型(host+client 共享)
151
- shared/api.ts # HTTP/SSE 线上契约
152
- host/
153
- store.ts # 台账:串行 mutate、原子持久化、订阅
154
- tools.ts # 8 个 taskboard_* 工具 + 代码级协议闸
155
- sdk.ts # SDK 兼容层(零运行时官方包 import)
156
- protocol-text.ts # agent 工作流协议 systemPrompt section
157
- execution.ts # 执行服务(新会话/模型/结算)
158
- scheduler.ts # host 侧 cron tick
159
- routes.ts # JSON + SSE HTTP 面
160
- client/
161
- index.ts # 注入入口(connection)
162
- api.ts controller.ts # fetch/SSE + useSyncExternalStore 状态源
163
- board/*.tsx # 看板视图(五列/筛选/卡片/详情/新建·编辑弹窗 TaskFormModal)
164
- sidebar-entry.ts board-mount.tsx styles.ts
165
- tests/ # vitest:protocol/tools/execution/routes/client + smoke-host
166
- e2e/ # dev-server 手工验证脚本(P0–P4,见 tests/e2e/README.md)
167
- ```
51
+ ### 0.1.2
52
+
53
+ - **看板拖拽**:所有列的卡片可互相拖动(按状态机校验合法流转,非法拖放弹窗提示);正在执行的任务拖动会被拦截,提示「该任务正在由【任务名】会话执行,不能拖动」
54
+ - **执行会话体验**:执行会话标题固定为任务名(经 `sessionTitle.rename` 写入,不会被自动重命名覆盖);首条消息以正常用户消息呈现(不再是插件上下文行)
55
+ - **交互优化**:
56
+ - 新建/编辑弹框新增「⚡ 立即执行」按钮(保存后直接发起执行)
57
+ - 详情页「立即执行」移至「编辑」按钮旁
58
+ - 「+ 新建任务」移至看板标题统计旁
59
+ - 状态流转按钮文案统一为「移至→{状态}」
60
+ - **弹窗**:原生 `alert()` 全部替换为主题化模态弹窗(Esc/遮罩可关)
61
+
62
+ ### 0.1.1
63
+
64
+ - 初始 npm 发布:看板协作、8 个 agent 工具、手动/cron 执行、SSE 实时视图
168
65
 
169
66
  License: Apache-2.0
package/lib/client.js CHANGED
@@ -349,17 +349,19 @@ window.__ModuleLoader__.load({
349
349
  toggleSecondary() {
350
350
  this.setState({ secondaryOpen: !this.state.secondaryOpen });
351
351
  }
352
- /** Create a task (composer submit). */
352
+ /** Create a task (composer submit); returns the new task id, undefined on failure. */
353
353
  async create(body) {
354
354
  try {
355
- await this.client.create(body);
355
+ const summary = await this.client.create(body);
356
356
  this.setState({
357
357
  composerOpen: false,
358
358
  error: void 0
359
359
  });
360
360
  await this.refresh();
361
+ return summary.id;
361
362
  } catch (error) {
362
363
  this.setState({ error: error instanceof Error ? error.message : String(error) });
364
+ return;
363
365
  }
364
366
  }
365
367
  /** Edit task fields (form modal submit; the GUI is the owner surface). */
@@ -375,8 +377,10 @@ window.__ModuleLoader__.load({
375
377
  error: void 0
376
378
  });
377
379
  await this.refresh();
380
+ return true;
378
381
  } catch (error) {
379
382
  this.setState({ error: error instanceof Error ? error.message : String(error) });
383
+ return false;
380
384
  }
381
385
  }
382
386
  /** Move a task (user surface: done allowed). */
@@ -608,12 +612,12 @@ window.__ModuleLoader__.load({
608
612
  .dsh-atb-desc { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.55; }
609
613
 
610
614
  .dsh-atb-detail-actions { display: flex; flex-direction: column; gap: 8px; }
611
- .dsh-atb-runbtn {
612
- font: inherit; font-size: 13px; font-weight: 600; padding: 8px 14px; border-radius: 9px; cursor: pointer;
613
- border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); color: var(--dsw-alias-label-primary-foreground, #fff); text-align: center;
615
+ .dsh-atb-detail-run {
616
+ font: inherit; font-size: 12px; font-weight: 600; padding: 4px 11px; border-radius: 7px; cursor: pointer;
617
+ border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); color: var(--dsw-alias-label-primary-foreground, #fff);
614
618
  transition: filter .12s ease;
615
619
  }
616
- .dsh-atb-runbtn:hover { filter: brightness(1.1); }
620
+ .dsh-atb-detail-run:hover { filter: brightness(1.1); }
617
621
  .dsh-atb-movebtns { display: flex; gap: 6px; flex-wrap: wrap; }
618
622
  .dsh-atb-movebtn {
619
623
  font: inherit; font-size: 12px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
@@ -812,6 +816,29 @@ window.__ModuleLoader__.load({
812
816
  .dsh-atb-secondary { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
813
817
  .dsh-atb-link { color: var(--dsw-alias-state-business-primary, #3e63dd); cursor: pointer; text-decoration: none; }
814
818
  .dsh-atb-link:hover { text-decoration: underline; }
819
+
820
+ /* ---------- alert modal ---------- */
821
+ .dsh-atb-alert-backdrop {
822
+ position: fixed; inset: 0; z-index: 90;
823
+ background: var(--dsw-alias-bg-mask-drop, rgba(28,30,36,.4)); backdrop-filter: var(--dsw-mask-blur, blur(2px));
824
+ display: flex; align-items: center; justify-content: center;
825
+ animation: dsh-atb-fade .12s ease;
826
+ }
827
+ .dsh-atb-alert {
828
+ min-width: 280px; max-width: 380px; padding: 20px 24px; border-radius: 14px;
829
+ background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
830
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
831
+ box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
832
+ display: flex; flex-direction: column; align-items: center; gap: 14px;
833
+ animation: dsh-atb-pop .14s ease;
834
+ }
835
+ .dsh-atb-alert-icon { font-size: 28px; line-height: 1; }
836
+ .dsh-atb-alert-msg {
837
+ font-size: 13.5px; line-height: 1.55; text-align: center;
838
+ word-break: break-word; white-space: pre-wrap;
839
+ color: var(--dsw-alias-label-primary, inherit);
840
+ }
841
+ .dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
815
842
  `;
816
843
  let injected = false;
817
844
  /** Inject the stylesheet once (idempotent). */
@@ -968,9 +995,10 @@ window.__ModuleLoader__.load({
968
995
  * The card view.
969
996
  * @param task - the task record.
970
997
  * @param controller - the controller.
971
- * @param draggable - enable dragging (backlog/todo columns only).
998
+ * @param draggable - enable dragging.
999
+ * @param onAlert - show an alert message (replaces native alert).
972
1000
  */
973
- function TaskCard({ task, controller, draggable = false }) {
1001
+ function TaskCard({ task, controller, draggable = false, onAlert }) {
974
1002
  const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : void 0;
975
1003
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
976
1004
  type: "button",
@@ -978,6 +1006,13 @@ window.__ModuleLoader__.load({
978
1006
  "data-urgency": task.urgency,
979
1007
  draggable,
980
1008
  onDragStart: (e) => {
1009
+ if (task.executions.find((ex) => ex.outcome === "running") !== void 0) {
1010
+ e.preventDefault();
1011
+ const msg = `该任务正在由【${task.title}】会话执行,不能拖动`;
1012
+ if (onAlert !== void 0) onAlert(msg);
1013
+ else alert(msg);
1014
+ return;
1015
+ }
981
1016
  e.dataTransfer.setData(DRAG_TYPE, task.id);
982
1017
  e.dataTransfer.effectAllowed = "move";
983
1018
  e.currentTarget.dataset.dragging = "true";
@@ -1179,18 +1214,28 @@ window.__ModuleLoader__.load({
1179
1214
  ]
1180
1215
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
1216
  className: "dsh-atb-detail-topbtns",
1182
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1183
- type: "button",
1184
- className: "dsh-atb-detail-edit",
1185
- onClick: () => controller.openEditor(task.id),
1186
- children: "✎ 编辑"
1187
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1188
- type: "button",
1189
- className: "dsh-atb-detail-close",
1190
- "aria-label": "关闭",
1191
- onClick: () => controller.select(void 0),
1192
- children: ""
1193
- })]
1217
+ children: [
1218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1219
+ type: "button",
1220
+ className: "dsh-atb-detail-edit",
1221
+ onClick: () => controller.openEditor(task.id),
1222
+ children: "✎ 编辑"
1223
+ }),
1224
+ canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1225
+ type: "button",
1226
+ className: "dsh-atb-detail-run",
1227
+ title: task.model !== void 0 ? `新会话执行(${task.model.model})` : "新会话执行(默认模型)",
1228
+ onClick: () => void controller.run(task.id),
1229
+ children: "▶ 立即执行"
1230
+ }),
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1232
+ type: "button",
1233
+ className: "dsh-atb-detail-close",
1234
+ "aria-label": "关闭",
1235
+ onClick: () => controller.select(void 0),
1236
+ children: "✕"
1237
+ })
1238
+ ]
1194
1239
  })]
1195
1240
  }),
1196
1241
  task.description.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1214,14 +1259,9 @@ window.__ModuleLoader__.load({
1214
1259
  children: task.prompt
1215
1260
  })]
1216
1261
  }),
1217
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1218
1263
  className: "dsh-atb-detail-actions",
1219
- children: [canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1220
- type: "button",
1221
- className: "dsh-atb-runbtn",
1222
- onClick: () => void controller.run(task.id),
1223
- children: ["▶ 执行 · 新会话", task.model !== void 0 ? `(${task.model.model})` : "(默认模型)"]
1224
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1264
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1225
1265
  className: "dsh-atb-movebtns",
1226
1266
  children: [moveTargets(task).map((to) => to === "done" ? confirmDone ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1227
1267
  className: "dsh-atb-confirm",
@@ -1252,13 +1292,13 @@ window.__ModuleLoader__.load({
1252
1292
  className: "dsh-atb-movebtn",
1253
1293
  "data-to": to,
1254
1294
  onClick: () => setConfirmDone(true),
1255
- children: ["", MOVE_LABEL[to]]
1256
- }, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1295
+ children: ["移至→", MOVE_LABEL[to]]
1296
+ }, to) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1257
1297
  type: "button",
1258
1298
  className: "dsh-atb-movebtn",
1259
1299
  "data-to": to,
1260
1300
  onClick: () => void controller.move(task.id, task.version, to),
1261
- children: MOVE_LABEL[to]
1301
+ children: ["移至→", MOVE_LABEL[to]]
1262
1302
  }, to)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1263
1303
  type: "button",
1264
1304
  className: "dsh-atb-movebtn",
@@ -1266,7 +1306,7 @@ window.__ModuleLoader__.load({
1266
1306
  onClick: () => void controller.toggleBlocked(task),
1267
1307
  children: task.blocked ? "✓ 解除受阻" : "⛔ 标记受阻"
1268
1308
  })]
1269
- })]
1309
+ })
1270
1310
  }),
1271
1311
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1272
1312
  className: "dsh-atb-section",
@@ -1509,6 +1549,7 @@ window.__ModuleLoader__.load({
1509
1549
  const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
1510
1550
  const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
1511
1551
  const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
1552
+ const runBlocked = editing && task.status === "in_progress";
1512
1553
  const submit = () => {
1513
1554
  if (!valid) return;
1514
1555
  const picked = model !== "" ? JSON.parse(model) : void 0;
@@ -1537,6 +1578,40 @@ window.__ModuleLoader__.load({
1537
1578
  model: picked
1538
1579
  });
1539
1580
  };
1581
+ /** Save the form, then immediately trigger a manual run of the task. */
1582
+ const submitAndRun = () => {
1583
+ if (!valid || runBlocked) return;
1584
+ const picked = model !== "" ? JSON.parse(model) : void 0;
1585
+ if (editing) (async () => {
1586
+ if (await controller.update(task.id, task.version, {
1587
+ title,
1588
+ description,
1589
+ prompt,
1590
+ urgency,
1591
+ workspaceId,
1592
+ execution: mode === "scheduled" ? {
1593
+ mode,
1594
+ cron: cron.trim()
1595
+ } : { mode },
1596
+ model: picked ?? null
1597
+ })) await controller.run(task.id);
1598
+ })();
1599
+ else (async () => {
1600
+ const id = await controller.create({
1601
+ title,
1602
+ workspaceId,
1603
+ urgency,
1604
+ description: description.length > 0 ? description : void 0,
1605
+ prompt: prompt.length > 0 ? prompt : void 0,
1606
+ execution: mode === "scheduled" ? {
1607
+ mode,
1608
+ cron: cron.trim()
1609
+ } : { mode },
1610
+ model: picked
1611
+ });
1612
+ if (id !== void 0) await controller.run(id);
1613
+ })();
1614
+ };
1540
1615
  const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
1541
1616
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1542
1617
  className: "dsh-atb-modal-backdrop",
@@ -1727,19 +1802,30 @@ window.__ModuleLoader__.load({
1727
1802
  children: hint
1728
1803
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1729
1804
  className: "dsh-atb-modal-footbtns",
1730
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1731
- type: "button",
1732
- className: "dsh-atb-btn",
1733
- onClick: () => controller.closeForm(),
1734
- children: "取消"
1735
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1736
- type: "button",
1737
- className: "dsh-atb-btn",
1738
- "data-primary": "true",
1739
- disabled: !valid,
1740
- onClick: submit,
1741
- children: editing ? "保存修改" : "创建任务"
1742
- })]
1805
+ children: [
1806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1807
+ type: "button",
1808
+ className: "dsh-atb-btn",
1809
+ onClick: () => controller.closeForm(),
1810
+ children: "取消"
1811
+ }),
1812
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1813
+ type: "button",
1814
+ className: "dsh-atb-btn",
1815
+ disabled: !valid || runBlocked,
1816
+ title: runBlocked ? "任务正在执行中,不能重复发起" : "保存后立即发起执行(新会话)",
1817
+ onClick: submitAndRun,
1818
+ children: "⚡ 立即执行"
1819
+ }),
1820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1821
+ type: "button",
1822
+ className: "dsh-atb-btn",
1823
+ "data-primary": "true",
1824
+ disabled: !valid,
1825
+ onClick: submit,
1826
+ children: editing ? "保存修改" : "创建任务"
1827
+ })
1828
+ ]
1743
1829
  })]
1744
1830
  })
1745
1831
  ]
@@ -1747,6 +1833,62 @@ window.__ModuleLoader__.load({
1747
1833
  });
1748
1834
  }
1749
1835
 
1836
+ //#endregion
1837
+ //#region src/client/board/AlertModal.tsx
1838
+ /**
1839
+ * A lightweight alert modal — replaces native alert() with a themed overlay
1840
+ * that matches the shell design tokens.
1841
+ *
1842
+ * @module dsh-taskboard/client/board/AlertModal
1843
+ */
1844
+ /** Show a non-blocking alert modal. Returns true when opened. */
1845
+ function useAlert() {
1846
+ const [msg, setMsg] = (0, react.useState)(null);
1847
+ const show = (m) => setMsg(m);
1848
+ const close = () => setMsg(null);
1849
+ return {
1850
+ alert: show,
1851
+ el: msg !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertModal, {
1852
+ message: msg,
1853
+ onClose: close
1854
+ }) : null
1855
+ };
1856
+ }
1857
+ function AlertModal({ message, onClose }) {
1858
+ (0, react.useEffect)(() => {
1859
+ const handler = (e) => {
1860
+ if (e.key === "Escape") onClose();
1861
+ };
1862
+ window.addEventListener("keydown", handler);
1863
+ return () => window.removeEventListener("keydown", handler);
1864
+ }, [onClose]);
1865
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1866
+ className: "dsh-atb-alert-backdrop",
1867
+ onClick: onClose,
1868
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1869
+ className: "dsh-atb-alert",
1870
+ onClick: (e) => e.stopPropagation(),
1871
+ children: [
1872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1873
+ className: "dsh-atb-alert-icon",
1874
+ children: "⛔"
1875
+ }),
1876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1877
+ className: "dsh-atb-alert-msg",
1878
+ children: message
1879
+ }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1881
+ type: "button",
1882
+ className: "dsh-atb-btn",
1883
+ "data-primary": "true",
1884
+ onClick: onClose,
1885
+ children: "知道了"
1886
+ })
1887
+ ]
1888
+ })
1889
+ });
1890
+ }
1891
+
1750
1892
  //#endregion
1751
1893
  //#region src/client/board/TaskBoard.tsx
1752
1894
  /**
@@ -1765,8 +1907,6 @@ window.__ModuleLoader__.load({
1765
1907
  canceled: "已取消",
1766
1908
  archived: "已归档"
1767
1909
  };
1768
- /** The two columns between which cards may be dragged both ways. */
1769
- const DRAGGABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo"]);
1770
1910
  /** Urgency chip labels. */
1771
1911
  const URGENCY_LABELS = {
1772
1912
  urgent: "紧急",
@@ -1792,6 +1932,7 @@ window.__ModuleLoader__.load({
1792
1932
  const state = (0, react.useSyncExternalStore)((cb) => controller.subscribe(cb), () => controller.getSnapshot());
1793
1933
  const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
1794
1934
  const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
1935
+ const { alert: showAlert, el: alertEl } = useAlert();
1795
1936
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1796
1937
  className: "dsh-atb-board",
1797
1938
  children: [
@@ -1810,6 +1951,13 @@ window.__ModuleLoader__.load({
1810
1951
  state.ledger.revision
1811
1952
  ]
1812
1953
  }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1955
+ type: "button",
1956
+ className: "dsh-atb-btn",
1957
+ "data-primary": "true",
1958
+ onClick: () => controller.setComposer(true),
1959
+ children: "+ 新建任务"
1960
+ }),
1813
1961
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-spacer" }),
1814
1962
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1815
1963
  className: "dsh-atb-select",
@@ -1843,13 +1991,6 @@ window.__ModuleLoader__.load({
1843
1991
  className: "dsh-atb-btn",
1844
1992
  onClick: () => controller.toggleSecondary(),
1845
1993
  children: state.secondaryOpen ? "返回看板" : "其它任务"
1846
- }),
1847
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1848
- type: "button",
1849
- className: "dsh-atb-btn",
1850
- "data-primary": "true",
1851
- onClick: () => controller.setComposer(true),
1852
- children: "+ 新建任务"
1853
1994
  })
1854
1995
  ]
1855
1996
  }),
@@ -1864,28 +2005,31 @@ window.__ModuleLoader__.load({
1864
2005
  className: "dsh-atb-columns",
1865
2006
  children: MAIN_STATUSES.map((status) => {
1866
2007
  const columnTasks = live.filter((t) => t.status === status);
1867
- const dropTarget = DRAGGABLE_STATUSES.has(status);
1868
2008
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1869
2009
  className: "dsh-atb-column",
1870
- onDragOver: dropTarget ? (e) => {
2010
+ onDragOver: (e) => {
1871
2011
  if (e.dataTransfer.types.includes("application/x-dsh-atb-task")) {
1872
2012
  e.preventDefault();
1873
2013
  e.dataTransfer.dropEffect = "move";
1874
2014
  e.currentTarget.dataset.dragover = "true";
1875
2015
  }
1876
- } : void 0,
1877
- onDragLeave: dropTarget ? (e) => {
2016
+ },
2017
+ onDragLeave: (e) => {
1878
2018
  delete e.currentTarget.dataset.dragover;
1879
- } : void 0,
1880
- onDrop: dropTarget ? (e) => {
2019
+ },
2020
+ onDrop: (e) => {
1881
2021
  e.preventDefault();
1882
2022
  delete e.currentTarget.dataset.dragover;
1883
2023
  const id = e.dataTransfer.getData(DRAG_TYPE);
1884
2024
  if (id.length === 0) return;
1885
2025
  const task = state.ledger.tasks.find((t) => t.id === id);
1886
2026
  if (task === void 0 || task.status === status) return;
2027
+ if (!canTransition(task.status, status)) {
2028
+ showAlert(`无法从「${COLUMN_LABELS[task.status]}」拖至「${COLUMN_LABELS[status]}」`);
2029
+ return;
2030
+ }
1887
2031
  controller.move(id, task.version, status);
1888
- } : void 0,
2032
+ },
1889
2033
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1890
2034
  className: "dsh-atb-colhead",
1891
2035
  children: [COLUMN_LABELS[status], /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1897,7 +2041,8 @@ window.__ModuleLoader__.load({
1897
2041
  children: [columnTasks.map((task) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
1898
2042
  task,
1899
2043
  controller,
1900
- draggable: dropTarget
2044
+ draggable: true,
2045
+ onAlert: showAlert
1901
2046
  }, task.id)), columnTasks.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1902
2047
  className: "dsh-atb-empty",
1903
2048
  children: "无任务"
@@ -1916,7 +2061,8 @@ window.__ModuleLoader__.load({
1916
2061
  state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
1917
2062
  controller,
1918
2063
  task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
1919
- })
2064
+ }),
2065
+ alertEl
1920
2066
  ]
1921
2067
  });
1922
2068
  }