dsh-taskboard 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +25 -1
  2. package/lib/client.js +242 -174
  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/board-mount.tsx +9 -6
  34. package/src/client/controller.ts +60 -13
  35. package/src/client/index.ts +7 -5
  36. package/src/client/sidebar-entry.ts +16 -5
  37. package/src/client/styles.ts +5 -3
  38. package/src/host/execution.ts +90 -16
  39. package/src/host/git.ts +39 -10
  40. package/src/host/routes.ts +227 -126
  41. package/src/host/scheduler.ts +62 -36
  42. package/src/host/sdk.ts +12 -1
  43. package/src/host/store.ts +53 -7
  44. package/src/host/templates.ts +12 -3
  45. package/src/host/tools.ts +180 -123
  46. package/src/index.ts +10 -1
  47. package/src/shared/api.ts +1 -1
  48. package/src/shared/protocol.ts +35 -1
  49. package/src/shared/version.ts +1 -1
  50. 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 = [
@@ -985,10 +1027,12 @@ window.__ModuleLoader__.load({
985
1027
  .dsh-atb-search { width: 130px; }
986
1028
  .dsh-atb-badge[data-kind="stale"] { background: rgba(217,130,43,.15); color: #d9822b; }
987
1029
 
988
- /* 0.4.2: dual column matching — dev shell's data-pane pane OR the Desktop
989
- * shell's CSS-Module hashed centerCol (see board-mount.tsx). */
1030
+ /* Triple-generation column matching — dev shell's data-pane pane, the
1031
+ * official layout shell's CSS-Module hashed centerCol (0.4.2), or DSH
1032
+ * Desktop's non-compat extended frame surface (0.5.2, see board-mount.tsx). */
990
1033
  html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]),
991
- html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view]) { display: none !important; }
1034
+ html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view]),
1035
+ html[data-dsh-atb-active] .dshDesktopConversationSurface > *:not([data-dsh-atb-view]) { display: none !important; }
992
1036
  .dsh-atb-view { display: none; }
993
1037
  html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
994
1038
 
