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.
Files changed (48) hide show
  1. package/README.md +15 -0
  2. package/lib/client.js +219 -161
  3. package/lib/host/execution.js +80 -33
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +49 -5
  6. package/lib/host/git.js.map +1 -1
  7. package/lib/host/routes.js +180 -109
  8. package/lib/host/routes.js.map +1 -1
  9. package/lib/host/scheduler.js +50 -28
  10. package/lib/host/scheduler.js.map +1 -1
  11. package/lib/host/sdk.js +7 -2
  12. package/lib/host/sdk.js.map +1 -1
  13. package/lib/host/store.js +41 -8
  14. package/lib/host/store.js.map +1 -1
  15. package/lib/host/templates.js +10 -3
  16. package/lib/host/templates.js.map +1 -1
  17. package/lib/host/tools.js +124 -93
  18. package/lib/host/tools.js.map +1 -1
  19. package/lib/index.js +3 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/shared/api.js.map +1 -1
  22. package/lib/shared/protocol.js +23 -2
  23. package/lib/shared/protocol.js.map +1 -1
  24. package/package.json +3 -2
  25. package/src/client/api.ts +19 -9
  26. package/src/client/board/ImportModal.tsx +1 -1
  27. package/src/client/board/TaskBoard.tsx +7 -38
  28. package/src/client/board/TaskCard.tsx +3 -5
  29. package/src/client/board/TaskDetail.tsx +30 -21
  30. package/src/client/board/TaskFormModal.tsx +30 -23
  31. package/src/client/board/format.ts +26 -0
  32. package/src/client/board/labels.ts +44 -0
  33. package/src/client/controller.ts +60 -13
  34. package/src/client/index.ts +7 -5
  35. package/src/client/sidebar-entry.ts +5 -1
  36. package/src/host/execution.ts +90 -16
  37. package/src/host/git.ts +39 -10
  38. package/src/host/routes.ts +227 -126
  39. package/src/host/scheduler.ts +62 -36
  40. package/src/host/sdk.ts +12 -1
  41. package/src/host/store.ts +53 -7
  42. package/src/host/templates.ts +12 -3
  43. package/src/host/tools.ts +180 -123
  44. package/src/index.ts +10 -1
  45. package/src/shared/api.ts +1 -1
  46. package/src/shared/protocol.ts +35 -1
  47. package/src/shared/version.ts +1 -1
  48. 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: () => unwrap(fetch("/dsh-taskboard/state")),
32
- workspaces: () => unwrap(fetch("/dsh-taskboard/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) => unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(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: () => unwrap(fetch("/dsh-taskboard/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 unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`));
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: () => unwrap(fetch("/dsh-taskboard/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: () => unwrap(fetch("/dsh-taskboard/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
- const payload = JSON.parse(event.data);
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
- const payload = JSON.parse(event.data);
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.setState({ ledger: {
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
- const [ledger, workspaces] = await Promise.all([this.client.state(), this.client.workspaces()]);
351
- let selected;
352
- if (this.state.selectedId !== void 0) selected = ledger.tasks.find((t) => t.id === this.state.selectedId);
353
- this.setState({
354
- ledger,
355
- workspaces,
356
- error: void 0,
357
- selectedId: selected === void 0 ? void 0 : this.state.selectedId
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
- /** Append a user comment. */
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
- const s = String(v ?? "");
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
- window.__atbDebug = debug;
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.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
+ }
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 = `该任务正在由【${task.title}】会话执行,不能拖动`;
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$1[task.urgency]
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$1[last.outcome] ?? last.outcome
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
- onClick: () => void controller.duplicate(task),
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
- onClick: () => {
2798
- controller.saveAsTemplate(task).then((ok) => {
2799
- if (ok) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
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
- onClick: () => void controller.run(task.id, true),
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
- onClick: () => void controller.run(task.id),
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
- controller.comment(task.id, comment);
2991
- setComment("");
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
- setComment("");
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 (task?.presetId === void 0 && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
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
- if (editing) controller.update(task.id, task.version, {
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
- if (editing) (async () => {
3358
- if (await controller.update(task.id, task.version, {
3359
- title,
3360
- description,
3361
- prompt,
3362
- urgency,
3363
- workspaceId,
3364
- execution: mode === "scheduled" ? {
3365
- mode,
3366
- cron: cron.trim()
3367
- } : { mode },
3368
- model: picked ?? null,
3369
- ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
3370
- presetId: presetOut ?? null,
3371
- checklist: rows.length > 0 ? rows : null
3372
- })) await controller.run(task.id);
3373
- })();
3374
- else (async () => {
3375
- const id = await controller.create({
3376
- title,
3377
- workspaceId,
3378
- urgency,
3379
- description: description.length > 0 ? description : void 0,
3380
- prompt: prompt.length > 0 ? prompt : void 0,
3381
- execution: mode === "scheduled" ? {
3382
- mode,
3383
- cron: cron.trim()
3384
- } : { mode },
3385
- model: picked,
3386
- ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
3387
- ...presetOut !== void 0 ? { presetId: presetOut } : {},
3388
- ...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
3389
- });
3390
- if (id !== void 0) await controller.run(id);
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 || result !== void 0 && false,
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.jsxs)("button", {
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: [t.name, t.builtin === true ? "" : ""]
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
- }), URGENCY_LABELS[u]]
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.modelCatalog = async () => {
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.presetCatalog = async () => {
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"),