dsh-taskboard 0.1.1 → 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 +17 -0
- 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
|
@@ -46,4 +46,21 @@ npm test # vitest 41 项
|
|
|
46
46
|
node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## 升级日志
|
|
50
|
+
|
|
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 实时视图
|
|
65
|
+
|
|
49
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
|
}
|
package/lib/host/execution.js
CHANGED
|
@@ -126,6 +126,9 @@ var ExecutionService = class {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => {});
|
|
129
|
+
try {
|
|
130
|
+
this.deps.renameSession?.(sessionId, task.title);
|
|
131
|
+
} catch {}
|
|
129
132
|
await this.patchExecution(executionId, { sessionId });
|
|
130
133
|
const message = {
|
|
131
134
|
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
@@ -134,10 +137,7 @@ var ExecutionService = class {
|
|
|
134
137
|
type: "text",
|
|
135
138
|
text: this.executionPrompt(task)
|
|
136
139
|
}],
|
|
137
|
-
source: {
|
|
138
|
-
kind: "plugin",
|
|
139
|
-
plugin: "dsh-taskboard"
|
|
140
|
-
}
|
|
140
|
+
source: { kind: "user" }
|
|
141
141
|
};
|
|
142
142
|
handle.agent.followup(message);
|
|
143
143
|
const settle = () => {
|
|
@@ -166,10 +166,9 @@ var ExecutionService = class {
|
|
|
166
166
|
}
|
|
167
167
|
/** The prompt text one execution submits (task context + instructions). */
|
|
168
168
|
executionPrompt(task) {
|
|
169
|
-
const head = `【任务看板执行】${task.title}(任务 ID: ${task.id})`;
|
|
170
169
|
const state = "本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。";
|
|
171
170
|
const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`;
|
|
172
|
-
return
|
|
171
|
+
return `【任务】${task.title}(任务 ID: ${task.id})\n\n${state}\n\n${effectivePrompt(task)}\n\n${tail}`;
|
|
173
172
|
}
|
|
174
173
|
/** Move a task back out of in_progress after a failed start. */
|
|
175
174
|
async revertProgress(taskId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport { effectivePrompt, newExecutionId, type ExecutionRecord, type TaskRecord } from '../shared/protocol.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string }\n agentOptions?: { provider?: string; model?: string }\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const detail = JSON.stringify(error) ?? ''\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', detail.slice(0, 2000))\n void detail\n return { message }\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Execution ids currently settling. */\n private readonly settling = new Map<string, () => void>()\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) this.noteFailure(sessionId, failure.message)\n })\n }\n\n /** Record a turn failure against the running execution of that session. */\n private noteFailure(sessionId: string, message: string): void {\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n return [task]\n }\n }\n }\n return undefined\n })\n }\n\n /** Patch one task's execution record in the ledger. */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger']): Promise<RunRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n if (task.status === 'in_progress') {\n return { ok: false, error: 'task is already in progress' }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 1. Open the execution record and move the card to in_progress in one write.\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'user' }\n return [target]\n })\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: { cwd: workspace.path },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n return { ok: false, error: message }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the effective prompt as an ordinary user message and settle\n // on quiescence (turn/end errors were already folded by the listener).\n const message = {\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.executionPrompt(task) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n }\n handle.agent.followup(message)\n\n // 6. Settlement watcher.\n const settle = (): void => {\n this.settling.delete(executionId)\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n execution.outcome = 'succeeded'\n execution.endedAt = this.deps.now()\n return [t]\n }\n }\n return undefined\n })\n }\n this.settling.set(executionId, settle)\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n settle()\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /** The prompt text one execution submits (task context + instructions). */\n private executionPrompt(task: TaskRecord): string {\n const head = `【任务看板执行】${task.title}(任务 ID: ${task.id})`\n const state = '本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。'\n const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;`\n + `2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);`\n + `3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`\n return `${head}\\n\\n${state}\\n\\n${effectivePrompt(task)}\\n\\n${tail}`\n }\n\n /** Move a task back out of in_progress after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport { effectivePrompt, newExecutionId, type ExecutionRecord, type TaskRecord } from '../shared/protocol.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string }\n agentOptions?: { provider?: string; model?: string }\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const detail = JSON.stringify(error) ?? ''\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', detail.slice(0, 2000))\n void detail\n return { message }\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Execution ids currently settling. */\n private readonly settling = new Map<string, () => void>()\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) this.noteFailure(sessionId, failure.message)\n })\n }\n\n /** Record a turn failure against the running execution of that session. */\n private noteFailure(sessionId: string, message: string): void {\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n return [task]\n }\n }\n }\n return undefined\n })\n }\n\n /** Patch one task's execution record in the ledger. */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger']): Promise<RunRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n if (task.status === 'in_progress') {\n return { ok: false, error: 'task is already in progress' }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 1. Open the execution record and move the card to in_progress in one write.\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'user' }\n return [target]\n })\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: { cwd: workspace.path },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n return { ok: false, error: message }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the effective prompt as an ordinary user message and settle\n // on quiescence (turn/end errors were already folded by the listener).\n // Source `user` (not `plugin`) so the opening message renders as a\n // normal user bubble in the conversation, exactly like a typed prompt.\n const message = {\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.executionPrompt(task) }],\n source: { kind: 'user' as const },\n }\n handle.agent.followup(message)\n\n // 6. Settlement watcher.\n const settle = (): void => {\n this.settling.delete(executionId)\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n execution.outcome = 'succeeded'\n execution.endedAt = this.deps.now()\n return [t]\n }\n }\n return undefined\n })\n }\n this.settling.set(executionId, settle)\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n settle()\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /** The prompt text one execution submits (task context + instructions). */\n private executionPrompt(task: TaskRecord): string {\n const state = '本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。'\n const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;`\n + `2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);`\n + `3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`\n return `【任务】${task.title}(任务 ID: ${task.id})\\n\\n${state}\\n\\n${effectivePrompt(task)}\\n\\n${tail}`\n }\n\n /** Move a task back out of in_progress after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -48,6 +48,14 @@ function apply(ctx) {
|
|
|
48
48
|
},
|
|
49
49
|
events,
|
|
50
50
|
now,
|
|
51
|
+
renameSession: (sessionId, title) => {
|
|
52
|
+
try {
|
|
53
|
+
const sessions = agentCtx.get("sessions");
|
|
54
|
+
const sessionTitle = agentCtx.get("sessionTitle");
|
|
55
|
+
const session = sessions?.get(sessionId);
|
|
56
|
+
if (session !== void 0 && sessionTitle !== void 0) sessionTitle.rename(session, title);
|
|
57
|
+
} catch {}
|
|
58
|
+
},
|
|
51
59
|
defaultModel: () => {
|
|
52
60
|
try {
|
|
53
61
|
const selection = agentCtx.get("agentDefaultModel");
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the eight\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { ExecutionService, type EventsFace } from './host/execution.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const now = () => Date.now()\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string) => execution.run(taskId, 'manual'),\n })\n return () => disposeRoutes?.()\n })\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open.\n const scheduler = new SchedulerService({ store, execution, now })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":";;;;;;;;;AA8BA,MAAa,cAAc;;AAG3B,MAAa,OAAO;;AAGpB,MAAa,SAAS,CAAC,SAAS,cAAc;;;;;AAM9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,WAAW,EAAE,CAAC;CAC9D,MAAM,YAAY,KAAK,IAAI;CAG3B,MAAM,iBAAiB,IAAI,aAAa,QAAQ;EAC9C,MAAM;EACN,OAAA;EACA,MAAM;CACR,CAAC;CACD,IAAI,aAAa,gBAAgB,iCAAiC;CAIlE,IAAI,OAAO,CAAC,mBAAmB,IAAI,UAAmB;EACpD,MAAM,YAA+B,CAAC;EACtC,UAAU,KAAK,GAAG,uBAAuB,OAAO;GAC9C;GACA,YAAY,cAAc,MAAM,iBAAiB;GACjD;EACF,CAAC,CAAC;EAGF,MAAM,SAAqB,EACzB,iBAAiB,aAAa,MAAM,GAAG,kBAAkB,SAAS,UAAU;GAC1E,SAAS,QAAQ,IAAI,KAAyC;EAChE,CAAC,EACH;EAEA,MAAM,OAAO,CAAC,QAAQ,IAAI,aAAsB;GAC9C,MAAM,YAAY,IAAI,iBAAiB;IACrC;IACA,QAAQ,EACN,SAAS,YAA4B,SAAS,OAAO,OAAO,OAAgB,EAC9E;IACA,YAAY;KACV,MAAK,OAAM,cAAc,MAAM,iBAAiB,CAAC,CAAC,IAAI,EAAE;KACxD,QAAQ,OAAO,aAAa,cAAc;MACxC,MAAM,KAAK,MAAM,kBAAkB,IAAI,WAAoB;MAC3D,IAAI,OAAO,KAAA,GAAW,MAAM,GAAG,cAAc,SAAkB;KACjE;IACF;IACA;IACA;IACA,oBAAoB;KAClB,IAAI;MACF,MAAM,YAAY,SAAS,IAAI,mBAAmB;MAClD,MAAM,OAAO,WAAW;MACxB,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,KAAK,SAAS;KAC7D,QAAQ;MAAE;KAAiB;IAC7B;GACF,CAAC;GAGD,IAAI;GACJ,SAAS,OAAO,CAAC,WAAW,IAAI,WAAoB;IAClD,gBAAgB,wBAAwB,QAAQ;KAC9C;KACA,YAAY,cAAc,MAAM,iBAAiB;KACjD;KACA,MAAM,WAAmB,UAAU,IAAI,QAAQ,QAAQ;IACzD,CAAC;IACD,aAAa,gBAAgB;GAC/B,CAAC;GAID,MAAM,YAAY,IAAI,iBAAiB;IAAE;IAAO;IAAW;GAAI,CAAC;GAChE,UAAU,MAAM;GAChB,UAAU,WAAW,UAAU,QAAQ,CAAC;GAExC,aAAa;IACX,gBAAgB;IAChB,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACrD;EACF,CAAC;EAED,aAAa;GACX,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;EACrD;CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the eight\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { ExecutionService, type EventsFace } from './host/execution.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const now = () => Date.now()\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n renameSession: (sessionId, title) => {\n // Best-effort: pin the execution session's title to the task title\n // through the log-backed session-title service (user-sourced rename).\n try {\n const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined\n const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined\n const session = sessions?.get(sessionId)\n if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)\n } catch { /* cosmetic */ }\n },\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string) => execution.run(taskId, 'manual'),\n })\n return () => disposeRoutes?.()\n })\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open.\n const scheduler = new SchedulerService({ store, execution, now })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":";;;;;;;;;AA8BA,MAAa,cAAc;;AAG3B,MAAa,OAAO;;AAGpB,MAAa,SAAS,CAAC,SAAS,cAAc;;;;;AAM9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,WAAW,EAAE,CAAC;CAC9D,MAAM,YAAY,KAAK,IAAI;CAG3B,MAAM,iBAAiB,IAAI,aAAa,QAAQ;EAC9C,MAAM;EACN,OAAA;EACA,MAAM;CACR,CAAC;CACD,IAAI,aAAa,gBAAgB,iCAAiC;CAIlE,IAAI,OAAO,CAAC,mBAAmB,IAAI,UAAmB;EACpD,MAAM,YAA+B,CAAC;EACtC,UAAU,KAAK,GAAG,uBAAuB,OAAO;GAC9C;GACA,YAAY,cAAc,MAAM,iBAAiB;GACjD;EACF,CAAC,CAAC;EAGF,MAAM,SAAqB,EACzB,iBAAiB,aAAa,MAAM,GAAG,kBAAkB,SAAS,UAAU;GAC1E,SAAS,QAAQ,IAAI,KAAyC;EAChE,CAAC,EACH;EAEA,MAAM,OAAO,CAAC,QAAQ,IAAI,aAAsB;GAC9C,MAAM,YAAY,IAAI,iBAAiB;IACrC;IACA,QAAQ,EACN,SAAS,YAA4B,SAAS,OAAO,OAAO,OAAgB,EAC9E;IACA,YAAY;KACV,MAAK,OAAM,cAAc,MAAM,iBAAiB,CAAC,CAAC,IAAI,EAAE;KACxD,QAAQ,OAAO,aAAa,cAAc;MACxC,MAAM,KAAK,MAAM,kBAAkB,IAAI,WAAoB;MAC3D,IAAI,OAAO,KAAA,GAAW,MAAM,GAAG,cAAc,SAAkB;KACjE;IACF;IACA;IACA;IACA,gBAAgB,WAAW,UAAU;KAGnC,IAAI;MACF,MAAM,WAAW,SAAS,IAAI,UAAU;MACxC,MAAM,eAAe,SAAS,IAAI,cAAc;MAChD,MAAM,UAAU,UAAU,IAAI,SAAS;MACvC,IAAI,YAAY,KAAA,KAAa,iBAAiB,KAAA,GAAW,aAAa,OAAO,SAAS,KAAK;KAC7F,QAAQ,CAAiB;IAC3B;IACA,oBAAoB;KAClB,IAAI;MACF,MAAM,YAAY,SAAS,IAAI,mBAAmB;MAClD,MAAM,OAAO,WAAW;MACxB,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,KAAK,SAAS;KAC7D,QAAQ;MAAE;KAAiB;IAC7B;GACF,CAAC;GAGD,IAAI;GACJ,SAAS,OAAO,CAAC,WAAW,IAAI,WAAoB;IAClD,gBAAgB,wBAAwB,QAAQ;KAC9C;KACA,YAAY,cAAc,MAAM,iBAAiB;KACjD;KACA,MAAM,WAAmB,UAAU,IAAI,QAAQ,QAAQ;IACzD,CAAC;IACD,aAAa,gBAAgB;GAC/B,CAAC;GAID,MAAM,YAAY,IAAI,iBAAiB;IAAE;IAAO;IAAW;GAAI,CAAC;GAChE,UAAU,MAAM;GAChB,UAAU,WAAW,UAAU,QAAQ,CAAC;GAExC,aAAa;IACX,gBAAgB;IAChB,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACrD;EACF,CAAC;EAED,aAAa;GACX,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;EACrD;CACF,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-taskboard",
|
|
3
3
|
"description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lightweight alert modal — replaces native alert() with a themed overlay
|
|
3
|
+
* that matches the shell design tokens.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-taskboard/client/board/AlertModal
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect } from 'react'
|
|
8
|
+
|
|
9
|
+
/** Show a non-blocking alert modal. Returns true when opened. */
|
|
10
|
+
export function useAlert(): { alert: (msg: string) => void; el: React.ReactNode } {
|
|
11
|
+
const [msg, setMsg] = useState<string | null>(null)
|
|
12
|
+
const show = (m: string) => setMsg(m)
|
|
13
|
+
const close = () => setMsg(null)
|
|
14
|
+
const el = msg !== null
|
|
15
|
+
? <AlertModal message={msg} onClose={close} />
|
|
16
|
+
: null
|
|
17
|
+
return { alert: show, el }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function AlertModal({ message, onClose }: { message: string; onClose: () => void }) {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
|
23
|
+
window.addEventListener('keydown', handler)
|
|
24
|
+
return () => window.removeEventListener('keydown', handler)
|
|
25
|
+
}, [onClose])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="dsh-atb-alert-backdrop" onClick={onClose}>
|
|
29
|
+
<div className="dsh-atb-alert" onClick={e => e.stopPropagation()}>
|
|
30
|
+
<div className="dsh-atb-alert-icon">⛔</div>
|
|
31
|
+
<div className="dsh-atb-alert-msg">{message}</div>
|
|
32
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={onClose}>知道了</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { useSyncExternalStore } from 'react'
|
|
8
8
|
import type { BoardController, ControllerState } from '../controller.ts'
|
|
9
9
|
import type { TaskRecord, TaskStatus, Urgency } from '../../shared/protocol.ts'
|
|
10
|
-
import { MAIN_STATUSES } from '../../shared/protocol.ts'
|
|
10
|
+
import { MAIN_STATUSES, canTransition } from '../../shared/protocol.ts'
|
|
11
11
|
import { DRAG_TYPE, TaskCard } from './TaskCard.tsx'
|
|
12
12
|
import { TaskDetail } from './TaskDetail.tsx'
|
|
13
13
|
import { TaskFormModal } from './TaskFormModal.tsx'
|
|
14
|
+
import { useAlert } from './AlertModal.tsx'
|
|
14
15
|
|
|
15
16
|
/** Column labels. */
|
|
16
17
|
const COLUMN_LABELS: Readonly<Record<TaskStatus, string>> = {
|
|
@@ -23,8 +24,6 @@ const COLUMN_LABELS: Readonly<Record<TaskStatus, string>> = {
|
|
|
23
24
|
archived: '已归档',
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/** The two columns between which cards may be dragged both ways. */
|
|
27
|
-
const DRAGGABLE_STATUSES: ReadonlySet<TaskStatus> = new Set(['backlog', 'todo'])
|
|
28
27
|
|
|
29
28
|
/** Urgency chip labels. */
|
|
30
29
|
const URGENCY_LABELS: Readonly<Record<Urgency, string>> = {
|
|
@@ -59,12 +58,16 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
59
58
|
)
|
|
60
59
|
const live = filterTasks(state, state.ledger.tasks.filter(t => t.trashedAt === undefined))
|
|
61
60
|
const selected = state.selectedId === undefined ? undefined : state.ledger.tasks.find(t => t.id === state.selectedId)
|
|
61
|
+
const { alert: showAlert, el: alertEl } = useAlert()
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
64
|
<div className="dsh-atb-board">
|
|
65
65
|
<div className="dsh-atb-toolbar">
|
|
66
66
|
<h2 className="dsh-atb-title">Agent 任务看板</h2>
|
|
67
67
|
<span className="dsh-atb-count">{live.length} 任务 · rev {state.ledger.revision}</span>
|
|
68
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={() => controller.setComposer(true)}>
|
|
69
|
+
+ 新建任务
|
|
70
|
+
</button>
|
|
68
71
|
<div className="dsh-atb-spacer" />
|
|
69
72
|
<select
|
|
70
73
|
className="dsh-atb-select"
|
|
@@ -90,9 +93,6 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
90
93
|
<button type="button" className="dsh-atb-btn" onClick={() => controller.toggleSecondary()}>
|
|
91
94
|
{state.secondaryOpen ? '返回看板' : '其它任务'}
|
|
92
95
|
</button>
|
|
93
|
-
<button type="button" className="dsh-atb-btn" data-primary="true" onClick={() => controller.setComposer(true)}>
|
|
94
|
-
+ 新建任务
|
|
95
|
-
</button>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
98
|
{state.error !== undefined && <div className="dsh-atb-error">{state.error}</div>}
|
|
@@ -103,34 +103,31 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
103
103
|
<div className="dsh-atb-columns">
|
|
104
104
|
{MAIN_STATUSES.map(status => {
|
|
105
105
|
const columnTasks = live.filter(t => t.status === status)
|
|
106
|
-
const dropTarget = DRAGGABLE_STATUSES.has(status)
|
|
107
106
|
return (
|
|
108
107
|
<div
|
|
109
108
|
className="dsh-atb-column"
|
|
110
109
|
key={status}
|
|
111
|
-
onDragOver={
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
: undefined}
|
|
110
|
+
onDragOver={(e) => {
|
|
111
|
+
if (e.dataTransfer.types.includes(DRAG_TYPE)) {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
e.dataTransfer.dropEffect = 'move'
|
|
114
|
+
e.currentTarget.dataset.dragover = 'true'
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
onDragLeave={(e) => { delete e.currentTarget.dataset.dragover }}
|
|
118
|
+
onDrop={(e) => {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
delete e.currentTarget.dataset.dragover
|
|
121
|
+
const id = e.dataTransfer.getData(DRAG_TYPE)
|
|
122
|
+
if (id.length === 0) return
|
|
123
|
+
const task = state.ledger.tasks.find(t => t.id === id)
|
|
124
|
+
if (task === undefined || task.status === status) return
|
|
125
|
+
if (!canTransition(task.status, status)) {
|
|
126
|
+
showAlert(`无法从「${COLUMN_LABELS[task.status]}」拖至「${COLUMN_LABELS[status]}」`)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
void controller.move(id, task.version, status)
|
|
130
|
+
}}
|
|
134
131
|
>
|
|
135
132
|
<div className="dsh-atb-colhead">
|
|
136
133
|
{COLUMN_LABELS[status]}
|
|
@@ -142,7 +139,8 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
142
139
|
key={task.id}
|
|
143
140
|
task={task}
|
|
144
141
|
controller={controller}
|
|
145
|
-
draggable
|
|
142
|
+
draggable
|
|
143
|
+
onAlert={showAlert}
|
|
146
144
|
/>
|
|
147
145
|
))}
|
|
148
146
|
{columnTasks.length === 0 && <div className="dsh-atb-empty">无任务</div>}
|
|
@@ -165,6 +163,8 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
165
163
|
task={state.editingId === undefined ? undefined : state.ledger.tasks.find(t => t.id === state.editingId)}
|
|
166
164
|
/>
|
|
167
165
|
)}
|
|
166
|
+
|
|
167
|
+
{alertEl}
|
|
168
168
|
</div>
|
|
169
169
|
)
|
|
170
170
|
}
|
|
@@ -20,9 +20,10 @@ export const DRAG_TYPE = 'application/x-dsh-atb-task'
|
|
|
20
20
|
* The card view.
|
|
21
21
|
* @param task - the task record.
|
|
22
22
|
* @param controller - the controller.
|
|
23
|
-
* @param draggable - enable dragging
|
|
23
|
+
* @param draggable - enable dragging.
|
|
24
|
+
* @param onAlert - show an alert message (replaces native alert).
|
|
24
25
|
*/
|
|
25
|
-
export function TaskCard({ task, controller, draggable = false }: { task: TaskRecord; controller: BoardController; draggable?: boolean }) {
|
|
26
|
+
export function TaskCard({ task, controller, draggable = false, onAlert }: { task: TaskRecord; controller: BoardController; draggable?: boolean; onAlert?: (msg: string) => void }) {
|
|
26
27
|
const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : undefined
|
|
27
28
|
return (
|
|
28
29
|
<button
|
|
@@ -31,6 +32,15 @@ export function TaskCard({ task, controller, draggable = false }: { task: TaskRe
|
|
|
31
32
|
data-urgency={task.urgency}
|
|
32
33
|
draggable={draggable}
|
|
33
34
|
onDragStart={(e) => {
|
|
35
|
+
// Block drag if a session is still executing this task
|
|
36
|
+
const running = task.executions.find(ex => ex.outcome === 'running')
|
|
37
|
+
if (running !== undefined) {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
const msg = `该任务正在由【${task.title}】会话执行,不能拖动`
|
|
40
|
+
if (onAlert !== undefined) onAlert(msg)
|
|
41
|
+
else alert(msg)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
34
44
|
e.dataTransfer.setData(DRAG_TYPE, task.id)
|
|
35
45
|
e.dataTransfer.effectAllowed = 'move'
|
|
36
46
|
e.currentTarget.dataset.dragging = 'true'
|
|
@@ -84,6 +84,16 @@ export function TaskDetail({ task, controller }: { task: TaskRecord; controller:
|
|
|
84
84
|
</div>
|
|
85
85
|
<div className="dsh-atb-detail-topbtns">
|
|
86
86
|
<button type="button" className="dsh-atb-detail-edit" onClick={() => controller.openEditor(task.id)}>✎ 编辑</button>
|
|
87
|
+
{canRun && (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="dsh-atb-detail-run"
|
|
91
|
+
title={task.model !== undefined ? `新会话执行(${task.model.model})` : '新会话执行(默认模型)'}
|
|
92
|
+
onClick={() => void controller.run(task.id)}
|
|
93
|
+
>
|
|
94
|
+
▶ 立即执行
|
|
95
|
+
</button>
|
|
96
|
+
)}
|
|
87
97
|
<button type="button" className="dsh-atb-detail-close" aria-label="关闭" onClick={() => controller.select(undefined)}>✕</button>
|
|
88
98
|
</div>
|
|
89
99
|
</div>
|
|
@@ -103,11 +113,6 @@ export function TaskDetail({ task, controller }: { task: TaskRecord; controller:
|
|
|
103
113
|
)}
|
|
104
114
|
|
|
105
115
|
<div className="dsh-atb-detail-actions">
|
|
106
|
-
{canRun && (
|
|
107
|
-
<button type="button" className="dsh-atb-runbtn" onClick={() => void controller.run(task.id)}>
|
|
108
|
-
▶ 执行 · 新会话{task.model !== undefined ? `(${task.model.model})` : '(默认模型)'}
|
|
109
|
-
</button>
|
|
110
|
-
)}
|
|
111
116
|
<div className="dsh-atb-movebtns">
|
|
112
117
|
{moveTargets(task).map(to => to === 'done'
|
|
113
118
|
? (confirmDone
|
|
@@ -118,10 +123,10 @@ export function TaskDetail({ task, controller }: { task: TaskRecord; controller:
|
|
|
118
123
|
<button type="button" className="dsh-atb-btn" onClick={() => setConfirmDone(false)}>取消</button>
|
|
119
124
|
</span>
|
|
120
125
|
)
|
|
121
|
-
: <button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => setConfirmDone(true)}
|
|
126
|
+
: <button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => setConfirmDone(true)}>移至→{MOVE_LABEL[to]}</button>)
|
|
122
127
|
: (
|
|
123
128
|
<button key={to} type="button" className="dsh-atb-movebtn" data-to={to} onClick={() => void controller.move(task.id, task.version, to)}>
|
|
124
|
-
{MOVE_LABEL[to]}
|
|
129
|
+
移至→{MOVE_LABEL[to]}
|
|
125
130
|
</button>
|
|
126
131
|
))}
|
|
127
132
|
<button type="button" className="dsh-atb-movebtn" data-to="blocked" onClick={() => void controller.toggleBlocked(task)}>
|
|
@@ -94,6 +94,9 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
94
94
|
const cronBad = mode === 'scheduled' && (cronMatch === null || nextRun === null)
|
|
95
95
|
const valid = title.trim().length > 0 && workspaceId !== '' && !cronBad
|
|
96
96
|
|
|
97
|
+
// A task already in progress cannot be run again (host rejects it).
|
|
98
|
+
const runBlocked = editing && task.status === 'in_progress'
|
|
99
|
+
|
|
97
100
|
const submit = (): void => {
|
|
98
101
|
if (!valid) return
|
|
99
102
|
const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
|
|
@@ -121,6 +124,39 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
/** Save the form, then immediately trigger a manual run of the task. */
|
|
128
|
+
const submitAndRun = (): void => {
|
|
129
|
+
if (!valid || runBlocked) return
|
|
130
|
+
const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
|
|
131
|
+
if (editing) {
|
|
132
|
+
void (async () => {
|
|
133
|
+
const saved = await controller.update(task.id, task.version, {
|
|
134
|
+
title,
|
|
135
|
+
description,
|
|
136
|
+
prompt,
|
|
137
|
+
urgency,
|
|
138
|
+
workspaceId,
|
|
139
|
+
execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
|
|
140
|
+
model: picked ?? null,
|
|
141
|
+
})
|
|
142
|
+
if (saved) await controller.run(task.id)
|
|
143
|
+
})()
|
|
144
|
+
} else {
|
|
145
|
+
void (async () => {
|
|
146
|
+
const id = await controller.create({
|
|
147
|
+
title,
|
|
148
|
+
workspaceId,
|
|
149
|
+
urgency,
|
|
150
|
+
description: description.length > 0 ? description : undefined,
|
|
151
|
+
prompt: prompt.length > 0 ? prompt : undefined,
|
|
152
|
+
execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
|
|
153
|
+
model: picked,
|
|
154
|
+
})
|
|
155
|
+
if (id !== undefined) await controller.run(id)
|
|
156
|
+
})()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
124
160
|
const hint = !valid
|
|
125
161
|
? (title.trim().length === 0 ? '请填写标题' : workspaceId === '' ? '请选择项目' : 'Cron 表达式无效(分 时 日 月 周)')
|
|
126
162
|
: mode === 'scheduled' && nextRun !== null
|
|
@@ -233,6 +269,15 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
233
269
|
<span className="dsh-atb-modal-hint" data-tone={valid ? undefined : 'bad'}>{hint}</span>
|
|
234
270
|
<span className="dsh-atb-modal-footbtns">
|
|
235
271
|
<button type="button" className="dsh-atb-btn" onClick={() => controller.closeForm()}>取消</button>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
className="dsh-atb-btn"
|
|
275
|
+
disabled={!valid || runBlocked}
|
|
276
|
+
title={runBlocked ? '任务正在执行中,不能重复发起' : '保存后立即发起执行(新会话)'}
|
|
277
|
+
onClick={submitAndRun}
|
|
278
|
+
>
|
|
279
|
+
⚡ 立即执行
|
|
280
|
+
</button>
|
|
236
281
|
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid} onClick={submit}>
|
|
237
282
|
{editing ? '保存修改' : '创建任务'}
|
|
238
283
|
</button>
|
|
@@ -247,6 +292,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
247
292
|
interface TaskRecordLike {
|
|
248
293
|
id: string
|
|
249
294
|
version: number
|
|
295
|
+
status?: string
|
|
250
296
|
title: string
|
|
251
297
|
description: string
|
|
252
298
|
prompt: string
|
package/src/client/controller.ts
CHANGED
|
@@ -166,25 +166,29 @@ export class BoardController {
|
|
|
166
166
|
toggleSecondary(): void { this.setState({ secondaryOpen: !this.state.secondaryOpen }) }
|
|
167
167
|
|
|
168
168
|
// ---------------------------------------------------------------- writes
|
|
169
|
-
/** Create a task (composer submit). */
|
|
170
|
-
async create(body: Parameters<TaskboardClient['create']>[0]): Promise<
|
|
169
|
+
/** Create a task (composer submit); returns the new task id, undefined on failure. */
|
|
170
|
+
async create(body: Parameters<TaskboardClient['create']>[0]): Promise<string | undefined> {
|
|
171
171
|
try {
|
|
172
|
-
await this.client.create(body)
|
|
172
|
+
const summary = await this.client.create(body)
|
|
173
173
|
this.setState({ composerOpen: false, error: undefined })
|
|
174
174
|
await this.refresh()
|
|
175
|
+
return summary.id
|
|
175
176
|
} catch (error) {
|
|
176
177
|
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
178
|
+
return undefined
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
/** Edit task fields (form modal submit; the GUI is the owner surface). */
|
|
181
|
-
async update(id: string, ifVersion: number, body: Omit<UpdateTaskBody, 'ifVersion'>): Promise<
|
|
183
|
+
async update(id: string, ifVersion: number, body: Omit<UpdateTaskBody, 'ifVersion'>): Promise<boolean> {
|
|
182
184
|
try {
|
|
183
185
|
await this.client.update(id, { ifVersion, ...body })
|
|
184
186
|
this.setState({ composerOpen: false, editingId: undefined, error: undefined })
|
|
185
187
|
await this.refresh()
|
|
188
|
+
return true
|
|
186
189
|
} catch (error) {
|
|
187
190
|
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
191
|
+
return false
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
|
package/src/client/styles.ts
CHANGED
|
@@ -172,12 +172,12 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
|
|
|
172
172
|
.dsh-atb-desc { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.55; }
|
|
173
173
|
|
|
174
174
|
.dsh-atb-detail-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
175
|
-
.dsh-atb-
|
|
176
|
-
font: inherit; font-size:
|
|
177
|
-
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);
|
|
175
|
+
.dsh-atb-detail-run {
|
|
176
|
+
font: inherit; font-size: 12px; font-weight: 600; padding: 4px 11px; border-radius: 7px; cursor: pointer;
|
|
177
|
+
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);
|
|
178
178
|
transition: filter .12s ease;
|
|
179
179
|
}
|
|
180
|
-
.dsh-atb-
|
|
180
|
+
.dsh-atb-detail-run:hover { filter: brightness(1.1); }
|
|
181
181
|
.dsh-atb-movebtns { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
182
182
|
.dsh-atb-movebtn {
|
|
183
183
|
font: inherit; font-size: 12px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
|
|
@@ -376,6 +376,29 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
|
|
|
376
376
|
.dsh-atb-secondary { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
|
|
377
377
|
.dsh-atb-link { color: var(--dsw-alias-state-business-primary, #3e63dd); cursor: pointer; text-decoration: none; }
|
|
378
378
|
.dsh-atb-link:hover { text-decoration: underline; }
|
|
379
|
+
|
|
380
|
+
/* ---------- alert modal ---------- */
|
|
381
|
+
.dsh-atb-alert-backdrop {
|
|
382
|
+
position: fixed; inset: 0; z-index: 90;
|
|
383
|
+
background: var(--dsw-alias-bg-mask-drop, rgba(28,30,36,.4)); backdrop-filter: var(--dsw-mask-blur, blur(2px));
|
|
384
|
+
display: flex; align-items: center; justify-content: center;
|
|
385
|
+
animation: dsh-atb-fade .12s ease;
|
|
386
|
+
}
|
|
387
|
+
.dsh-atb-alert {
|
|
388
|
+
min-width: 280px; max-width: 380px; padding: 20px 24px; border-radius: 14px;
|
|
389
|
+
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
390
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
391
|
+
box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
|
|
392
|
+
display: flex; flex-direction: column; align-items: center; gap: 14px;
|
|
393
|
+
animation: dsh-atb-pop .14s ease;
|
|
394
|
+
}
|
|
395
|
+
.dsh-atb-alert-icon { font-size: 28px; line-height: 1; }
|
|
396
|
+
.dsh-atb-alert-msg {
|
|
397
|
+
font-size: 13.5px; line-height: 1.55; text-align: center;
|
|
398
|
+
word-break: break-word; white-space: pre-wrap;
|
|
399
|
+
color: var(--dsw-alias-label-primary, inherit);
|
|
400
|
+
}
|
|
401
|
+
.dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
|
|
379
402
|
`
|
|
380
403
|
|
|
381
404
|
let injected = false
|
package/src/host/execution.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface ExecutionDeps {
|
|
|
54
54
|
mintSessionId?: () => string
|
|
55
55
|
/** Mint message ids (injectable for tests). */
|
|
56
56
|
mintMessageId?: () => string
|
|
57
|
+
/** Best-effort session rename (pins the session list title to the task title). */
|
|
58
|
+
renameSession?: (sessionId: string, title: string) => void
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** Outcome of a run request (immediate; the run settles asynchronously). */
|
|
@@ -182,16 +184,25 @@ export class ExecutionService {
|
|
|
182
184
|
// 3. Attach the session to the workspace (GUI project session list).
|
|
183
185
|
await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })
|
|
184
186
|
|
|
187
|
+
// 3b. Best-effort rename: pin the session title to the task title so the
|
|
188
|
+
// session list shows the task name (a user-sourced title also stops
|
|
189
|
+
// automatic first-prompt retitling).
|
|
190
|
+
try {
|
|
191
|
+
this.deps.renameSession?.(sessionId, task.title)
|
|
192
|
+
} catch { /* cosmetic */ }
|
|
193
|
+
|
|
185
194
|
// 4. Record the session id (execution is really started now).
|
|
186
195
|
await this.patchExecution(executionId, { sessionId })
|
|
187
196
|
|
|
188
197
|
// 5. Submit the effective prompt as an ordinary user message and settle
|
|
189
198
|
// on quiescence (turn/end errors were already folded by the listener).
|
|
199
|
+
// Source `user` (not `plugin`) so the opening message renders as a
|
|
200
|
+
// normal user bubble in the conversation, exactly like a typed prompt.
|
|
190
201
|
const message = {
|
|
191
202
|
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
192
203
|
role: 'user' as const,
|
|
193
204
|
content: [{ type: 'text' as const, text: this.executionPrompt(task) }],
|
|
194
|
-
source: { kind: '
|
|
205
|
+
source: { kind: 'user' as const },
|
|
195
206
|
}
|
|
196
207
|
handle.agent.followup(message)
|
|
197
208
|
|
|
@@ -221,12 +232,11 @@ export class ExecutionService {
|
|
|
221
232
|
|
|
222
233
|
/** The prompt text one execution submits (task context + instructions). */
|
|
223
234
|
private executionPrompt(task: TaskRecord): string {
|
|
224
|
-
const head = `【任务看板执行】${task.title}(任务 ID: ${task.id})`
|
|
225
235
|
const state = '本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。'
|
|
226
236
|
const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;`
|
|
227
237
|
+ `2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);`
|
|
228
238
|
+ `3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`
|
|
229
|
-
return
|
|
239
|
+
return `【任务】${task.title}(任务 ID: ${task.id})\n\n${state}\n\n${effectivePrompt(task)}\n\n${tail}`
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
/** Move a task back out of in_progress after a failed start. */
|
package/src/index.ts
CHANGED
|
@@ -84,6 +84,16 @@ export function apply(ctx: Context): void {
|
|
|
84
84
|
},
|
|
85
85
|
events,
|
|
86
86
|
now,
|
|
87
|
+
renameSession: (sessionId, title) => {
|
|
88
|
+
// Best-effort: pin the execution session's title to the task title
|
|
89
|
+
// through the log-backed session-title service (user-sourced rename).
|
|
90
|
+
try {
|
|
91
|
+
const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined
|
|
92
|
+
const sessionTitle = agentCtx.get('sessionTitle') as { rename(session: unknown, title: string): unknown } | undefined
|
|
93
|
+
const session = sessions?.get(sessionId)
|
|
94
|
+
if (session !== undefined && sessionTitle !== undefined) sessionTitle.rename(session, title)
|
|
95
|
+
} catch { /* cosmetic */ }
|
|
96
|
+
},
|
|
87
97
|
defaultModel: () => {
|
|
88
98
|
try {
|
|
89
99
|
const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined
|