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 +44 -147
- package/lib/client.js +208 -62
- package/lib/host/execution.js +5 -6
- package/lib/host/execution.js.map +1 -1
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/board/AlertModal.tsx +36 -0
- package/src/client/board/TaskBoard.tsx +31 -31
- package/src/client/board/TaskCard.tsx +12 -2
- package/src/client/board/TaskDetail.tsx +12 -7
- package/src/client/board/TaskFormModal.tsx +46 -0
- package/src/client/controller.ts +8 -4
- package/src/client/styles.ts +27 -4
- package/src/host/execution.ts +13 -3
- package/src/index.ts +10 -0
package/README.md
CHANGED
|
@@ -1,169 +1,66 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/dsh-taskboard)
|
|
2
|
+
[](https://github.com/cloader/dsh-taskboard/blob/main/LICENSE)
|
|
3
|
+
|
|
1
4
|
# dsh-taskboard
|
|
2
5
|
|
|
3
|
-
DeepSeek Harness
|
|
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
|
-
|
|
14
|
+
## 核心功能
|
|
99
15
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
16
|
+
**看板协作**
|
|
17
|
+
- 五列看板(待规划 / 待办 / 进行中 / 待验收 / 已完成)+ 受阻标记,SSE 实时刷新
|
|
18
|
+
- 任务挂项目:认领校验会话归属,跨项目不可抢
|
|
19
|
+
- 紧急度三色(紧急/一般/不急)筛选与色条
|
|
20
|
+
- 新建/编辑弹窗:项目、模型、紧急度、执行方式、cron 实时校验与下次运行预览
|
|
21
|
+
- 详情面板:状态流转(done 仅限人工)、agent/用户评论流、执行记录回链会话
|
|
103
22
|
|
|
104
|
-
|
|
23
|
+
**Agent 工具(taskboard_\*)**
|
|
24
|
+
- 8 个工具:查板 / 建卡 / 改卡 / 移卡 / 评论 / 软删除,任何会话可用
|
|
25
|
+
- 代码级协议闸:agent 永远移不到 done;任务被持有时不可抢占;model/execution 对 agent 只读
|
|
105
26
|
|
|
106
|
-
|
|
27
|
+
**执行**
|
|
28
|
+
- 手动执行或 cron 定时:每次执行在任务项目内新建全新会话(干净上下文、可指定模型)
|
|
29
|
+
- host 侧调度:关掉浏览器照常触发;错过窗口跳过不补跑
|
|
30
|
+
- 乐观并发(ifVersion)+ 完整归因(谁改的、哪个会话执行的)
|
|
107
31
|
|
|
108
|
-
|
|
109
|
-
dsh plugin --profile <name> add dsh-taskboard
|
|
110
|
-
```
|
|
32
|
+
## 安装
|
|
111
33
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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-
|
|
612
|
-
font: inherit; font-size:
|
|
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);
|
|
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-
|
|
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
|
|
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: [
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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.
|
|
1262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1218
1263
|
className: "dsh-atb-detail-actions",
|
|
1219
|
-
children:
|
|
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: ["
|
|
1256
|
-
}, to) : /* @__PURE__ */ (0, react_jsx_runtime.
|
|
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: [
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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:
|
|
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
|
-
}
|
|
1877
|
-
onDragLeave:
|
|
2016
|
+
},
|
|
2017
|
+
onDragLeave: (e) => {
|
|
1878
2018
|
delete e.currentTarget.dataset.dragover;
|
|
1879
|
-
}
|
|
1880
|
-
onDrop:
|
|
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
|
-
}
|
|
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:
|
|
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
|
}
|