dsh-taskboard 0.5.0 → 0.5.1
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 +15 -0
- package/lib/client.js +219 -161
- package/lib/host/execution.js +80 -33
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +49 -5
- package/lib/host/git.js.map +1 -1
- package/lib/host/routes.js +180 -109
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +50 -28
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/sdk.js +7 -2
- package/lib/host/sdk.js.map +1 -1
- package/lib/host/store.js +41 -8
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +10 -3
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +124 -93
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +23 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/client/api.ts +19 -9
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/TaskBoard.tsx +7 -38
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +30 -23
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/controller.ts +60 -13
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +5 -1
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +227 -126
- package/src/host/scheduler.ts +62 -36
- package/src/host/sdk.ts +12 -1
- package/src/host/store.ts +53 -7
- package/src/host/templates.ts +12 -3
- package/src/host/tools.ts +180 -123
- package/src/index.ts +10 -1
- package/src/shared/api.ts +1 -1
- package/src/shared/protocol.ts +35 -1
- package/src/shared/version.ts +1 -1
- package/src/client/board/NewTaskModal.tsx +0 -8
package/lib/client.js
CHANGED
|
@@ -18,20 +18,26 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
if (!body.ok) throw new Error(`taskboard: ${body.error.code}: ${body.error.message}`);
|
|
19
19
|
return body.value;
|
|
20
20
|
}
|
|
21
|
+
/** Request timeout (S15: a hung fetch must never pin refreshInFlight forever). */
|
|
22
|
+
const TIMEOUT_MS = 1e4;
|
|
23
|
+
async function get(path) {
|
|
24
|
+
return unwrap(fetch(path, { signal: AbortSignal.timeout(TIMEOUT_MS) }));
|
|
25
|
+
}
|
|
21
26
|
async function post(path, body) {
|
|
22
27
|
return unwrap(await fetch(path, {
|
|
23
28
|
method: "POST",
|
|
24
29
|
headers: { "content-type": "application/json" },
|
|
25
|
-
body: JSON.stringify(body)
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
26
32
|
}));
|
|
27
33
|
}
|
|
28
34
|
/** Build the client over fetch + EventSource. */
|
|
29
35
|
function createClient() {
|
|
30
36
|
return {
|
|
31
|
-
state: () =>
|
|
32
|
-
workspaces: () =>
|
|
37
|
+
state: () => get("/dsh-taskboard/state"),
|
|
38
|
+
workspaces: () => get("/dsh-taskboard/workspaces"),
|
|
33
39
|
create: (body) => post("/dsh-taskboard/tasks", body),
|
|
34
|
-
get: (id) =>
|
|
40
|
+
get: (id) => get(`/dsh-taskboard/tasks/${encodeURIComponent(id)}`),
|
|
35
41
|
update: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/update`, body),
|
|
36
42
|
move: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/move`, body),
|
|
37
43
|
reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
|
|
@@ -41,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
41
47
|
cancel: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
|
|
42
48
|
mergeBranch: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/merge`, {}),
|
|
43
49
|
worktreeRemove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/worktree-remove`, body),
|
|
44
|
-
diagnostics: () =>
|
|
50
|
+
diagnostics: () => get("/dsh-taskboard/diagnostics"),
|
|
45
51
|
worktreeCleanup: (workspaceId, taskId) => post("/dsh-taskboard/worktree-cleanup", {
|
|
46
52
|
workspaceId,
|
|
47
53
|
taskId
|
|
@@ -50,28 +56,38 @@ window.__ModuleLoader__.load({
|
|
|
50
56
|
const params = new URLSearchParams({ execution: query.execution });
|
|
51
57
|
if (query.commit !== void 0) params.set("commit", query.commit);
|
|
52
58
|
if (query.path !== void 0) params.set("path", query.path);
|
|
53
|
-
return
|
|
59
|
+
return get(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`);
|
|
54
60
|
},
|
|
55
61
|
importPreview: (file) => post("/dsh-taskboard/import/preview", file),
|
|
56
62
|
importCommit: (mode, ledger) => post("/dsh-taskboard/import", {
|
|
57
63
|
mode,
|
|
58
64
|
ledger
|
|
59
65
|
}),
|
|
60
|
-
templates: () =>
|
|
66
|
+
templates: () => get("/dsh-taskboard/templates"),
|
|
61
67
|
templateUpsert: (body) => post("/dsh-taskboard/templates", body),
|
|
62
68
|
templateDelete: (id) => post("/dsh-taskboard/templates/delete", { id }),
|
|
63
|
-
settings: () =>
|
|
69
|
+
settings: () => get("/dsh-taskboard/settings"),
|
|
64
70
|
updateSettings: (body) => post("/dsh-taskboard/settings/update", body),
|
|
65
71
|
stream(onChange, onGap) {
|
|
66
72
|
const es = new EventSource("/dsh-taskboard/events");
|
|
67
73
|
let revision;
|
|
68
74
|
const hello = (event) => {
|
|
69
|
-
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = JSON.parse(event.data);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
70
81
|
if (revision !== void 0 && payload.revision !== revision) onGap();
|
|
71
82
|
revision = payload.revision;
|
|
72
83
|
};
|
|
73
84
|
const change = (event) => {
|
|
74
|
-
|
|
85
|
+
let payload;
|
|
86
|
+
try {
|
|
87
|
+
payload = JSON.parse(event.data);
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
75
91
|
if (revision !== void 0 && payload.revision !== revision + 1) onGap();
|
|
76
92
|
revision = payload.revision;
|
|
77
93
|
onChange(payload);
|
|
@@ -304,7 +320,11 @@ window.__ModuleLoader__.load({
|
|
|
304
320
|
disposed = false;
|
|
305
321
|
disposeStream;
|
|
306
322
|
refreshInFlight;
|
|
323
|
+
/** Newest change-frame revision seen on the SSE stream (S16 refresh chase). */
|
|
324
|
+
seenRevision;
|
|
307
325
|
sessionJumper;
|
|
326
|
+
/** Composer catalog faces, installed formally by the client entry (T13). */
|
|
327
|
+
catalogFaces = {};
|
|
308
328
|
/** @param client - the route client. */
|
|
309
329
|
constructor(client) {
|
|
310
330
|
this.client = client;
|
|
@@ -333,10 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
333
353
|
start() {
|
|
334
354
|
this.refresh();
|
|
335
355
|
this.disposeStream = this.client.stream((change) => {
|
|
336
|
-
this.
|
|
337
|
-
...this.state.ledger,
|
|
338
|
-
revision: change.revision
|
|
339
|
-
} });
|
|
356
|
+
this.seenRevision = change.revision;
|
|
340
357
|
this.refresh();
|
|
341
358
|
}, () => {
|
|
342
359
|
this.refresh();
|
|
@@ -347,15 +364,18 @@ window.__ModuleLoader__.load({
|
|
|
347
364
|
if (this.refreshInFlight !== void 0) return this.refreshInFlight;
|
|
348
365
|
this.refreshInFlight = (async () => {
|
|
349
366
|
try {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
for (let round = 0; round < 3; round++) {
|
|
368
|
+
const [ledger, workspaces] = await Promise.all([this.client.state(), this.client.workspaces()]);
|
|
369
|
+
let selected;
|
|
370
|
+
if (this.state.selectedId !== void 0) selected = ledger.tasks.find((t) => t.id === this.state.selectedId);
|
|
371
|
+
this.setState({
|
|
372
|
+
ledger,
|
|
373
|
+
workspaces,
|
|
374
|
+
error: void 0,
|
|
375
|
+
selectedId: selected === void 0 ? void 0 : this.state.selectedId
|
|
376
|
+
});
|
|
377
|
+
if (this.seenRevision === void 0 || ledger.revision >= this.seenRevision) break;
|
|
378
|
+
}
|
|
359
379
|
} catch (error) {
|
|
360
380
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
361
381
|
} finally {
|
|
@@ -473,6 +493,22 @@ window.__ModuleLoader__.load({
|
|
|
473
493
|
installSessionJumper(jumper) {
|
|
474
494
|
this.sessionJumper = jumper;
|
|
475
495
|
}
|
|
496
|
+
/** T13: formal installers for the composer catalog faces (was a monkeypatch from the client entry). */
|
|
497
|
+
installModelCatalog(fn) {
|
|
498
|
+
this.catalogFaces.models = fn;
|
|
499
|
+
}
|
|
500
|
+
/** T13: formal installer for the preset roster face. */
|
|
501
|
+
installPresetRoster(fn) {
|
|
502
|
+
this.catalogFaces.presets = fn;
|
|
503
|
+
}
|
|
504
|
+
/** The installed model catalog face, when the runtime provides one. */
|
|
505
|
+
get modelCatalog() {
|
|
506
|
+
return this.catalogFaces.models;
|
|
507
|
+
}
|
|
508
|
+
/** The installed preset roster face, when the runtime provides one. */
|
|
509
|
+
get presetCatalog() {
|
|
510
|
+
return this.catalogFaces.presets;
|
|
511
|
+
}
|
|
476
512
|
/**
|
|
477
513
|
* Jump to an execution's session (open it in the GUI). On success the board
|
|
478
514
|
* closes so the conversation shows; a deleted-or-archived session reports
|
|
@@ -606,13 +642,18 @@ window.__ModuleLoader__.load({
|
|
|
606
642
|
return;
|
|
607
643
|
}
|
|
608
644
|
}
|
|
609
|
-
/**
|
|
645
|
+
/**
|
|
646
|
+
* Append a user comment. Returns whether it landed — the composer keeps its
|
|
647
|
+
* text on failure (T13: it used to clear unconditionally and lose the draft).
|
|
648
|
+
*/
|
|
610
649
|
async comment(id, body) {
|
|
611
650
|
try {
|
|
612
651
|
await this.client.comment(id, body);
|
|
613
652
|
await this.refresh();
|
|
653
|
+
return true;
|
|
614
654
|
} catch (error) {
|
|
615
655
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
656
|
+
return false;
|
|
616
657
|
}
|
|
617
658
|
}
|
|
618
659
|
/** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
|
|
@@ -730,7 +771,7 @@ window.__ModuleLoader__.load({
|
|
|
730
771
|
async duplicate(task) {
|
|
731
772
|
try {
|
|
732
773
|
await this.client.create({
|
|
733
|
-
title: `${task.title}(副本)`,
|
|
774
|
+
title: `${task.title.slice(0, 196)}(副本)`,
|
|
734
775
|
workspaceId: task.workspaceId,
|
|
735
776
|
urgency: task.urgency,
|
|
736
777
|
description: task.description.length > 0 ? task.description : void 0,
|
|
@@ -855,7 +896,8 @@ window.__ModuleLoader__.load({
|
|
|
855
896
|
/** Download the task list as a CSV (BOM-prefixed for Excel + Chinese text). */
|
|
856
897
|
exportCsv() {
|
|
857
898
|
const esc = (v) => {
|
|
858
|
-
|
|
899
|
+
let s = String(v ?? "");
|
|
900
|
+
if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
859
901
|
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, "\"\"")}"` : s;
|
|
860
902
|
};
|
|
861
903
|
const header = [
|
|
@@ -1818,7 +1860,8 @@ window.__ModuleLoader__.load({
|
|
|
1818
1860
|
found: false,
|
|
1819
1861
|
placed: false
|
|
1820
1862
|
};
|
|
1821
|
-
|
|
1863
|
+
const host = globalThis.location?.hostname;
|
|
1864
|
+
if (host === "localhost" || host === "127.0.0.1") window.__atbDebug = debug;
|
|
1822
1865
|
let root;
|
|
1823
1866
|
let placed = false;
|
|
1824
1867
|
const tryPlace = () => {
|
|
@@ -1889,7 +1932,70 @@ window.__ModuleLoader__.load({
|
|
|
1889
1932
|
* @module dsh-taskboard/shared/version
|
|
1890
1933
|
*/
|
|
1891
1934
|
/** The package version (must equal package.json "version"). */
|
|
1892
|
-
const PLUGIN_VERSION = "0.5.
|
|
1935
|
+
const PLUGIN_VERSION = "0.5.1";
|
|
1936
|
+
|
|
1937
|
+
//#endregion
|
|
1938
|
+
//#region src/client/board/labels.ts
|
|
1939
|
+
/** Column headers on the five-column main board (+ secondary tab). */
|
|
1940
|
+
const COLUMN_LABELS = {
|
|
1941
|
+
backlog: "待规划",
|
|
1942
|
+
todo: "待办",
|
|
1943
|
+
in_progress: "进行中",
|
|
1944
|
+
in_review: "待验收",
|
|
1945
|
+
done: "已完成",
|
|
1946
|
+
canceled: "已取消",
|
|
1947
|
+
archived: "已归档"
|
|
1948
|
+
};
|
|
1949
|
+
/** Status pill text (detail pane) — historical wording kept verbatim:
|
|
1950
|
+
* terminal states read short here, the column headers carry the full forms. */
|
|
1951
|
+
const STATUS_LABEL = {
|
|
1952
|
+
backlog: "待规划",
|
|
1953
|
+
todo: "待办",
|
|
1954
|
+
in_progress: "进行中",
|
|
1955
|
+
in_review: "待验收",
|
|
1956
|
+
done: "完成",
|
|
1957
|
+
canceled: "取消",
|
|
1958
|
+
archived: "归档"
|
|
1959
|
+
};
|
|
1960
|
+
/** Move-button verbs (shorter than the pill text). */
|
|
1961
|
+
const MOVE_LABEL = {
|
|
1962
|
+
backlog: "待规划",
|
|
1963
|
+
todo: "待办",
|
|
1964
|
+
in_progress: "进行中",
|
|
1965
|
+
in_review: "待验收",
|
|
1966
|
+
done: "完成",
|
|
1967
|
+
canceled: "取消",
|
|
1968
|
+
archived: "归档"
|
|
1969
|
+
};
|
|
1970
|
+
/** Urgency chip labels. */
|
|
1971
|
+
const URGENCY_LABEL = {
|
|
1972
|
+
urgent: "紧急",
|
|
1973
|
+
normal: "一般",
|
|
1974
|
+
relaxed: "不急"
|
|
1975
|
+
};
|
|
1976
|
+
/** Execution outcome labels. */
|
|
1977
|
+
const OUTCOME_LABEL = {
|
|
1978
|
+
running: "执行中",
|
|
1979
|
+
succeeded: "成功",
|
|
1980
|
+
failed: "失败",
|
|
1981
|
+
cancelled: "已取消"
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
//#endregion
|
|
1985
|
+
//#region src/client/board/format.ts
|
|
1986
|
+
/** Format an epoch ms as a short local stamp. */
|
|
1987
|
+
function fmtTime(ms) {
|
|
1988
|
+
if (ms === void 0) return "";
|
|
1989
|
+
const d = new Date(ms);
|
|
1990
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1991
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1992
|
+
}
|
|
1993
|
+
/** A claim idle for longer than this is highlighted as stale (ms). */
|
|
1994
|
+
const STALE_CLAIM_MS = 30 * 6e4;
|
|
1995
|
+
/** Whether the task's claim is stale (in_progress, held, idle too long). */
|
|
1996
|
+
function isStaleClaim(task, now) {
|
|
1997
|
+
return task.status === "in_progress" && task.claimedAt !== void 0 && now - task.claimedAt > 18e5;
|
|
1998
|
+
}
|
|
1893
1999
|
|
|
1894
2000
|
//#endregion
|
|
1895
2001
|
//#region src/client/board/TaskCard.tsx
|
|
@@ -1906,17 +2012,6 @@ window.__ModuleLoader__.load({
|
|
|
1906
2012
|
*
|
|
1907
2013
|
* @module dsh-taskboard/client/board/TaskCard
|
|
1908
2014
|
*/
|
|
1909
|
-
const URGENCY_LABEL$1 = {
|
|
1910
|
-
urgent: "紧急",
|
|
1911
|
-
normal: "一般",
|
|
1912
|
-
relaxed: "不急"
|
|
1913
|
-
};
|
|
1914
|
-
const OUTCOME_LABEL$1 = {
|
|
1915
|
-
running: "执行中",
|
|
1916
|
-
succeeded: "成功",
|
|
1917
|
-
failed: "失败",
|
|
1918
|
-
cancelled: "已取消"
|
|
1919
|
-
};
|
|
1920
2015
|
/** dataTransfer type carrying the dragged task id. */
|
|
1921
2016
|
const DRAG_TYPE = "application/x-dsh-atb-task";
|
|
1922
2017
|
/**
|
|
@@ -1952,7 +2047,7 @@ window.__ModuleLoader__.load({
|
|
|
1952
2047
|
onDragStart: (e) => {
|
|
1953
2048
|
if (running !== void 0) {
|
|
1954
2049
|
e.preventDefault();
|
|
1955
|
-
const msg =
|
|
2050
|
+
const msg = `该任务正由会话执行中(${task.title}),不能拖动`;
|
|
1956
2051
|
if (onAlert !== void 0) onAlert(msg);
|
|
1957
2052
|
else alert(msg);
|
|
1958
2053
|
return;
|
|
@@ -1982,7 +2077,7 @@ window.__ModuleLoader__.load({
|
|
|
1982
2077
|
children: [
|
|
1983
2078
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1984
2079
|
className: "dsh-atb-badge",
|
|
1985
|
-
children: URGENCY_LABEL
|
|
2080
|
+
children: URGENCY_LABEL[task.urgency]
|
|
1986
2081
|
}),
|
|
1987
2082
|
task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1988
2083
|
className: "dsh-atb-badge",
|
|
@@ -2022,7 +2117,7 @@ window.__ModuleLoader__.load({
|
|
|
2022
2117
|
last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2023
2118
|
className: "dsh-atb-badge",
|
|
2024
2119
|
"data-kind": last.outcome === "running" ? "running" : last.outcome,
|
|
2025
|
-
children: OUTCOME_LABEL
|
|
2120
|
+
children: OUTCOME_LABEL[last.outcome] ?? last.outcome
|
|
2026
2121
|
}),
|
|
2027
2122
|
task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
|
|
2028
2123
|
task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2175,27 +2270,6 @@ window.__ModuleLoader__.load({
|
|
|
2175
2270
|
"archived"
|
|
2176
2271
|
].filter((to) => canTransition(task.status, to));
|
|
2177
2272
|
}
|
|
2178
|
-
const MOVE_LABEL = {
|
|
2179
|
-
backlog: "待规划",
|
|
2180
|
-
todo: "待办",
|
|
2181
|
-
in_progress: "进行中",
|
|
2182
|
-
in_review: "待验收",
|
|
2183
|
-
done: "完成",
|
|
2184
|
-
canceled: "取消",
|
|
2185
|
-
archived: "归档"
|
|
2186
|
-
};
|
|
2187
|
-
const STATUS_LABEL = { ...MOVE_LABEL };
|
|
2188
|
-
const URGENCY_LABEL = {
|
|
2189
|
-
urgent: "紧急",
|
|
2190
|
-
normal: "一般",
|
|
2191
|
-
relaxed: "不急"
|
|
2192
|
-
};
|
|
2193
|
-
const OUTCOME_LABEL = {
|
|
2194
|
-
running: "执行中",
|
|
2195
|
-
succeeded: "成功",
|
|
2196
|
-
failed: "失败",
|
|
2197
|
-
cancelled: "已取消"
|
|
2198
|
-
};
|
|
2199
2273
|
/** Compact session-id display (execution sessions carry the taskboard infix). */
|
|
2200
2274
|
function shortId(id) {
|
|
2201
2275
|
if (id === void 0) return "";
|
|
@@ -2664,6 +2738,7 @@ window.__ModuleLoader__.load({
|
|
|
2664
2738
|
const [confirmDone, setConfirmDone] = (0, react.useState)(false);
|
|
2665
2739
|
const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
|
|
2666
2740
|
const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
|
|
2741
|
+
const [actionBusy, setActionBusy] = (0, react.useState)(false);
|
|
2667
2742
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
2668
2743
|
const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
|
|
2669
2744
|
const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
|
|
@@ -2671,6 +2746,12 @@ window.__ModuleLoader__.load({
|
|
|
2671
2746
|
const holder = task.status === "in_progress" ? task.claimedBy : void 0;
|
|
2672
2747
|
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
2673
2748
|
const unchecked = (task.checklist ?? []).filter((i) => !i.checked).length;
|
|
2749
|
+
/** Fire one top action under the shared busy guard; re-enable on settle. */
|
|
2750
|
+
const runAction = (action) => {
|
|
2751
|
+
if (actionBusy) return;
|
|
2752
|
+
setActionBusy(true);
|
|
2753
|
+
action().catch(() => void 0).finally(() => setActionBusy(false));
|
|
2754
|
+
};
|
|
2674
2755
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
2675
2756
|
const jumpToSession = (sessionId) => {
|
|
2676
2757
|
controller.openSession(sessionId).then((result) => {
|
|
@@ -2770,7 +2851,7 @@ window.__ModuleLoader__.load({
|
|
|
2770
2851
|
"更新 ",
|
|
2771
2852
|
fmtTime(task.updatedAt),
|
|
2772
2853
|
" · 最近操作 ",
|
|
2773
|
-
task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : "👤 用户"
|
|
2854
|
+
task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : task.updatedBy.kind === "system" ? "⚙️ 系统" : "👤 用户"
|
|
2774
2855
|
]
|
|
2775
2856
|
})
|
|
2776
2857
|
]
|
|
@@ -2787,32 +2868,34 @@ window.__ModuleLoader__.load({
|
|
|
2787
2868
|
type: "button",
|
|
2788
2869
|
className: "dsh-atb-detail-edit",
|
|
2789
2870
|
title: "复制此任务的全部配置为一张新卡(待办列)",
|
|
2790
|
-
|
|
2871
|
+
disabled: actionBusy,
|
|
2872
|
+
onClick: () => runAction(() => controller.duplicate(task)),
|
|
2791
2873
|
children: "⧉ 复制"
|
|
2792
2874
|
}),
|
|
2793
2875
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2794
2876
|
type: "button",
|
|
2795
2877
|
className: "dsh-atb-detail-edit",
|
|
2796
2878
|
title: "把此任务的配置(含清单)保存为模板,新建任务时可用",
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
},
|
|
2879
|
+
disabled: actionBusy,
|
|
2880
|
+
onClick: () => runAction(async () => {
|
|
2881
|
+
if (await controller.saveAsTemplate(task)) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
|
|
2882
|
+
}),
|
|
2802
2883
|
children: "⌗ 存为模板"
|
|
2803
2884
|
}),
|
|
2804
2885
|
canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2805
2886
|
type: "button",
|
|
2806
2887
|
className: "dsh-atb-detail-run",
|
|
2807
2888
|
title: "续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线",
|
|
2808
|
-
|
|
2889
|
+
disabled: actionBusy,
|
|
2890
|
+
onClick: () => runAction(() => controller.run(task.id, true)),
|
|
2809
2891
|
children: "↻ 续跑"
|
|
2810
2892
|
}),
|
|
2811
2893
|
canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2812
2894
|
type: "button",
|
|
2813
2895
|
className: "dsh-atb-detail-run",
|
|
2814
2896
|
title: task.model !== void 0 ? `新会话执行(${task.model.model})` : "新会话执行(默认模型)",
|
|
2815
|
-
|
|
2897
|
+
disabled: actionBusy,
|
|
2898
|
+
onClick: () => runAction(() => controller.run(task.id)),
|
|
2816
2899
|
children: "▶ 立即执行"
|
|
2817
2900
|
}),
|
|
2818
2901
|
runningExecution !== void 0 && (confirmCancel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -2986,18 +3069,18 @@ window.__ModuleLoader__.load({
|
|
|
2986
3069
|
placeholder: "以用户身份留言(agent 开工前会读)…",
|
|
2987
3070
|
onChange: (e) => setComment(e.target.value),
|
|
2988
3071
|
onKeyDown: (e) => {
|
|
2989
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) {
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
}
|
|
3072
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) controller.comment(task.id, comment).then((ok) => {
|
|
3073
|
+
if (ok) setComment("");
|
|
3074
|
+
});
|
|
2993
3075
|
}
|
|
2994
3076
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2995
3077
|
type: "button",
|
|
2996
3078
|
className: "dsh-atb-composer-send",
|
|
2997
3079
|
disabled: comment.trim().length === 0,
|
|
2998
3080
|
onClick: () => {
|
|
2999
|
-
controller.comment(task.id, comment)
|
|
3000
|
-
|
|
3081
|
+
controller.comment(task.id, comment).then((ok) => {
|
|
3082
|
+
if (ok) setComment("");
|
|
3083
|
+
});
|
|
3001
3084
|
},
|
|
3002
3085
|
children: "发表"
|
|
3003
3086
|
})]
|
|
@@ -3261,6 +3344,7 @@ window.__ModuleLoader__.load({
|
|
|
3261
3344
|
checked: false
|
|
3262
3345
|
})));
|
|
3263
3346
|
const titleRef = (0, react.useRef)(null);
|
|
3347
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3264
3348
|
(0, react.useEffect)(() => {
|
|
3265
3349
|
titleRef.current?.focus();
|
|
3266
3350
|
const onKey = (e) => {
|
|
@@ -3280,10 +3364,11 @@ window.__ModuleLoader__.load({
|
|
|
3280
3364
|
face().then((roster) => {
|
|
3281
3365
|
setPresets(roster.presets);
|
|
3282
3366
|
setPresetDefault(roster.defaultId);
|
|
3283
|
-
if (
|
|
3367
|
+
if (!editing && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
|
|
3284
3368
|
}).catch(() => setPresets([]));
|
|
3285
3369
|
}, [
|
|
3286
3370
|
controller,
|
|
3371
|
+
editing,
|
|
3287
3372
|
task?.presetId,
|
|
3288
3373
|
initialPreset
|
|
3289
3374
|
]);
|
|
@@ -3311,12 +3396,13 @@ window.__ModuleLoader__.load({
|
|
|
3311
3396
|
text: r.text.trim()
|
|
3312
3397
|
})).filter((r) => r.text.length > 0);
|
|
3313
3398
|
const submit = () => {
|
|
3314
|
-
if (!valid) return;
|
|
3399
|
+
if (!valid || busy) return;
|
|
3315
3400
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3316
3401
|
const isolationOut = isolationPayload();
|
|
3317
3402
|
const presetOut = presetPayload();
|
|
3318
3403
|
const rows = filledRows();
|
|
3319
|
-
|
|
3404
|
+
setBusy(true);
|
|
3405
|
+
(editing ? controller.update(task.id, task.version, {
|
|
3320
3406
|
title,
|
|
3321
3407
|
description,
|
|
3322
3408
|
prompt,
|
|
@@ -3330,8 +3416,7 @@ window.__ModuleLoader__.load({
|
|
|
3330
3416
|
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
3331
3417
|
presetId: presetOut ?? null,
|
|
3332
3418
|
checklist: rows.length > 0 ? rows : null
|
|
3333
|
-
})
|
|
3334
|
-
else controller.create({
|
|
3419
|
+
}) : controller.create({
|
|
3335
3420
|
title,
|
|
3336
3421
|
workspaceId,
|
|
3337
3422
|
urgency,
|
|
@@ -3345,50 +3430,52 @@ window.__ModuleLoader__.load({
|
|
|
3345
3430
|
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
3346
3431
|
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3347
3432
|
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
3348
|
-
});
|
|
3433
|
+
})).catch(() => void 0).finally(() => setBusy(false));
|
|
3349
3434
|
};
|
|
3350
3435
|
/** Save the form, then immediately trigger a manual run of the task. */
|
|
3351
3436
|
const submitAndRun = () => {
|
|
3352
|
-
if (!valid || runBlocked) return;
|
|
3437
|
+
if (!valid || runBlocked || busy) return;
|
|
3353
3438
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3354
3439
|
const isolationOut = isolationPayload();
|
|
3355
3440
|
const presetOut = presetPayload();
|
|
3356
3441
|
const rows = filledRows();
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
mode
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3442
|
+
setBusy(true);
|
|
3443
|
+
(async () => {
|
|
3444
|
+
if (editing) {
|
|
3445
|
+
if (await controller.update(task.id, task.version, {
|
|
3446
|
+
title,
|
|
3447
|
+
description,
|
|
3448
|
+
prompt,
|
|
3449
|
+
urgency,
|
|
3450
|
+
workspaceId,
|
|
3451
|
+
execution: mode === "scheduled" ? {
|
|
3452
|
+
mode,
|
|
3453
|
+
cron: cron.trim()
|
|
3454
|
+
} : { mode },
|
|
3455
|
+
model: picked ?? null,
|
|
3456
|
+
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
3457
|
+
presetId: presetOut ?? null,
|
|
3458
|
+
checklist: rows.length > 0 ? rows : null
|
|
3459
|
+
})) await controller.run(task.id);
|
|
3460
|
+
} else {
|
|
3461
|
+
const id = await controller.create({
|
|
3462
|
+
title,
|
|
3463
|
+
workspaceId,
|
|
3464
|
+
urgency,
|
|
3465
|
+
description: description.length > 0 ? description : void 0,
|
|
3466
|
+
prompt: prompt.length > 0 ? prompt : void 0,
|
|
3467
|
+
execution: mode === "scheduled" ? {
|
|
3468
|
+
mode,
|
|
3469
|
+
cron: cron.trim()
|
|
3470
|
+
} : { mode },
|
|
3471
|
+
model: picked,
|
|
3472
|
+
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
3473
|
+
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3474
|
+
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
3475
|
+
});
|
|
3476
|
+
if (id !== void 0) await controller.run(id);
|
|
3477
|
+
}
|
|
3478
|
+
})().catch(() => void 0).finally(() => setBusy(false));
|
|
3392
3479
|
};
|
|
3393
3480
|
const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
|
|
3394
3481
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -3654,8 +3741,8 @@ window.__ModuleLoader__.load({
|
|
|
3654
3741
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3655
3742
|
type: "button",
|
|
3656
3743
|
className: "dsh-atb-btn",
|
|
3657
|
-
disabled: !valid || runBlocked,
|
|
3658
|
-
title: runBlocked ? "任务正在执行中,不能重复发起" : "保存后立即发起执行(新会话)",
|
|
3744
|
+
disabled: !valid || runBlocked || busy,
|
|
3745
|
+
title: runBlocked ? "任务正在执行中,不能重复发起" : busy ? "正在提交…" : "保存后立即发起执行(新会话)",
|
|
3659
3746
|
onClick: submitAndRun,
|
|
3660
3747
|
children: "⚡ 立即执行"
|
|
3661
3748
|
}),
|
|
@@ -3663,7 +3750,7 @@ window.__ModuleLoader__.load({
|
|
|
3663
3750
|
type: "button",
|
|
3664
3751
|
className: "dsh-atb-btn",
|
|
3665
3752
|
"data-primary": "true",
|
|
3666
|
-
disabled: !valid,
|
|
3753
|
+
disabled: !valid || busy,
|
|
3667
3754
|
onClick: submit,
|
|
3668
3755
|
children: editing ? "保存修改" : "创建任务"
|
|
3669
3756
|
})
|
|
@@ -4045,7 +4132,7 @@ window.__ModuleLoader__.load({
|
|
|
4045
4132
|
className: "dsh-atb-btn",
|
|
4046
4133
|
"data-primary": "true",
|
|
4047
4134
|
"data-danger": mode === "replace" && confirmReplace ? "true" : void 0,
|
|
4048
|
-
disabled: plan === void 0 || busy
|
|
4135
|
+
disabled: plan === void 0 || busy,
|
|
4049
4136
|
onClick: commit,
|
|
4050
4137
|
children: mode === "replace" && confirmReplace ? "确认整册替换" : "执行导入"
|
|
4051
4138
|
})]
|
|
@@ -4235,35 +4322,6 @@ window.__ModuleLoader__.load({
|
|
|
4235
4322
|
*
|
|
4236
4323
|
* @module dsh-taskboard/client/board/TaskBoard
|
|
4237
4324
|
*/
|
|
4238
|
-
/** Column labels. */
|
|
4239
|
-
const COLUMN_LABELS = {
|
|
4240
|
-
backlog: "待规划",
|
|
4241
|
-
todo: "待办",
|
|
4242
|
-
in_progress: "进行中",
|
|
4243
|
-
in_review: "待验收",
|
|
4244
|
-
done: "已完成",
|
|
4245
|
-
canceled: "已取消",
|
|
4246
|
-
archived: "已归档"
|
|
4247
|
-
};
|
|
4248
|
-
/** Urgency chip labels. */
|
|
4249
|
-
const URGENCY_LABELS = {
|
|
4250
|
-
urgent: "紧急",
|
|
4251
|
-
normal: "一般",
|
|
4252
|
-
relaxed: "不急"
|
|
4253
|
-
};
|
|
4254
|
-
/** Format an epoch ms as a short local stamp. */
|
|
4255
|
-
function fmtTime(ms) {
|
|
4256
|
-
if (ms === void 0) return "";
|
|
4257
|
-
const d = new Date(ms);
|
|
4258
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
4259
|
-
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
4260
|
-
}
|
|
4261
|
-
/** A claim idle for longer than this is highlighted as stale (ms). */
|
|
4262
|
-
const STALE_CLAIM_MS = 30 * 6e4;
|
|
4263
|
-
/** Whether the task's claim is stale (in_progress, held, idle too long). */
|
|
4264
|
-
function isStaleClaim(task, now) {
|
|
4265
|
-
return task.status === "in_progress" && task.claimedAt !== void 0 && now - task.claimedAt > 18e5;
|
|
4266
|
-
}
|
|
4267
4325
|
/** Urgency sort rank (urgent first). */
|
|
4268
4326
|
const URGENCY_RANK = {
|
|
4269
4327
|
urgent: 0,
|
|
@@ -4344,7 +4402,7 @@ window.__ModuleLoader__.load({
|
|
|
4344
4402
|
},
|
|
4345
4403
|
children: "空白任务"
|
|
4346
4404
|
}),
|
|
4347
|
-
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4405
|
+
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4348
4406
|
type: "button",
|
|
4349
4407
|
className: "dsh-atb-newmenu-opt",
|
|
4350
4408
|
title: t.task.description !== void 0 && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name,
|
|
@@ -4352,7 +4410,7 @@ window.__ModuleLoader__.load({
|
|
|
4352
4410
|
closeMenu();
|
|
4353
4411
|
controller.newFromTemplate(t.task);
|
|
4354
4412
|
},
|
|
4355
|
-
children:
|
|
4413
|
+
children: t.name
|
|
4356
4414
|
}, t.id)),
|
|
4357
4415
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-newmenu-sep" }),
|
|
4358
4416
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -4424,7 +4482,7 @@ window.__ModuleLoader__.load({
|
|
|
4424
4482
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4425
4483
|
className: "dsh-atb-dot",
|
|
4426
4484
|
"data-urgency": u
|
|
4427
|
-
}),
|
|
4485
|
+
}), URGENCY_LABEL[u]]
|
|
4428
4486
|
}, u)),
|
|
4429
4487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4430
4488
|
type: "button",
|
|
@@ -4567,7 +4625,7 @@ window.__ModuleLoader__.load({
|
|
|
4567
4625
|
task: selected,
|
|
4568
4626
|
controller,
|
|
4569
4627
|
now
|
|
4570
|
-
})
|
|
4628
|
+
}, selected.id)
|
|
4571
4629
|
}),
|
|
4572
4630
|
state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
|
|
4573
4631
|
controller,
|
|
@@ -4917,7 +4975,7 @@ window.__ModuleLoader__.load({
|
|
|
4917
4975
|
const controller = new BoardController(createClient());
|
|
4918
4976
|
const connection = ctx.get?.("connection");
|
|
4919
4977
|
if (connection !== void 0) {
|
|
4920
|
-
controller.
|
|
4978
|
+
controller.installModelCatalog(async () => {
|
|
4921
4979
|
const response = await connection.api.llm.models({});
|
|
4922
4980
|
if (!response.result.ok) return [];
|
|
4923
4981
|
const out = [];
|
|
@@ -4927,8 +4985,8 @@ window.__ModuleLoader__.load({
|
|
|
4927
4985
|
name: model.name
|
|
4928
4986
|
});
|
|
4929
4987
|
return out;
|
|
4930
|
-
};
|
|
4931
|
-
controller.
|
|
4988
|
+
});
|
|
4989
|
+
controller.installPresetRoster(async () => {
|
|
4932
4990
|
const list = connection.api.agentPresets;
|
|
4933
4991
|
if (list === void 0) return { presets: [] };
|
|
4934
4992
|
const response = await list.list({});
|
|
@@ -4942,7 +5000,7 @@ window.__ModuleLoader__.load({
|
|
|
4942
5000
|
presets,
|
|
4943
5001
|
...def !== void 0 ? { defaultId: def.id } : {}
|
|
4944
5002
|
};
|
|
4945
|
-
};
|
|
5003
|
+
});
|
|
4946
5004
|
}
|
|
4947
5005
|
controller.installSessionJumper(createSessionJumper({
|
|
4948
5006
|
getSessions: () => ctx.get?.("sessions"),
|