@@ -1656,14 +1700,19 @@ window.__ModuleLoader__.load({
1656
1700
  //#region src/client/sidebar-entry.ts
1657
1701
  /** Stable data attribute identifying this entry row. */
1658
1702
  const ENTRY_SELECTOR = "[data-dsh-atb-entry]";
1659
- /** Inline icon (16px nav-icon look). */
1660
- const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M2 6.5h12M6.5 6.5v7\"/></svg>";
1703
+ /** Inline icon: a three-lane kanban board (16px nav-icon look). */
1704
+ const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M6 2v12M10 2v12\"/></svg>";
1661
1705
  /**
1662
1706
  * Find the sidebar shell root element, or undefined while not yet mounted.
1663
- * (Same as the working family plugins: sidebarCol pane → logoRow owner.)
1707
+ * Triple-generation matching (0.5.2): the dev shell's data-pane pane, the
1708
+ * official layout's CSS-Module sidebarCol, and DSH Desktop's non-compat
1709
+ * (extended) frame — which disables the official ui-layout row and owns
1710
+ * the columns itself (aside.dshDesktopSidebarSurface >
1711
+ * div.dshDesktopUpstreamSidebar wrapping the unchanged official sidebar).
1712
+ * logoRow-owner resolution is identical on all three generations.
1664
1713
  */
1665
1714
  function sidebarRoot() {
1666
- const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
1715
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface");
1667
1716
  if (column === null) return void 0;
1668
1717
  return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
1669
1718
  }
@@ -1818,7 +1867,8 @@ window.__ModuleLoader__.load({
1818
1867
  found: false,
1819
1868
  placed: false
1820
1869
  };
1821
- window.__atbDebug = debug;
1870
+ const host = globalThis.location?.hostname;
1871
+ if (host === "localhost" || host === "127.0.0.1") window.__atbDebug = debug;
1822
1872
  let root;
1823
1873
  let placed = false;
1824
1874
  const tryPlace = () => {
@@ -1889,7 +1939,70 @@ window.__ModuleLoader__.load({
1889
1939
  * @module dsh-taskboard/shared/version
1890
1940
  */
1891
1941
  /** The package version (must equal package.json "version"). */
1892
- const PLUGIN_VERSION = "0.5.0";
1942
+ const PLUGIN_VERSION = "0.5.2";
1943
+
1944
+ //#endregion
1945
+ //#region src/client/board/labels.ts
1946
+ /** Column headers on the five-column main board (+ secondary tab). */
1947
+ const COLUMN_LABELS = {
1948
+ backlog: "待规划",
1949
+ todo: "待办",
1950
+ in_progress: "进行中",
1951
+ in_review: "待验收",
1952
+ done: "已完成",
1953
+ canceled: "已取消",
1954
+ archived: "已归档"
1955
+ };
1956
+ /** Status pill text (detail pane) — historical wording kept verbatim:
1957
+ * terminal states read short here, the column headers carry the full forms. */
1958
+ const STATUS_LABEL = {
1959
+ backlog: "待规划",
1960
+ todo: "待办",
1961
+ in_progress: "进行中",
1962
+ in_review: "待验收",
1963
+ done: "完成",
1964
+ canceled: "取消",
1965
+ archived: "归档"
1966
+ };
1967
+ /** Move-button verbs (shorter than the pill text). */
1968
+ const MOVE_LABEL = {
1969
+ backlog: "待规划",
1970
+ todo: "待办",
1971
+ in_progress: "进行中",
1972
+ in_review: "待验收",
1973
+ done: "完成",
1974
+ canceled: "取消",
1975
+ archived: "归档"
1976
+ };
1977
+ /** Urgency chip labels. */
1978
+ const URGENCY_LABEL = {
1979
+ urgent: "紧急",
1980
+ normal: "一般",
1981
+ relaxed: "不急"
1982
+ };
1983
+ /** Execution outcome labels. */
1984
+ const OUTCOME_LABEL = {
1985
+ running: "执行中",
1986
+ succeeded: "成功",
1987
+ failed: "失败",
1988
+ cancelled: "已取消"
1989
+ };
1990
+
1991
+ //#endregion
1992
+ //#region src/client/board/format.ts
1993
+ /** Format an epoch ms as a short local stamp. */
1994
+ function fmtTime(ms) {
1995
+ if (ms === void 0) return "";
1996
+ const d = new Date(ms);
1997
+ const pad = (n) => String(n).padStart(2, "0");
1998
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1999
+ }
2000
+ /** A claim idle for longer than this is highlighted as stale (ms). */
2001
+ const STALE_CLAIM_MS = 30 * 6e4;
2002
+ /** Whether the task's claim is stale (in_progress, held, idle too long). */
2003
+ function isStaleClaim(task, now) {
2004
+ return task.status === "in_progress" && task.claimedAt !== void 0 && now - task.claimedAt > 18e5;
2005
+ }
1893
2006
 
1894
2007
  //#endregion
1895
2008
  //#region src/client/board/TaskCard.tsx
@@ -1906,17 +2019,6 @@ window.__ModuleLoader__.load({
1906
2019
  *
1907
2020
  * @module dsh-taskboard/client/board/TaskCard
1908
2021
  */
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
2022
  /** dataTransfer type carrying the dragged task id. */
1921
2023
  const DRAG_TYPE = "application/x-dsh-atb-task";
1922
2024
  /**
@@ -1952,7 +2054,7 @@ window.__ModuleLoader__.load({
1952
2054
  onDragStart: (e) => {
1953
2055
  if (running !== void 0) {
1954
2056
  e.preventDefault();
1955
- const msg = `该任务正在由【${task.title}】会话执行,不能拖动`;
2057
+ const msg = `该任务正由会话执行中(${task.title}),不能拖动`;
1956
2058
  if (onAlert !== void 0) onAlert(msg);
1957
2059
  else alert(msg);
1958
2060
  return;
@@ -1982,7 +2084,7 @@ window.__ModuleLoader__.load({
1982
2084
  children: [
1983
2085
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1984
2086
  className: "dsh-atb-badge",
1985
- children: URGENCY_LABEL$1[task.urgency]
2087
+ children: URGENCY_LABEL[task.urgency]
1986
2088
  }),
1987
2089
  task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1988
2090
  className: "dsh-atb-badge",
@@ -2022,7 +2124,7 @@ window.__ModuleLoader__.load({
2022
2124
  last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2023
2125
  className: "dsh-atb-badge",
2024
2126
  "data-kind": last.outcome === "running" ? "running" : last.outcome,
2025
- children: OUTCOME_LABEL$1[last.outcome] ?? last.outcome
2127
+ children: OUTCOME_LABEL[last.outcome] ?? last.outcome
2026
2128
  }),
2027
2129
  task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
2028
2130
  task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2175,27 +2277,6 @@ window.__ModuleLoader__.load({
2175
2277
  "archived"
2176
2278
  ].filter((to) => canTransition(task.status, to));
2177
2279
  }
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
2280
  /** Compact session-id display (execution sessions carry the taskboard infix). */
2200
2281
  function shortId(id) {
2201
2282
  if (id === void 0) return "";
@@ -2664,6 +2745,7 @@ window.__ModuleLoader__.load({
2664
2745
  const [confirmDone, setConfirmDone] = (0, react.useState)(false);
2665
2746
  const [confirmPurge, setConfirmPurge] = (0, react.useState)(false);
2666
2747
  const [confirmCancel, setConfirmCancel] = (0, react.useState)(false);
2748
+ const [actionBusy, setActionBusy] = (0, react.useState)(false);
2667
2749
  const { alert: showAlert, el: alertEl } = useAlert();
2668
2750
  const ws = controller.getSnapshot().workspaces.find((w) => w.id === task.workspaceId);
2669
2751
  const canRun = task.status !== "in_progress" && task.status !== "done" && task.status !== "archived";
@@ -2671,6 +2753,12 @@ window.__ModuleLoader__.load({
2671
2753
  const holder = task.status === "in_progress" ? task.claimedBy : void 0;
2672
2754
  const stale = now !== void 0 && isStaleClaim(task, now);
2673
2755
  const unchecked = (task.checklist ?? []).filter((i) => !i.checked).length;
2756
+ /** Fire one top action under the shared busy guard; re-enable on settle. */
2757
+ const runAction = (action) => {
2758
+ if (actionBusy) return;
2759
+ setActionBusy(true);
2760
+ action().catch(() => void 0).finally(() => setActionBusy(false));
2761
+ };
2674
2762
  /** Jump to an execution's session; prompt precisely when it cannot open. */
2675
2763
  const jumpToSession = (sessionId) => {
2676
2764
  controller.openSession(sessionId).then((result) => {
@@ -2770,7 +2858,7 @@ window.__ModuleLoader__.load({
2770
2858
  "更新 ",
2771
2859
  fmtTime(task.updatedAt),
2772
2860
  " · 最近操作 ",
2773
- task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : "👤 用户"
2861
+ task.updatedBy.kind === "agent" ? `🤖 ${shortId(task.updatedBy.sessionId)}` : task.updatedBy.kind === "system" ? "⚙️ 系统" : "👤 用户"
2774
2862
  ]
2775
2863
  })
2776
2864
  ]
@@ -2787,32 +2875,34 @@ window.__ModuleLoader__.load({
2787
2875
  type: "button",
2788
2876
  className: "dsh-atb-detail-edit",
2789
2877
  title: "复制此任务的全部配置为一张新卡(待办列)",
2790
- onClick: () => void controller.duplicate(task),
2878
+ disabled: actionBusy,
2879
+ onClick: () => runAction(() => controller.duplicate(task)),
2791
2880
  children: "⧉ 复制"
2792
2881
  }),
2793
2882
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2794
2883
  type: "button",
2795
2884
  className: "dsh-atb-detail-edit",
2796
2885
  title: "把此任务的配置(含清单)保存为模板,新建任务时可用",
2797
- onClick: () => {
2798
- controller.saveAsTemplate(task).then((ok) => {
2799
- if (ok) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
2800
- });
2801
- },
2886
+ disabled: actionBusy,
2887
+ onClick: () => runAction(async () => {
2888
+ if (await controller.saveAsTemplate(task)) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
2889
+ }),
2802
2890
  children: "⌗ 存为模板"
2803
2891
  }),
2804
2892
  canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2805
2893
  type: "button",
2806
2894
  className: "dsh-atb-detail-run",
2807
2895
  title: "续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线",
2808
- onClick: () => void controller.run(task.id, true),
2896
+ disabled: actionBusy,
2897
+ onClick: () => runAction(() => controller.run(task.id, true)),
2809
2898
  children: "↻ 续跑"
2810
2899
  }),
2811
2900
  canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2812
2901
  type: "button",
2813
2902
  className: "dsh-atb-detail-run",
2814
2903
  title: task.model !== void 0 ? `新会话执行(${task.model.model})` : "新会话执行(默认模型)",
2815
- onClick: () => void controller.run(task.id),
2904
+ disabled: actionBusy,
2905
+ onClick: () => runAction(() => controller.run(task.id)),
2816
2906
  children: "▶ 立即执行"
2817
2907
  }),
2818
2908
  runningExecution !== void 0 && (confirmCancel ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -2986,18 +3076,18 @@ window.__ModuleLoader__.load({
2986
3076
  placeholder: "以用户身份留言(agent 开工前会读)…",
2987
3077
  onChange: (e) => setComment(e.target.value),
2988
3078
  onKeyDown: (e) => {
2989
- if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) {
2990
- controller.comment(task.id, comment);
2991
- setComment("");
2992
- }
3079
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && comment.trim().length > 0) controller.comment(task.id, comment).then((ok) => {
3080
+ if (ok) setComment("");
3081
+ });
2993
3082
  }
2994
3083
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2995
3084
  type: "button",
2996
3085
  className: "dsh-atb-composer-send",
2997
3086
  disabled: comment.trim().length === 0,
2998
3087
  onClick: () => {
2999
- controller.comment(task.id, comment);
3000
- setComment("");
3088
+ controller.comment(task.id, comment).then((ok) => {
3089
+ if (ok) setComment("");
3090
+ });
3001
3091
  },
3002
3092
  children: "发表"
3003
3093
  })]
@@ -3261,6 +3351,7 @@ window.__ModuleLoader__.load({
3261
3351
  checked: false
3262
3352
  })));
3263
3353
  const titleRef = (0, react.useRef)(null);
3354
+ const [busy, setBusy] = (0, react.useState)(false);
3264
3355
  (0, react.useEffect)(() => {
3265
3356
  titleRef.current?.focus();
3266
3357
  const onKey = (e) => {
@@ -3280,10 +3371,11 @@ window.__ModuleLoader__.load({
3280
3371
  face().then((roster) => {
3281
3372
  setPresets(roster.presets);
3282
3373
  setPresetDefault(roster.defaultId);
3283
- if (task?.presetId === void 0 && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
3374
+ if (!editing && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
3284
3375
  }).catch(() => setPresets([]));
3285
3376
  }, [
3286
3377
  controller,
3378
+ editing,
3287
3379
  task?.presetId,
3288
3380
  initialPreset
3289
3381
  ]);
@@ -3311,12 +3403,13 @@ window.__ModuleLoader__.load({
3311
3403
  text: r.text.trim()
3312
3404
  })).filter((r) => r.text.length > 0);
3313
3405
  const submit = () => {
3314
- if (!valid) return;
3406
+ if (!valid || busy) return;
3315
3407
  const picked = model !== "" ? JSON.parse(model) : void 0;
3316
3408
  const isolationOut = isolationPayload();
3317
3409
  const presetOut = presetPayload();
3318
3410
  const rows = filledRows();
3319
- if (editing) controller.update(task.id, task.version, {
3411
+ setBusy(true);
3412
+ (editing ? controller.update(task.id, task.version, {
3320
3413
  title,
3321
3414
  description,
3322
3415
  prompt,
@@ -3330,8 +3423,7 @@ window.__ModuleLoader__.load({
3330
3423
  ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
3331
3424
  presetId: presetOut ?? null,
3332
3425
  checklist: rows.length > 0 ? rows : null
3333
- });
3334
- else controller.create({
3426
+ }) : controller.create({
3335
3427
  title,
3336
3428
  workspaceId,
3337
3429
  urgency,
@@ -3345,50 +3437,52 @@ window.__ModuleLoader__.load({
3345
3437
  ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
3346
3438
  ...presetOut !== void 0 ? { presetId: presetOut } : {},
3347
3439
  ...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
3348
- });
3440
+ })).catch(() => void 0).finally(() => setBusy(false));
3349
3441
  };
3350
3442
  /** Save the form, then immediately trigger a manual run of the task. */
3351
3443
  const submitAndRun = () => {
3352
- if (!valid || runBlocked) return;
3444
+ if (!valid || runBlocked || busy) return;
3353
3445
  const picked = model !== "" ? JSON.parse(model) : void 0;
3354
3446
  const isolationOut = isolationPayload();
3355
3447
  const presetOut = presetPayload();
3356
3448
  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
- })();
3449
+ setBusy(true);
3450
+ (async () => {
3451
+ if (editing) {
3452
+ if (await controller.update(task.id, task.version, {
3453
+ title,
3454
+ description,
3455
+ prompt,
3456
+ urgency,
3457
+ workspaceId,
3458
+ execution: mode === "scheduled" ? {
3459
+ mode,
3460
+ cron: cron.trim()
3461
+ } : { mode },
3462
+ model: picked ?? null,
3463
+ ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
3464
+ presetId: presetOut ?? null,
3465
+ checklist: rows.length > 0 ? rows : null
3466
+ })) await controller.run(task.id);
3467
+ } else {
3468
+ const id = await controller.create({
3469
+ title,
3470
+ workspaceId,
3471
+ urgency,
3472
+ description: description.length > 0 ? description : void 0,
3473
+ prompt: prompt.length > 0 ? prompt : void 0,
3474
+ execution: mode === "scheduled" ? {
3475
+ mode,
3476
+ cron: cron.trim()
3477
+ } : { mode },
3478
+ model: picked,
3479
+ ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
3480
+ ...presetOut !== void 0 ? { presetId: presetOut } : {},
3481
+ ...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
3482
+ });
3483
+ if (id !== void 0) await controller.run(id);
3484
+ }
3485
+ })().catch(() => void 0).finally(() => setBusy(false));
3392
3486
  };
3393
3487
  const hint = !valid ? title.trim().length === 0 ? "请填写标题" : workspaceId === "" ? "请选择项目" : "Cron 表达式无效(分 时 日 月 周)" : mode === "scheduled" && nextRun !== null ? `下次运行 ${fmtTime(nextRun)}` : editing ? `保存后版本 v${task.version} → v${task.version + 1}` : "创建后项目内会话可认领执行";
3394
3488
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3654,8 +3748,8 @@ window.__ModuleLoader__.load({
3654
3748
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3655
3749
  type: "button",
3656
3750
  className: "dsh-atb-btn",
3657
- disabled: !valid || runBlocked,
3658
- title: runBlocked ? "任务正在执行中,不能重复发起" : "保存后立即发起执行(新会话)",
3751
+ disabled: !valid || runBlocked || busy,
3752
+ title: runBlocked ? "任务正在执行中,不能重复发起" : busy ? "正在提交…" : "保存后立即发起执行(新会话)",
3659
3753
  onClick: submitAndRun,
3660
3754
  children: "⚡ 立即执行"
3661
3755
  }),
@@ -3663,7 +3757,7 @@ window.__ModuleLoader__.load({
3663
3757
  type: "button",
3664
3758
  className: "dsh-atb-btn",
3665
3759
  "data-primary": "true",
3666
- disabled: !valid,
3760
+ disabled: !valid || busy,
3667
3761
  onClick: submit,
3668
3762
  children: editing ? "保存修改" : "创建任务"
3669
3763
  })
@@ -4045,7 +4139,7 @@ window.__ModuleLoader__.load({
4045
4139
  className: "dsh-atb-btn",
4046
4140
  "data-primary": "true",
4047
4141
  "data-danger": mode === "replace" && confirmReplace ? "true" : void 0,
4048
- disabled: plan === void 0 || busy || result !== void 0 && false,
4142
+ disabled: plan === void 0 || busy,
4049
4143
  onClick: commit,
4050
4144
  children: mode === "replace" && confirmReplace ? "确认整册替换" : "执行导入"
4051
4145
  })]
@@ -4235,35 +4329,6 @@ window.__ModuleLoader__.load({
4235
4329
  *
4236
4330
  * @module dsh-taskboard/client/board/TaskBoard
4237
4331
  */
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
4332
  /** Urgency sort rank (urgent first). */
4268
4333
  const URGENCY_RANK = {
4269
4334
  urgent: 0,
@@ -4344,7 +4409,7 @@ window.__ModuleLoader__.load({
4344
4409
  },
4345
4410
  children: "空白任务"
4346
4411
  }),
4347
- state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4412
+ state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4348
4413
  type: "button",
4349
4414
  className: "dsh-atb-newmenu-opt",
4350
4415
  title: t.task.description !== void 0 && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name,
@@ -4352,7 +4417,7 @@ window.__ModuleLoader__.load({
4352
4417
  closeMenu();
4353
4418
  controller.newFromTemplate(t.task);
4354
4419
  },
4355
- children: [t.name, t.builtin === true ? "" : ""]
4420
+ children: t.name
4356
4421
  }, t.id)),
4357
4422
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-newmenu-sep" }),
4358
4423
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -4424,7 +4489,7 @@ window.__ModuleLoader__.load({
4424
4489
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4425
4490
  className: "dsh-atb-dot",
4426
4491
  "data-urgency": u
4427
- }), URGENCY_LABELS[u]]
4492
+ }), URGENCY_LABEL[u]]
4428
4493
  }, u)),
4429
4494
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4430
4495
  type: "button",
@@ -4567,7 +4632,7 @@ window.__ModuleLoader__.load({
4567
4632
  task: selected,
4568
4633
  controller,
4569
4634
  now
4570
- })
4635
+ }, selected.id)
4571
4636
  }),
