dsh-taskboard 0.4.5 → 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 +24 -1
- package/lib/client.js +434 -193
- 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 +210 -112
- 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 +128 -97
- 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 +48 -5
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +9 -8
- package/src/client/api.ts +26 -8
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/SettingsModal.tsx +84 -0
- package/src/client/board/TaskBoard.tsx +47 -40
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +39 -31
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/controller.ts +86 -34
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +5 -1
- package/src/client/styles.ts +4 -0
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +263 -128
- 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 +187 -126
- package/src/index.ts +10 -1
- package/src/shared/api.ts +11 -2
- package/src/shared/protocol.ts +83 -6
- 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,26 +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 }),
|
|
69
|
+
settings: () => get("/dsh-taskboard/settings"),
|
|
70
|
+
updateSettings: (body) => post("/dsh-taskboard/settings/update", body),
|
|
63
71
|
stream(onChange, onGap) {
|
|
64
72
|
const es = new EventSource("/dsh-taskboard/events");
|
|
65
73
|
let revision;
|
|
66
74
|
const hello = (event) => {
|
|
67
|
-
|
|
75
|
+
let payload;
|
|
76
|
+
try {
|
|
77
|
+
payload = JSON.parse(event.data);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
68
81
|
if (revision !== void 0 && payload.revision !== revision) onGap();
|
|
69
82
|
revision = payload.revision;
|
|
70
83
|
};
|
|
71
84
|
const change = (event) => {
|
|
72
|
-
|
|
85
|
+
let payload;
|
|
86
|
+
try {
|
|
87
|
+
payload = JSON.parse(event.data);
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
73
91
|
if (revision !== void 0 && payload.revision !== revision + 1) onGap();
|
|
74
92
|
revision = payload.revision;
|
|
75
93
|
onChange(payload);
|
|
@@ -134,6 +152,16 @@ window.__ModuleLoader__.load({
|
|
|
134
152
|
return TRANSITIONS[from].includes(to);
|
|
135
153
|
}
|
|
136
154
|
/**
|
|
155
|
+
* Factory-default isolation (0.5.0): 原目录执行. Applies when neither the
|
|
156
|
+
* task record nor the board setting (`BoardSettings.defaultIsolation`)
|
|
157
|
+
* says otherwise. Before 0.5.0 the implicit default was 'worktree'.
|
|
158
|
+
*/
|
|
159
|
+
const DEFAULT_ISOLATION = "none";
|
|
160
|
+
/** The effective default isolation for NEW tasks (board setting → factory default). */
|
|
161
|
+
function defaultIsolationOf(settings) {
|
|
162
|
+
return settings?.defaultIsolation ?? "none";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
137
165
|
* Parse a five-field cron expression. Supported field syntax: star, star/step
|
|
138
166
|
* (`* / n` without spaces), a single number, an `a-b` range, and comma lists
|
|
139
167
|
* of those. Day-of-week accepts both 0 and 7 as Sunday (normalized to 0).
|
|
@@ -238,22 +266,6 @@ window.__ModuleLoader__.load({
|
|
|
238
266
|
//#region src/client/controller.ts
|
|
239
267
|
/** localStorage key for persisted view state (filters + sort). */
|
|
240
268
|
const VIEW_KEY = "dsh-taskboard-view-v1";
|
|
241
|
-
/** localStorage key for the remembered isolation toggle choice (0.3.0). */
|
|
242
|
-
const ISOLATION_KEY = "dsh-taskboard-isolation-v1";
|
|
243
|
-
/** Load the remembered default isolation (worktree unless explicitly turned off). */
|
|
244
|
-
function loadDefaultIsolation() {
|
|
245
|
-
try {
|
|
246
|
-
return localStorage.getItem(ISOLATION_KEY) === "none" ? "none" : "worktree";
|
|
247
|
-
} catch {
|
|
248
|
-
return "worktree";
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
/** Remember the isolation toggle choice across forms (best effort). */
|
|
252
|
-
function saveDefaultIsolation(mode) {
|
|
253
|
-
try {
|
|
254
|
-
localStorage.setItem(ISOLATION_KEY, mode);
|
|
255
|
-
} catch {}
|
|
256
|
-
}
|
|
257
269
|
/** Load the persisted view state (never throws; fresh on any parse error). */
|
|
258
270
|
function loadView() {
|
|
259
271
|
try {
|
|
@@ -294,7 +306,8 @@ window.__ModuleLoader__.load({
|
|
|
294
306
|
diagOpen: false,
|
|
295
307
|
templates: [],
|
|
296
308
|
tplManagerOpen: false,
|
|
297
|
-
importOpen: false
|
|
309
|
+
importOpen: false,
|
|
310
|
+
settingsOpen: false
|
|
298
311
|
};
|
|
299
312
|
}
|
|
300
313
|
/**
|
|
@@ -307,7 +320,11 @@ window.__ModuleLoader__.load({
|
|
|
307
320
|
disposed = false;
|
|
308
321
|
disposeStream;
|
|
309
322
|
refreshInFlight;
|
|
323
|
+
/** Newest change-frame revision seen on the SSE stream (S16 refresh chase). */
|
|
324
|
+
seenRevision;
|
|
310
325
|
sessionJumper;
|
|
326
|
+
/** Composer catalog faces, installed formally by the client entry (T13). */
|
|
327
|
+
catalogFaces = {};
|
|
311
328
|
/** @param client - the route client. */
|
|
312
329
|
constructor(client) {
|
|
313
330
|
this.client = client;
|
|
@@ -336,10 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
336
353
|
start() {
|
|
337
354
|
this.refresh();
|
|
338
355
|
this.disposeStream = this.client.stream((change) => {
|
|
339
|
-
this.
|
|
340
|
-
...this.state.ledger,
|
|
341
|
-
revision: change.revision
|
|
342
|
-
} });
|
|
356
|
+
this.seenRevision = change.revision;
|
|
343
357
|
this.refresh();
|
|
344
358
|
}, () => {
|
|
345
359
|
this.refresh();
|
|
@@ -350,15 +364,18 @@ window.__ModuleLoader__.load({
|
|
|
350
364
|
if (this.refreshInFlight !== void 0) return this.refreshInFlight;
|
|
351
365
|
this.refreshInFlight = (async () => {
|
|
352
366
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
379
|
} catch (error) {
|
|
363
380
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
364
381
|
} finally {
|
|
@@ -476,6 +493,22 @@ window.__ModuleLoader__.load({
|
|
|
476
493
|
installSessionJumper(jumper) {
|
|
477
494
|
this.sessionJumper = jumper;
|
|
478
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
|
+
}
|
|
479
512
|
/**
|
|
480
513
|
* Jump to an execution's session (open it in the GUI). On success the board
|
|
481
514
|
* closes so the conversation shows; a deleted-or-archived session reports
|
|
@@ -609,13 +642,18 @@ window.__ModuleLoader__.load({
|
|
|
609
642
|
return;
|
|
610
643
|
}
|
|
611
644
|
}
|
|
612
|
-
/**
|
|
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
|
+
*/
|
|
613
649
|
async comment(id, body) {
|
|
614
650
|
try {
|
|
615
651
|
await this.client.comment(id, body);
|
|
616
652
|
await this.refresh();
|
|
653
|
+
return true;
|
|
617
654
|
} catch (error) {
|
|
618
655
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
656
|
+
return false;
|
|
619
657
|
}
|
|
620
658
|
}
|
|
621
659
|
/** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
|
|
@@ -683,6 +721,29 @@ window.__ModuleLoader__.load({
|
|
|
683
721
|
closeDiagnostics() {
|
|
684
722
|
this.setState({ diagOpen: false });
|
|
685
723
|
}
|
|
724
|
+
/** Open the board-settings modal (0.5.0). */
|
|
725
|
+
openSettings() {
|
|
726
|
+
this.setState({ settingsOpen: true });
|
|
727
|
+
}
|
|
728
|
+
/** Close the board-settings modal. */
|
|
729
|
+
closeSettings() {
|
|
730
|
+
this.setState({ settingsOpen: false });
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Replace board settings (0.5.0). The host broadcasts a settings-updated
|
|
734
|
+
* frame; refresh() pulls ledger.settings so every open view follows.
|
|
735
|
+
* @returns whether the write succeeded.
|
|
736
|
+
*/
|
|
737
|
+
async updateSettings(body) {
|
|
738
|
+
try {
|
|
739
|
+
await this.client.updateSettings(body);
|
|
740
|
+
await this.refresh();
|
|
741
|
+
return true;
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
686
747
|
/** Clean one orphan worktree (⚙ panel); refreshes the diagnostics payload. */
|
|
687
748
|
async cleanupOrphan(workspaceId, taskId) {
|
|
688
749
|
try {
|
|
@@ -710,7 +771,7 @@ window.__ModuleLoader__.load({
|
|
|
710
771
|
async duplicate(task) {
|
|
711
772
|
try {
|
|
712
773
|
await this.client.create({
|
|
713
|
-
title: `${task.title}(副本)`,
|
|
774
|
+
title: `${task.title.slice(0, 196)}(副本)`,
|
|
714
775
|
workspaceId: task.workspaceId,
|
|
715
776
|
urgency: task.urgency,
|
|
716
777
|
description: task.description.length > 0 ? task.description : void 0,
|
|
@@ -835,7 +896,8 @@ window.__ModuleLoader__.load({
|
|
|
835
896
|
/** Download the task list as a CSV (BOM-prefixed for Excel + Chinese text). */
|
|
836
897
|
exportCsv() {
|
|
837
898
|
const esc = (v) => {
|
|
838
|
-
|
|
899
|
+
let s = String(v ?? "");
|
|
900
|
+
if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
839
901
|
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, "\"\"")}"` : s;
|
|
840
902
|
};
|
|
841
903
|
const header = [
|
|
@@ -1585,6 +1647,10 @@ window.__ModuleLoader__.load({
|
|
|
1585
1647
|
.dsh-atb-imp-row-status { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); flex-shrink: 0; }
|
|
1586
1648
|
.dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
|
|
1587
1649
|
.dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
|
|
1650
|
+
/* ---------- 0.5.0 board settings ---------- */
|
|
1651
|
+
.dsh-atb-set { max-width: 460px; width: min(460px, 92vw); }
|
|
1652
|
+
.dsh-atb-set .dsh-atb-mode-picker { margin-top: 8px; }
|
|
1653
|
+
.dsh-atb-set .dsh-atb-isolation-note { margin-top: 10px; }
|
|
1588
1654
|
`;
|
|
1589
1655
|
/** Style element id (stable since 0.1.x: hook for tests and debugging). */
|
|
1590
1656
|
const STYLE_ID = "dsh-taskboard-styles";
|
|
@@ -1794,7 +1860,8 @@ window.__ModuleLoader__.load({
|
|
|
1794
1860
|
found: false,
|
|
1795
1861
|
placed: false
|
|
1796
1862
|
};
|
|
1797
|
-
|
|
1863
|
+
const host = globalThis.location?.hostname;
|
|
1864
|
+
if (host === "localhost" || host === "127.0.0.1") window.__atbDebug = debug;
|
|
1798
1865
|
let root;
|
|
1799
1866
|
let placed = false;
|
|
1800
1867
|
const tryPlace = () => {
|
|
@@ -1865,7 +1932,70 @@ window.__ModuleLoader__.load({
|
|
|
1865
1932
|
* @module dsh-taskboard/shared/version
|
|
1866
1933
|
*/
|
|
1867
1934
|
/** The package version (must equal package.json "version"). */
|
|
1868
|
-
const PLUGIN_VERSION = "0.
|
|
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
|
+
}
|
|
1869
1999
|
|
|
1870
2000
|
//#endregion
|
|
1871
2001
|
//#region src/client/board/TaskCard.tsx
|
|
@@ -1882,17 +2012,6 @@ window.__ModuleLoader__.load({
|
|
|
1882
2012
|
*
|
|
1883
2013
|
* @module dsh-taskboard/client/board/TaskCard
|
|
1884
2014
|
*/
|
|
1885
|
-
const URGENCY_LABEL$1 = {
|
|
1886
|
-
urgent: "紧急",
|
|
1887
|
-
normal: "一般",
|
|
1888
|
-
relaxed: "不急"
|
|
1889
|
-
};
|
|
1890
|
-
const OUTCOME_LABEL$1 = {
|
|
1891
|
-
running: "执行中",
|
|
1892
|
-
succeeded: "成功",
|
|
1893
|
-
failed: "失败",
|
|
1894
|
-
cancelled: "已取消"
|
|
1895
|
-
};
|
|
1896
2015
|
/** dataTransfer type carrying the dragged task id. */
|
|
1897
2016
|
const DRAG_TYPE = "application/x-dsh-atb-task";
|
|
1898
2017
|
/**
|
|
@@ -1928,7 +2047,7 @@ window.__ModuleLoader__.load({
|
|
|
1928
2047
|
onDragStart: (e) => {
|
|
1929
2048
|
if (running !== void 0) {
|
|
1930
2049
|
e.preventDefault();
|
|
1931
|
-
const msg =
|
|
2050
|
+
const msg = `该任务正由会话执行中(${task.title}),不能拖动`;
|
|
1932
2051
|
if (onAlert !== void 0) onAlert(msg);
|
|
1933
2052
|
else alert(msg);
|
|
1934
2053
|
return;
|
|
@@ -1958,7 +2077,7 @@ window.__ModuleLoader__.load({
|
|
|
1958
2077
|
children: [
|
|
1959
2078
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1960
2079
|
className: "dsh-atb-badge",
|
|
1961
|
-
children: URGENCY_LABEL
|
|
2080
|
+
children: URGENCY_LABEL[task.urgency]
|
|
1962
2081
|
}),
|
|
1963
2082
|
task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1964
2083
|
className: "dsh-atb-badge",
|
|
@@ -1998,7 +2117,7 @@ window.__ModuleLoader__.load({
|
|
|
1998
2117
|
last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1999
2118
|
className: "dsh-atb-badge",
|
|
2000
2119
|
"data-kind": last.outcome === "running" ? "running" : last.outcome,
|
|
2001
|
-
children: OUTCOME_LABEL
|
|
2120
|
+
children: OUTCOME_LABEL[last.outcome] ?? last.outcome
|
|
2002
2121
|
}),
|
|
2003
2122
|
task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
|
|
2004
2123
|
task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2151,27 +2270,6 @@ window.__ModuleLoader__.load({
|
|
|
2151
2270
|
"archived"
|
|
2152
2271
|
].filter((to) => canTransition(task.status, to));
|
|
2153
2272
|
}
|
|
2154
|
-
const MOVE_LABEL = {
|
|
2155
|
-
backlog: "待规划",
|
|
2156
|
-
todo: "待办",
|
|
2157
|
-
in_progress: "进行中",
|
|
2158
|
-
in_review: "待验收",
|
|
2159
|
-
done: "完成",
|
|
2160
|
-
canceled: "取消",
|
|
2161
|
-
archived: "归档"
|
|
2162
|
-
};
|
|
2163
|
-
const STATUS_LABEL = { ...MOVE_LABEL };
|
|
2164
|
-
const URGENCY_LABEL = {
|
|
2165
|
-
urgent: "紧急",
|
|
2166
|
-
normal: "一般",
|
|
2167
|
-
relaxed: "不急"
|
|
2168
|
-
};
|
|
2169
|
-
const OUTCOME_LABEL = {
|
|
2170
|
-
running: "执行中",
|
|
2171
|
-
succeeded: "成功",
|
|
2172
|
-
failed: "失败",
|
|
2173
|
-
cancelled: "已取消"
|
|
2174
|
-
};
|
|
2175
2273
|
/** Compact session-id display (execution sessions carry the taskboard infix). */
|
|
2176
2274
|
function shortId(id) {
|
|
2177
2275
|
if (id === void 0) return "";
|
|
@@ -2640,6 +2738,7 @@ window.__ModuleLoader__.load({
|
|
|
2640
2738
|
const [confirmDone, setConfirmDone] = (0, react.useState)(false);
|
|
2641
2739
|
const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
|
|
2642
2740
|
const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
|
|
2741
|
+
const [actionBusy, setActionBusy] = (0, react.useState)(false);
|
|
2643
2742
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
2644
2743
|
const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
|
|
2645
2744
|
const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
|
|
@@ -2647,6 +2746,12 @@ window.__ModuleLoader__.load({
|
|
|
2647
2746
|
const holder = task.status === "in_progress" ? task.claimedBy : void 0;
|
|
2648
2747
|
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
2649
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
|
+
};
|
|
2650
2755
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
2651
2756
|
const jumpToSession = (sessionId) => {
|
|
2652
2757
|
controller.openSession(sessionId).then((result) => {
|
|
@@ -2746,7 +2851,7 @@ window.__ModuleLoader__.load({
|
|
|
2746
2851
|
"更新 ",
|
|
2747
2852
|
fmtTime(task.updatedAt),
|
|
2748
2853
|
" · 最近操作 ",
|
|
2749
|
-
task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : "👤 用户"
|
|
2854
|
+
task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : task.updatedBy.kind === "system" ? "⚙️ 系统" : "👤 用户"
|
|
2750
2855
|
]
|
|
2751
2856
|
})
|
|
2752
2857
|
]
|
|
@@ -2763,32 +2868,34 @@ window.__ModuleLoader__.load({
|
|
|
2763
2868
|
type: "button",
|
|
2764
2869
|
className: "dsh-atb-detail-edit",
|
|
2765
2870
|
title: "复制此任务的全部配置为一张新卡(待办列)",
|
|
2766
|
-
|
|
2871
|
+
disabled: actionBusy,
|
|
2872
|
+
onClick: () => runAction(() => controller.duplicate(task)),
|
|
2767
2873
|
children: "⧉ 复制"
|
|
2768
2874
|
}),
|
|
2769
2875
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2770
2876
|
type: "button",
|
|
2771
2877
|
className: "dsh-atb-detail-edit",
|
|
2772
2878
|
title: "把此任务的配置(含清单)保存为模板,新建任务时可用",
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
},
|
|
2879
|
+
disabled: actionBusy,
|
|
2880
|
+
onClick: () => runAction(async () => {
|
|
2881
|
+
if (await controller.saveAsTemplate(task)) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
|
|
2882
|
+
}),
|
|
2778
2883
|
children: "⌗ 存为模板"
|
|
2779
2884
|
}),
|
|
2780
2885
|
canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2781
2886
|
type: "button",
|
|
2782
2887
|
className: "dsh-atb-detail-run",
|
|
2783
2888
|
title: "续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线",
|
|
2784
|
-
|
|
2889
|
+
disabled: actionBusy,
|
|
2890
|
+
onClick: () => runAction(() => controller.run(task.id, true)),
|
|
2785
2891
|
children: "↻ 续跑"
|
|
2786
2892
|
}),
|
|
2787
2893
|
canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2788
2894
|
type: "button",
|
|
2789
2895
|
className: "dsh-atb-detail-run",
|
|
2790
2896
|
title: task.model !== void 0 ? `新会话执行(${task.model.model})` : "新会话执行(默认模型)",
|
|
2791
|
-
|
|
2897
|
+
disabled: actionBusy,
|
|
2898
|
+
onClick: () => runAction(() => controller.run(task.id)),
|
|
2792
2899
|
children: "▶ 立即执行"
|
|
2793
2900
|
}),
|
|
2794
2901
|
runningExecution !== void 0 && (confirmCancel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -2962,18 +3069,18 @@ window.__ModuleLoader__.load({
|
|
|
2962
3069
|
placeholder: "以用户身份留言(agent 开工前会读)…",
|
|
2963
3070
|
onChange: (e) => setComment(e.target.value),
|
|
2964
3071
|
onKeyDown: (e) => {
|
|
2965
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) {
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
}
|
|
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
|
+
});
|
|
2969
3075
|
}
|
|
2970
3076
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2971
3077
|
type: "button",
|
|
2972
3078
|
className: "dsh-atb-composer-send",
|
|
2973
3079
|
disabled: comment.trim().length === 0,
|
|
2974
3080
|
onClick: () => {
|
|
2975
|
-
controller.comment(task.id, comment)
|
|
2976
|
-
|
|
3081
|
+
controller.comment(task.id, comment).then((ok) => {
|
|
3082
|
+
if (ok) setComment("");
|
|
3083
|
+
});
|
|
2977
3084
|
},
|
|
2978
3085
|
children: "发表"
|
|
2979
3086
|
})]
|
|
@@ -3231,12 +3338,13 @@ window.__ModuleLoader__.load({
|
|
|
3231
3338
|
const [presetId, setPresetId] = (0, react.useState)(initialPreset);
|
|
3232
3339
|
const [presets, setPresets] = (0, react.useState)([]);
|
|
3233
3340
|
const [presetDefault, setPresetDefault] = (0, react.useState)(void 0);
|
|
3234
|
-
const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? (prefill?.isolation === "none" ? "none" : prefill?.isolation === "worktree" ? "worktree" :
|
|
3341
|
+
const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? (prefill?.isolation === "none" ? "none" : prefill?.isolation === "worktree" ? "worktree" : defaultIsolationOf(state.ledger.settings)));
|
|
3235
3342
|
const [checkRows, setCheckRows] = (0, react.useState)(task?.checklist !== void 0 && task.checklist.length > 0 ? task.checklist.map((i) => ({ ...i })) : (prefill?.checklist ?? []).map((text) => ({
|
|
3236
3343
|
text,
|
|
3237
3344
|
checked: false
|
|
3238
3345
|
})));
|
|
3239
3346
|
const titleRef = (0, react.useRef)(null);
|
|
3347
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3240
3348
|
(0, react.useEffect)(() => {
|
|
3241
3349
|
titleRef.current?.focus();
|
|
3242
3350
|
const onKey = (e) => {
|
|
@@ -3256,10 +3364,11 @@ window.__ModuleLoader__.load({
|
|
|
3256
3364
|
face().then((roster) => {
|
|
3257
3365
|
setPresets(roster.presets);
|
|
3258
3366
|
setPresetDefault(roster.defaultId);
|
|
3259
|
-
if (
|
|
3367
|
+
if (!editing && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
|
|
3260
3368
|
}).catch(() => setPresets([]));
|
|
3261
3369
|
}, [
|
|
3262
3370
|
controller,
|
|
3371
|
+
editing,
|
|
3263
3372
|
task?.presetId,
|
|
3264
3373
|
initialPreset
|
|
3265
3374
|
]);
|
|
@@ -3271,10 +3380,12 @@ window.__ModuleLoader__.load({
|
|
|
3271
3380
|
const isolationLocked = editing && ((task.executions?.length ?? 0) > 0 || task.status === "in_progress");
|
|
3272
3381
|
const gitOk = controller.gitAvailable(workspaceId);
|
|
3273
3382
|
const isolationDisabled = isolationLocked || !gitOk;
|
|
3274
|
-
/**
|
|
3383
|
+
/**
|
|
3384
|
+
* Isolation payload for submit: undefined lets the HOST materialize the
|
|
3385
|
+
* current board default at creation (non-git projects degrade naturally).
|
|
3386
|
+
*/
|
|
3275
3387
|
const isolationPayload = () => {
|
|
3276
3388
|
if (!gitOk) return void 0;
|
|
3277
|
-
if (!editing) saveDefaultIsolation(isolation);
|
|
3278
3389
|
return isolation;
|
|
3279
3390
|
};
|
|
3280
3391
|
/** Preset payload: '' = follow the deployment default (submit omits). */
|
|
@@ -3285,12 +3396,13 @@ window.__ModuleLoader__.load({
|
|
|
3285
3396
|
text: r.text.trim()
|
|
3286
3397
|
})).filter((r) => r.text.length > 0);
|
|
3287
3398
|
const submit = () => {
|
|
3288
|
-
if (!valid) return;
|
|
3399
|
+
if (!valid || busy) return;
|
|
3289
3400
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3290
3401
|
const isolationOut = isolationPayload();
|
|
3291
3402
|
const presetOut = presetPayload();
|
|
3292
3403
|
const rows = filledRows();
|
|
3293
|
-
|
|
3404
|
+
setBusy(true);
|
|
3405
|
+
(editing ? controller.update(task.id, task.version, {
|
|
3294
3406
|
title,
|
|
3295
3407
|
description,
|
|
3296
3408
|
prompt,
|
|
@@ -3304,8 +3416,7 @@ window.__ModuleLoader__.load({
|
|
|
3304
3416
|
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
3305
3417
|
presetId: presetOut ?? null,
|
|
3306
3418
|
checklist: rows.length > 0 ? rows : null
|
|
3307
|
-
})
|
|
3308
|
-
else controller.create({
|
|
3419
|
+
}) : controller.create({
|
|
3309
3420
|
title,
|
|
3310
3421
|
workspaceId,
|
|
3311
3422
|
urgency,
|
|
@@ -3319,50 +3430,52 @@ window.__ModuleLoader__.load({
|
|
|
3319
3430
|
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
3320
3431
|
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3321
3432
|
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
3322
|
-
});
|
|
3433
|
+
})).catch(() => void 0).finally(() => setBusy(false));
|
|
3323
3434
|
};
|
|
3324
3435
|
/** Save the form, then immediately trigger a manual run of the task. */
|
|
3325
3436
|
const submitAndRun = () => {
|
|
3326
|
-
if (!valid || runBlocked) return;
|
|
3437
|
+
if (!valid || runBlocked || busy) return;
|
|
3327
3438
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
3328
3439
|
const isolationOut = isolationPayload();
|
|
3329
3440
|
const presetOut = presetPayload();
|
|
3330
3441
|
const rows = filledRows();
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
mode
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
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));
|
|
3366
3479
|
};
|
|
3367
3480
|
const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
|
|
3368
3481
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -3628,8 +3741,8 @@ window.__ModuleLoader__.load({
|
|
|
3628
3741
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3629
3742
|
type: "button",
|
|
3630
3743
|
className: "dsh-atb-btn",
|
|
3631
|
-
disabled: !valid || runBlocked,
|
|
3632
|
-
title: runBlocked ? "任务正在执行中,不能重复发起" : "保存后立即发起执行(新会话)",
|
|
3744
|
+
disabled: !valid || runBlocked || busy,
|
|
3745
|
+
title: runBlocked ? "任务正在执行中,不能重复发起" : busy ? "正在提交…" : "保存后立即发起执行(新会话)",
|
|
3633
3746
|
onClick: submitAndRun,
|
|
3634
3747
|
children: "⚡ 立即执行"
|
|
3635
3748
|
}),
|
|
@@ -3637,7 +3750,7 @@ window.__ModuleLoader__.load({
|
|
|
3637
3750
|
type: "button",
|
|
3638
3751
|
className: "dsh-atb-btn",
|
|
3639
3752
|
"data-primary": "true",
|
|
3640
|
-
disabled: !valid,
|
|
3753
|
+
disabled: !valid || busy,
|
|
3641
3754
|
onClick: submit,
|
|
3642
3755
|
children: editing ? "保存修改" : "创建任务"
|
|
3643
3756
|
})
|
|
@@ -3649,6 +3762,133 @@ window.__ModuleLoader__.load({
|
|
|
3649
3762
|
});
|
|
3650
3763
|
}
|
|
3651
3764
|
|
|
3765
|
+
//#endregion
|
|
3766
|
+
//#region src/client/board/SettingsModal.tsx
|
|
3767
|
+
/**
|
|
3768
|
+
* Board-settings modal (0.5.0): the user-owned defaults applied when a NEW
|
|
3769
|
+
* task is created without an explicit choice. Currently one section — 默认执行
|
|
3770
|
+
* 隔离 (worktree vs original directory); further sections can slot into the
|
|
3771
|
+
* body below. Saving goes through the host route (whole-object replace) and
|
|
3772
|
+
* the SSE change stream refreshes every open view.
|
|
3773
|
+
*
|
|
3774
|
+
* @module dsh-taskboard/client/board/SettingsModal
|
|
3775
|
+
*/
|
|
3776
|
+
/** The isolation options with one-line hints (mirrors the task form). */
|
|
3777
|
+
const ISOLATION_OPTIONS = [{
|
|
3778
|
+
value: "none",
|
|
3779
|
+
name: "📁 原目录执行",
|
|
3780
|
+
hint: "不使用 git,直接在项目目录工作(出厂默认)"
|
|
3781
|
+
}, {
|
|
3782
|
+
value: "worktree",
|
|
3783
|
+
name: "🌿 Worktree 隔离",
|
|
3784
|
+
hint: "每次执行在独立 worktree 分支上进行(task/标题+ID),互不污染"
|
|
3785
|
+
}];
|
|
3786
|
+
/**
|
|
3787
|
+
* The 看板设置 modal: reads the live ledger settings, stages a local draft,
|
|
3788
|
+
* and writes back through the controller on save.
|
|
3789
|
+
* @param controller - the board controller.
|
|
3790
|
+
*/
|
|
3791
|
+
function SettingsModal({ controller }) {
|
|
3792
|
+
const current = controller.getSnapshot().ledger.settings?.defaultIsolation ?? "none";
|
|
3793
|
+
const [draft, setDraft] = (0, react.useState)(current);
|
|
3794
|
+
const dirty = draft !== current;
|
|
3795
|
+
const save = () => {
|
|
3796
|
+
controller.updateSettings({ defaultIsolation: draft }).then((ok) => {
|
|
3797
|
+
if (ok) controller.closeSettings();
|
|
3798
|
+
});
|
|
3799
|
+
};
|
|
3800
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3801
|
+
className: "dsh-atb-modal-backdrop",
|
|
3802
|
+
onClick: (e) => {
|
|
3803
|
+
if (e.target === e.currentTarget) controller.closeSettings();
|
|
3804
|
+
},
|
|
3805
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3806
|
+
className: "dsh-atb-modal dsh-atb-set",
|
|
3807
|
+
role: "dialog",
|
|
3808
|
+
"aria-modal": "true",
|
|
3809
|
+
"aria-label": "看板设置",
|
|
3810
|
+
children: [
|
|
3811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3812
|
+
className: "dsh-atb-modal-head",
|
|
3813
|
+
children: [
|
|
3814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3815
|
+
className: "dsh-atb-modal-headicon",
|
|
3816
|
+
children: "🛠"
|
|
3817
|
+
}),
|
|
3818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3819
|
+
className: "dsh-atb-modal-headtext",
|
|
3820
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "看板设置" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "新建任务时应用的默认值(不影响已有任务)" })]
|
|
3821
|
+
}),
|
|
3822
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3823
|
+
type: "button",
|
|
3824
|
+
className: "dsh-atb-modal-close",
|
|
3825
|
+
"aria-label": "关闭",
|
|
3826
|
+
onClick: () => controller.closeSettings(),
|
|
3827
|
+
children: "✕"
|
|
3828
|
+
})
|
|
3829
|
+
]
|
|
3830
|
+
}),
|
|
3831
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3832
|
+
className: "dsh-atb-modal-body",
|
|
3833
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3834
|
+
className: "dsh-atb-diag-sec",
|
|
3835
|
+
children: [
|
|
3836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "默认执行隔离" }),
|
|
3837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3838
|
+
className: "dsh-atb-mode-picker",
|
|
3839
|
+
children: ISOLATION_OPTIONS.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3840
|
+
type: "button",
|
|
3841
|
+
className: "dsh-atb-mode-opt",
|
|
3842
|
+
"data-on": draft === o.value,
|
|
3843
|
+
title: o.hint,
|
|
3844
|
+
onClick: () => setDraft(o.value),
|
|
3845
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3846
|
+
className: "dsh-atb-mode-name",
|
|
3847
|
+
children: o.name
|
|
3848
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3849
|
+
className: "dsh-atb-mode-hint",
|
|
3850
|
+
children: o.hint
|
|
3851
|
+
})]
|
|
3852
|
+
}, o.value))
|
|
3853
|
+
}),
|
|
3854
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3855
|
+
className: "dsh-atb-isolation-note",
|
|
3856
|
+
children: [
|
|
3857
|
+
"当前保存的默认:",
|
|
3858
|
+
current === "worktree" ? "🌿 Worktree 隔离" : "📁 原目录执行",
|
|
3859
|
+
"。 仅影响之后新建的任务;已有任务保持创建时的选择,非 git 项目运行时仍自动降级原目录。"
|
|
3860
|
+
]
|
|
3861
|
+
})
|
|
3862
|
+
]
|
|
3863
|
+
})
|
|
3864
|
+
}),
|
|
3865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3866
|
+
className: "dsh-atb-modal-foot",
|
|
3867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3868
|
+
className: "dsh-atb-modal-hint",
|
|
3869
|
+
children: dirty ? "有未保存的修改" : "与看板当前设置一致"
|
|
3870
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3871
|
+
className: "dsh-atb-modal-footbtns",
|
|
3872
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3873
|
+
type: "button",
|
|
3874
|
+
className: "dsh-atb-btn",
|
|
3875
|
+
onClick: () => controller.closeSettings(),
|
|
3876
|
+
children: "取消"
|
|
3877
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3878
|
+
type: "button",
|
|
3879
|
+
className: "dsh-atb-btn",
|
|
3880
|
+
"data-primary": "true",
|
|
3881
|
+
disabled: !dirty,
|
|
3882
|
+
onClick: save,
|
|
3883
|
+
children: "保存设置"
|
|
3884
|
+
})]
|
|
3885
|
+
})]
|
|
3886
|
+
})
|
|
3887
|
+
]
|
|
3888
|
+
})
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3652
3892
|
//#endregion
|
|
3653
3893
|
//#region src/client/board/ImportModal.tsx
|
|
3654
3894
|
/**
|
|
@@ -3892,7 +4132,7 @@ window.__ModuleLoader__.load({
|
|
|
3892
4132
|
className: "dsh-atb-btn",
|
|
3893
4133
|
"data-primary": "true",
|
|
3894
4134
|
"data-danger": mode === "replace" && confirmReplace ? "true" : void 0,
|
|
3895
|
-
disabled: plan === void 0 || busy
|
|
4135
|
+
disabled: plan === void 0 || busy,
|
|
3896
4136
|
onClick: commit,
|
|
3897
4137
|
children: mode === "replace" && confirmReplace ? "确认整册替换" : "执行导入"
|
|
3898
4138
|
})]
|
|
@@ -4082,35 +4322,6 @@ window.__ModuleLoader__.load({
|
|
|
4082
4322
|
*
|
|
4083
4323
|
* @module dsh-taskboard/client/board/TaskBoard
|
|
4084
4324
|
*/
|
|
4085
|
-
/** Column labels. */
|
|
4086
|
-
const COLUMN_LABELS = {
|
|
4087
|
-
backlog: "待规划",
|
|
4088
|
-
todo: "待办",
|
|
4089
|
-
in_progress: "进行中",
|
|
4090
|
-
in_review: "待验收",
|
|
4091
|
-
done: "已完成",
|
|
4092
|
-
canceled: "已取消",
|
|
4093
|
-
archived: "已归档"
|
|
4094
|
-
};
|
|
4095
|
-
/** Urgency chip labels. */
|
|
4096
|
-
const URGENCY_LABELS = {
|
|
4097
|
-
urgent: "紧急",
|
|
4098
|
-
normal: "一般",
|
|
4099
|
-
relaxed: "不急"
|
|
4100
|
-
};
|
|
4101
|
-
/** Format an epoch ms as a short local stamp. */
|
|
4102
|
-
function fmtTime(ms) {
|
|
4103
|
-
if (ms === void 0) return "";
|
|
4104
|
-
const d = new Date(ms);
|
|
4105
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
4106
|
-
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
4107
|
-
}
|
|
4108
|
-
/** A claim idle for longer than this is highlighted as stale (ms). */
|
|
4109
|
-
const STALE_CLAIM_MS = 30 * 6e4;
|
|
4110
|
-
/** Whether the task's claim is stale (in_progress, held, idle too long). */
|
|
4111
|
-
function isStaleClaim(task, now) {
|
|
4112
|
-
return task.status === "in_progress" && task.claimedAt !== void 0 && now - task.claimedAt > 18e5;
|
|
4113
|
-
}
|
|
4114
4325
|
/** Urgency sort rank (urgent first). */
|
|
4115
4326
|
const URGENCY_RANK = {
|
|
4116
4327
|
urgent: 0,
|
|
@@ -4144,6 +4355,8 @@ window.__ModuleLoader__.load({
|
|
|
4144
4355
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
4145
4356
|
const [newMenuOpen, setNewMenuOpen] = (0, react.useState)(false);
|
|
4146
4357
|
const closeMenu = () => setNewMenuOpen(false);
|
|
4358
|
+
const [exportOpen, setExportOpen] = (0, react.useState)(false);
|
|
4359
|
+
const closeExport = () => setExportOpen(false);
|
|
4147
4360
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4148
4361
|
className: "dsh-atb-board",
|
|
4149
4362
|
children: [
|
|
@@ -4189,7 +4402,7 @@ window.__ModuleLoader__.load({
|
|
|
4189
4402
|
},
|
|
4190
4403
|
children: "空白任务"
|
|
4191
4404
|
}),
|
|
4192
|
-
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4405
|
+
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4193
4406
|
type: "button",
|
|
4194
4407
|
className: "dsh-atb-newmenu-opt",
|
|
4195
4408
|
title: t.task.description !== void 0 && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name,
|
|
@@ -4197,7 +4410,7 @@ window.__ModuleLoader__.load({
|
|
|
4197
4410
|
closeMenu();
|
|
4198
4411
|
controller.newFromTemplate(t.task);
|
|
4199
4412
|
},
|
|
4200
|
-
children:
|
|
4413
|
+
children: t.name
|
|
4201
4414
|
}, t.id)),
|
|
4202
4415
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-newmenu-sep" }),
|
|
4203
4416
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -4269,7 +4482,7 @@ window.__ModuleLoader__.load({
|
|
|
4269
4482
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4270
4483
|
className: "dsh-atb-dot",
|
|
4271
4484
|
"data-urgency": u
|
|
4272
|
-
}),
|
|
4485
|
+
}), URGENCY_LABEL[u]]
|
|
4273
4486
|
}, u)),
|
|
4274
4487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4275
4488
|
type: "button",
|
|
@@ -4277,6 +4490,13 @@ window.__ModuleLoader__.load({
|
|
|
4277
4490
|
onClick: () => controller.toggleSecondary(),
|
|
4278
4491
|
children: state.secondaryOpen ? "返回看板" : "其它任务"
|
|
4279
4492
|
}),
|
|
4493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4494
|
+
type: "button",
|
|
4495
|
+
className: "dsh-atb-btn",
|
|
4496
|
+
title: "看板设置:新建任务的默认执行隔离等",
|
|
4497
|
+
onClick: () => controller.openSettings(),
|
|
4498
|
+
children: "🛠 设置"
|
|
4499
|
+
}),
|
|
4280
4500
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4281
4501
|
type: "button",
|
|
4282
4502
|
className: "dsh-atb-btn",
|
|
@@ -4291,19 +4511,39 @@ window.__ModuleLoader__.load({
|
|
|
4291
4511
|
onClick: () => controller.openImport(),
|
|
4292
4512
|
children: "⬆ 导入"
|
|
4293
4513
|
}),
|
|
4294
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4515
|
+
className: "dsh-atb-newmenu",
|
|
4516
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4517
|
+
type: "button",
|
|
4518
|
+
className: "dsh-atb-btn",
|
|
4519
|
+
title: "导出台账:完整 JSON 备份或任务清单 CSV",
|
|
4520
|
+
onClick: () => setExportOpen(!exportOpen),
|
|
4521
|
+
children: "⬇ 导出 ▼"
|
|
4522
|
+
}), exportOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4523
|
+
className: "dsh-atb-newmenu-backdrop",
|
|
4524
|
+
onClick: closeExport
|
|
4525
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4526
|
+
className: "dsh-atb-newmenu-list",
|
|
4527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4528
|
+
type: "button",
|
|
4529
|
+
className: "dsh-atb-newmenu-opt",
|
|
4530
|
+
title: "完整台账备份(含执行历史与看板设置),可用于导入恢复",
|
|
4531
|
+
onClick: () => {
|
|
4532
|
+
closeExport();
|
|
4533
|
+
controller.exportJson();
|
|
4534
|
+
},
|
|
4535
|
+
children: "完整台账(JSON)"
|
|
4536
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4537
|
+
type: "button",
|
|
4538
|
+
className: "dsh-atb-newmenu-opt",
|
|
4539
|
+
title: "任务清单表格(Excel 可直接打开,中文已加 BOM)",
|
|
4540
|
+
onClick: () => {
|
|
4541
|
+
closeExport();
|
|
4542
|
+
controller.exportCsv();
|
|
4543
|
+
},
|
|
4544
|
+
children: "任务清单(CSV)"
|
|
4545
|
+
})]
|
|
4546
|
+
})] })]
|
|
4307
4547
|
}),
|
|
4308
4548
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
4309
4549
|
className: "dsh-atb-ver",
|
|
@@ -4385,13 +4625,14 @@ window.__ModuleLoader__.load({
|
|
|
4385
4625
|
task: selected,
|
|
4386
4626
|
controller,
|
|
4387
4627
|
now
|
|
4388
|
-
})
|
|
4628
|
+
}, selected.id)
|
|
4389
4629
|
}),
|
|
4390
4630
|
state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
|
|
4391
4631
|
controller,
|
|
4392
4632
|
task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
|
|
4393
4633
|
}),
|
|
4394
4634
|
state.diagOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsPanel, { controller }),
|
|
4635
|
+
state.settingsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsModal, { controller }),
|
|
4395
4636
|
state.importOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportModal, { controller }),
|
|
4396
4637
|
state.tplManagerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateManager, { controller }),
|
|
4397
4638
|
alertEl
|
|
@@ -4734,7 +4975,7 @@ window.__ModuleLoader__.load({
|
|
|
4734
4975
|
const controller = new BoardController(createClient());
|
|
4735
4976
|
const connection = ctx.get?.("connection");
|
|
4736
4977
|
if (connection !== void 0) {
|
|
4737
|
-
controller.
|
|
4978
|
+
controller.installModelCatalog(async () => {
|
|
4738
4979
|
const response = await connection.api.llm.models({});
|
|
4739
4980
|
if (!response.result.ok) return [];
|
|
4740
4981
|
const out = [];
|
|
@@ -4744,8 +4985,8 @@ window.__ModuleLoader__.load({
|
|
|
4744
4985
|
name: model.name
|
|
4745
4986
|
});
|
|
4746
4987
|
return out;
|
|
4747
|
-
};
|
|
4748
|
-
controller.
|
|
4988
|
+
});
|
|
4989
|
+
controller.installPresetRoster(async () => {
|
|
4749
4990
|
const list = connection.api.agentPresets;
|
|
4750
4991
|
if (list === void 0) return { presets: [] };
|
|
4751
4992
|
const response = await list.list({});
|
|
@@ -4759,7 +5000,7 @@ window.__ModuleLoader__.load({
|
|
|
4759
5000
|
presets,
|
|
4760
5001
|
...def !== void 0 ? { defaultId: def.id } : {}
|
|
4761
5002
|
};
|
|
4762
|
-
};
|
|
5003
|
+
});
|
|
4763
5004
|
}
|
|
4764
5005
|
controller.installSessionJumper(createSessionJumper({
|
|
4765
5006
|
getSessions: () => ctx.get?.("sessions"),
|