4572
4637
  state.composerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskFormModal, {
4573
4638
  controller,
@@ -4776,15 +4841,18 @@ window.__ModuleLoader__.load({
4776
4841
  * conversation content while the board is active. Toggling rides a data
4777
4842
  * attribute on <html> — no React involvement in the shell.
4778
4843
  *
4779
- * Column matching is DUAL (0.4.2): the dev shell marks the column with
4780
- * `data-pane="conversation"`; the DSH Desktop shell (dsh-client-ui-layout)
4781
- * dropped data-pane entirely and uses CSS-Module hashed class names
4782
- * (`pI_x6G_centerCol`) — the class-substring fallback keeps both mounting,
4783
- * exactly like sidebar-entry's `[class*="sidebarCol"]` fallback.
4844
+ * Column matching is TRIPLE-generation: the dev shell marks the column
4845
+ * with `data-pane="conversation"`; the official layout shell
4846
+ * (dsh-client-ui-layout) dropped data-pane and uses CSS-Module hashed
4847
+ * class names (`pI_x6G_centerCol`) — and DSH Desktop's non-compat
4848
+ * (extended) mode disables the official layout row entirely, owning the
4849
+ * columns itself (`main.dshDesktopConversationSurface`, 0.5.2) — the
4850
+ * fallbacks keep all three mounting, exactly like sidebar-entry's column
4851
+ * selector.
4784
4852
  *
4785
4853
  * @module dsh-taskboard/client/board-mount
4786
4854
  */
4787
- const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
4855
+ const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"], .dshDesktopConversationSurface";
4788
4856
  const ACTIVE_ATTR = "data-dsh-atb-active";
4789
4857
  /** Sibling panels' activation attributes, evicted when this board opens. */
4790
4858
  const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
@@ -4917,7 +4985,7 @@ window.__ModuleLoader__.load({
4917
4985
  const controller = new BoardController(createClient());
4918
4986
  const connection = ctx.get?.("connection");
4919
4987
  if (connection !== void 0) {
4920
- controller.modelCatalog = async () => {
4988
+ controller.installModelCatalog(async () => {
4921
4989
  const response = await connection.api.llm.models({});
4922
4990
  if (!response.result.ok) return [];
4923
4991
  const out = [];
@@ -4927,8 +4995,8 @@ window.__ModuleLoader__.load({
4927
4995
  name: model.name
4928
4996
  });
4929
4997
  return out;
4930
- };
4931
- controller.presetCatalog = async () => {
4998
+ });
4999
+ controller.installPresetRoster(async () => {
4932
5000
  const list = connection.api.agentPresets;
4933
5001
  if (list === void 0) return { presets: [] };
4934
5002
  const response = await list.list({});
@@ -4942,7 +5010,7 @@ window.__ModuleLoader__.load({
4942
5010
  presets,
4943
5011
  ...def !== void 0 ? { defaultId: def.id } : {}
4944
5012
  };
4945
- };
5013
+ });
4946
5014
  }
4947
5015
  controller.installSessionJumper(createSessionJumper({
4948
5016
  getSessions: () => ctx.get?.("sessions"),