dsh-taskboard 0.2.1 → 0.3.3

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/lib/client.js CHANGED
@@ -34,10 +34,18 @@ window.__ModuleLoader__.load({
34
34
  get: (id) => unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(id)}`)),
35
35
  update: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/update`, body),
36
36
  move: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/move`, body),
37
+ reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
37
38
  comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
38
39
  remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
39
- run: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
40
+ run: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, body ?? {}),
40
41
  cancel: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
42
+ mergeBranch: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/merge`, {}),
43
+ worktreeRemove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/worktree-remove`, body),
44
+ diagnostics: () => unwrap(fetch("/dsh-taskboard/diagnostics")),
45
+ worktreeCleanup: (workspaceId, taskId) => post("/dsh-taskboard/worktree-cleanup", {
46
+ workspaceId,
47
+ taskId
48
+ }),
41
49
  stream(onChange, onGap) {
42
50
  const es = new EventSource("/dsh-taskboard/events");
43
51
  let revision;
@@ -208,6 +216,22 @@ window.__ModuleLoader__.load({
208
216
  //#region src/client/controller.ts
209
217
  /** localStorage key for persisted view state (filters + sort). */
210
218
  const VIEW_KEY = "dsh-taskboard-view-v1";
219
+ /** localStorage key for the remembered isolation toggle choice (0.3.0). */
220
+ const ISOLATION_KEY = "dsh-taskboard-isolation-v1";
221
+ /** Load the remembered default isolation (worktree unless explicitly turned off). */
222
+ function loadDefaultIsolation() {
223
+ try {
224
+ return localStorage.getItem(ISOLATION_KEY) === "none" ? "none" : "worktree";
225
+ } catch {
226
+ return "worktree";
227
+ }
228
+ }
229
+ /** Remember the isolation toggle choice across forms (best effort). */
230
+ function saveDefaultIsolation(mode) {
231
+ try {
232
+ localStorage.setItem(ISOLATION_KEY, mode);
233
+ } catch {}
234
+ }
211
235
  /** Load the persisted view state (never throws; fresh on any parse error). */
212
236
  function loadView() {
213
237
  try {
@@ -244,7 +268,8 @@ window.__ModuleLoader__.load({
244
268
  search: "",
245
269
  sortBy: view.sortBy,
246
270
  composerOpen: false,
247
- secondaryOpen: false
271
+ secondaryOpen: false,
272
+ diagOpen: false
248
273
  };
249
274
  }
250
275
  /**
@@ -402,6 +427,11 @@ window.__ModuleLoader__.load({
402
427
  toggleSecondary() {
403
428
  this.setState({ secondaryOpen: !this.state.secondaryOpen });
404
429
  }
430
+ /** Whether a workspace passed git detection (form toggle enablement). */
431
+ gitAvailable(workspaceId) {
432
+ if (workspaceId === void 0) return true;
433
+ return this.state.workspaces.find((w) => w.id === workspaceId)?.gitAvailable === true;
434
+ }
405
435
  /**
406
436
  * Install the session-jump bridge (built from the runtime sessions service
407
437
  * by the client entry). Without it openSession reports 'unavailable'.
@@ -474,6 +504,27 @@ window.__ModuleLoader__.load({
474
504
  this.setState({ error: error instanceof Error ? error.message : String(error) });
475
505
  }
476
506
  }
507
+ /**
508
+ * Quick-reject (card ✗ button): move back to todo with an optional user
509
+ * comment, committed atomically host-side. Returns whether the task moved.
510
+ * @param id - task id.
511
+ * @param ifVersion - optimistic version (captured at click time).
512
+ * @param comment - optional comment text; blank = move only.
513
+ */
514
+ async reject(id, ifVersion, comment) {
515
+ const body = comment !== void 0 && comment.trim().length > 0 ? comment.trim() : void 0;
516
+ try {
517
+ await this.client.reject(id, body === void 0 ? { ifVersion } : {
518
+ ifVersion,
519
+ body
520
+ });
521
+ await this.refresh();
522
+ return true;
523
+ } catch (error) {
524
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
525
+ return false;
526
+ }
527
+ }
477
528
  /** Toggle the blocked marker. */
478
529
  async toggleBlocked(task) {
479
530
  try {
@@ -495,10 +546,10 @@ window.__ModuleLoader__.load({
495
546
  this.setState({ error: error instanceof Error ? error.message : String(error) });
496
547
  }
497
548
  }
498
- /** Trigger a manual run (fresh in-project session, pinned model). */
499
- async run(id) {
549
+ /** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
550
+ async run(id, reuse = false) {
500
551
  try {
501
- await this.client.run(id);
552
+ await this.client.run(id, reuse ? { reuse: true } : {});
502
553
  await this.refresh();
503
554
  } catch (error) {
504
555
  this.setState({ error: error instanceof Error ? error.message : String(error) });
@@ -513,6 +564,66 @@ window.__ModuleLoader__.load({
513
564
  this.setState({ error: error instanceof Error ? error.message : String(error) });
514
565
  }
515
566
  }
567
+ /**
568
+ * ⇥ 合并 (detail page): merge the task branch into the main worktree.
569
+ * @returns the outcome; `noop` means the branch had no new commits (nothing merged).
570
+ */
571
+ async mergeBranch(id) {
572
+ try {
573
+ const value = await this.client.mergeBranch(id);
574
+ await this.refresh();
575
+ return value.noop === true ? {
576
+ ok: true,
577
+ noop: true
578
+ } : { ok: true };
579
+ } catch (error) {
580
+ return {
581
+ ok: false,
582
+ error: error instanceof Error ? error.message : String(error)
583
+ };
584
+ }
585
+ }
586
+ /**
587
+ * 🗑 删除 worktree (detail page), optionally deleting the task branch too.
588
+ * @returns the outcome; failures carry the git message for an alert.
589
+ */
590
+ async removeWorktree(id, deleteBranch) {
591
+ try {
592
+ const value = await this.client.worktreeRemove(id, { deleteBranch });
593
+ await this.refresh();
594
+ return value.branchError !== void 0 ? {
595
+ ok: true,
596
+ branchError: value.branchError
597
+ } : { ok: true };
598
+ } catch (error) {
599
+ return {
600
+ ok: false,
601
+ error: error instanceof Error ? error.message : String(error)
602
+ };
603
+ }
604
+ }
605
+ /** Open the ⚙ diagnostics panel and fetch a fresh snapshot. */
606
+ openDiagnostics() {
607
+ this.setState({ diagOpen: true });
608
+ this.client.diagnostics().then((diagnostics) => this.setState({ diagnostics })).catch((error) => this.setState({ error: error instanceof Error ? error.message : String(error) }));
609
+ }
610
+ /** Close the ⚙ diagnostics panel. */
611
+ closeDiagnostics() {
612
+ this.setState({ diagOpen: false });
613
+ }
614
+ /** Clean one orphan worktree (⚙ panel); refreshes the diagnostics payload. */
615
+ async cleanupOrphan(workspaceId, taskId) {
616
+ try {
617
+ await this.client.worktreeCleanup(workspaceId, taskId);
618
+ const diagnostics = await this.client.diagnostics();
619
+ this.setState({
620
+ diagnostics,
621
+ error: void 0
622
+ });
623
+ } catch (error) {
624
+ this.setState({ error: error instanceof Error ? error.message : String(error) });
625
+ }
626
+ }
516
627
  /** Soft-delete (agent parity) then optional purge. */
517
628
  async remove(id, ifVersion, purge) {
518
629
  try {
@@ -523,7 +634,7 @@ window.__ModuleLoader__.load({
523
634
  this.setState({ error: error instanceof Error ? error.message : String(error) });
524
635
  }
525
636
  }
526
- /** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model). */
637
+ /** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model/isolation). */
527
638
  async duplicate(task) {
528
639
  try {
529
640
  await this.client.create({
@@ -536,7 +647,9 @@ window.__ModuleLoader__.load({
536
647
  mode: "scheduled",
537
648
  cron: task.execution.cron
538
649
  } : { mode: "claim" },
539
- model: task.model
650
+ model: task.model,
651
+ isolation: task.isolation,
652
+ ...task.presetId !== void 0 ? { presetId: task.presetId } : {}
540
653
  });
541
654
  await this.refresh();
542
655
  } catch (error) {
@@ -763,6 +876,21 @@ window.__ModuleLoader__.load({
763
876
  .dsh-atb-badge[data-kind="done"] { background: rgba(46,160,67,.16); color: #2ea043; }
764
877
  .dsh-atb-badge[data-kind="running"] { background: rgba(229,152,42,.16); color: #e69842; }
765
878
 
879
+ /* ---------- card quick review (in_review column) ---------- */
880
+ .dsh-atb-quick { display: flex; gap: 6px; margin-top: 7px; }
881
+ .dsh-atb-quickbtn {
882
+ flex: 1; font-size: 11.5px; padding: 3px 8px; border-radius: 6px; cursor: pointer;
883
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.3));
884
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.12)); color: inherit;
885
+ }
886
+ .dsh-atb-quickbtn:hover { border-color: var(--dsw-border-strong, rgba(128,128,128,.6)); }
887
+ .dsh-atb-quickbtn[data-act="done"] { background: rgba(46,160,67,.14); color: #2ea043; border-color: rgba(46,160,67,.4); }
888
+ .dsh-atb-quickbtn[data-act="done"]:hover { background: rgba(46,160,67,.22); }
889
+ .dsh-atb-quickbtn[data-act="reject"] { background: rgba(229,152,42,.12); color: #d9822b; border-color: rgba(229,152,42,.4); }
890
+ .dsh-atb-quickbtn[data-act="reject"]:hover { background: rgba(229,152,42,.2); }
891
+ .dsh-atb-quick-reject { display: flex; gap: 6px; margin-top: 7px; align-items: stretch; }
892
+ .dsh-atb-quick-note { flex: 1; min-width: 0; font-size: 11.5px; padding: 3px 8px; }
893
+
766
894
  .dsh-atb-error { font-size: 12px; color: #e5484d; padding: 4px 8px; border-radius: 6px; background: rgba(229,72,77,.1); }
767
895
  .dsh-atb-empty { font-size: 12px; color: var(--dsw-text-secondary, gray); padding: 10px 4px; }
768
896
 
@@ -1068,6 +1196,51 @@ window.__ModuleLoader__.load({
1068
1196
  color: var(--dsw-alias-label-primary, inherit);
1069
1197
  }
1070
1198
  .dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
1199
+
1200
+ /* ---------- 0.3.0 isolation ---------- */
1201
+ .dsh-atb-isolation-note { display: block; margin-top: 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
1202
+ .dsh-atb-mode-picker[data-disabled="true"] .dsh-atb-mode-opt { cursor: not-allowed; opacity: .55; }
1203
+ .dsh-atb-iso-none { font-size: 12.5px; color: var(--dsw-alias-label-secondary, inherit); }
1204
+ .dsh-atb-iso-facts { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 8px; }
1205
+ .dsh-atb-iso-fact { font-size: 11.5px; color: var(--dsw-alias-label-secondary, inherit); }
1206
+ .dsh-atb-iso-fact b { font-weight: 600; color: var(--dsw-alias-state-business-primary, #3e63dd); }
1207
+ .dsh-atb-iso-commits { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
1208
+ .dsh-atb-iso-commit { display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; }
1209
+ .dsh-atb-iso-commit code {
1210
+ font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
1211
+ color: var(--dsh-alias-state-business-primary, #3e63dd); flex-shrink: 0;
1212
+ }
1213
+ .dsh-atb-iso-commit span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
1214
+ .dsh-atb-iso-more { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
1215
+ .dsh-atb-iso-nocommit { font-size: 11.5px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 8px; }
1216
+ .dsh-atb-iso-dirty {
1217
+ font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
1218
+ background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
1219
+ border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
1220
+ }
1221
+ .dsh-atb-iso-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1222
+ .dsh-atb-iso-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
1223
+
1224
+ /* ---------- 0.3.0 diagnostics ---------- */
1225
+ .dsh-atb-diag { max-width: 520px; width: min(520px, 92vw); }
1226
+ .dsh-atb-diag-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 14px; }
1227
+ .dsh-atb-diag-item {
1228
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
1229
+ padding: 10px 6px; border-radius: 10px;
1230
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
1231
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.04));
1232
+ }
1233
+ .dsh-atb-diag-item b { font-size: 18px; font-weight: 700; }
1234
+ .dsh-atb-diag-item span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
1235
+ .dsh-atb-diag-item[data-bad="true"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
1236
+ .dsh-atb-diag-sec h4 { margin: 0 0 8px; font-size: 12.5px; }
1237
+ .dsh-atb-diag-orphans { display: flex; flex-direction: column; gap: 6px; }
1238
+ .dsh-atb-diag-orphan {
1239
+ display: flex; align-items: center; gap: 10px; justify-content: space-between;
1240
+ padding: 7px 10px; border-radius: 8px;
1241
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
1242
+ }
1243
+ .dsh-atb-diag-orphan-path { font-size: 11.5px; font-family: ui-monospace, Consolas, monospace; word-break: break-all; }
1071
1244
  `;
1072
1245
  let injected = false;
1073
1246
  /** Inject the stylesheet once (idempotent). */
@@ -1312,10 +1485,23 @@ window.__ModuleLoader__.load({
1312
1485
  * @module dsh-taskboard/shared/version
1313
1486
  */
1314
1487
  /** The package version (must equal package.json "version"). */
1315
- const PLUGIN_VERSION = "0.2.1";
1488
+ const PLUGIN_VERSION = "0.3.3";
1316
1489
 
1317
1490
  //#endregion
1318
1491
  //#region src/client/board/TaskCard.tsx
1492
+ /**
1493
+ * One board card: urgency edge, title, project/urgency/model/schedule/
1494
+ * blocked/trashed badges, comment count, and the last execution outcome.
1495
+ * Click opens the detail pane; cards in the backlog/todo columns are
1496
+ * draggable between those two columns (HTML5 drag & drop). Cards sitting
1497
+ * in the in_review column also carry quick-review actions (✓ complete /
1498
+ * ✗ send back with an optional note).
1499
+ *
1500
+ * The root is a div[role=button] (not a <button>) so the quick actions can
1501
+ * be real nested buttons — valid HTML and native keyboard activation.
1502
+ *
1503
+ * @module dsh-taskboard/client/board/TaskCard
1504
+ */
1319
1505
  const URGENCY_LABEL$1 = {
1320
1506
  urgent: "紧急",
1321
1507
  normal: "一般",
@@ -1338,14 +1524,27 @@ window.__ModuleLoader__.load({
1338
1524
  * @param onAlert - show an alert message (replaces native alert).
1339
1525
  */
1340
1526
  function TaskCard({ task, controller, draggable = false, now, onAlert }) {
1527
+ const [rejectOpen, setRejectOpen] = (0, react.useState)(false);
1528
+ const [note, setNote] = (0, react.useState)("");
1341
1529
  const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : void 0;
1342
1530
  const running = task.executions.find((ex) => ex.outcome === "running");
1343
1531
  const stale = now !== void 0 && isStaleClaim(task, now);
1344
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1345
- type: "button",
1532
+ const reviewing = task.status === "in_review" && task.trashedAt === void 0;
1533
+ /** Submit the quick-reject: one atomic route (move + optional note). */
1534
+ const submitReject = () => {
1535
+ controller.reject(task.id, task.version, note).then((ok) => {
1536
+ if (ok) {
1537
+ setRejectOpen(false);
1538
+ setNote("");
1539
+ }
1540
+ });
1541
+ };
1542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1543
+ role: "button",
1544
+ tabIndex: 0,
1346
1545
  className: "dsh-atb-card",
1347
1546
  "data-urgency": task.urgency,
1348
- draggable,
1547
+ draggable: draggable && !rejectOpen,
1349
1548
  onDragStart: (e) => {
1350
1549
  if (running !== void 0) {
1351
1550
  e.preventDefault();
@@ -1362,57 +1561,123 @@ window.__ModuleLoader__.load({
1362
1561
  delete e.currentTarget.dataset.dragging;
1363
1562
  },
1364
1563
  onClick: () => controller.select(task.id),
1365
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1366
- className: "dsh-atb-card-title",
1367
- children: task.title
1368
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1369
- className: "dsh-atb-card-meta",
1370
- children: [
1371
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1372
- className: "dsh-atb-badge",
1373
- children: URGENCY_LABEL$1[task.urgency]
1374
- }),
1375
- task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1376
- className: "dsh-atb-badge",
1377
- "data-kind": "blocked",
1378
- children: "受阻"
1379
- }),
1380
- stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1381
- className: "dsh-atb-badge",
1382
- "data-kind": "stale",
1383
- children: "⏱ 认领超时"
1384
- }),
1385
- task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1386
- className: "dsh-atb-badge",
1387
- "data-kind": "scheduled",
1388
- children: ["⏰ ", fmtTime(task.execution.nextRunAt)]
1389
- }),
1390
- task.model !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1391
- className: "dsh-atb-badge",
1392
- children: task.model.model
1393
- }),
1394
- task.status === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1395
- className: "dsh-atb-badge",
1396
- "data-kind": "done",
1397
- children: "完成"
1398
- }),
1399
- last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1400
- className: "dsh-atb-badge",
1401
- "data-kind": last.outcome === "running" ? "running" : last.outcome,
1402
- children: OUTCOME_LABEL$1[last.outcome] ?? last.outcome
1403
- }),
1404
- task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
1405
- task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1406
- className: "dsh-atb-badge",
1407
- "data-kind": "trashed",
1408
- children: "待清除"
1409
- }),
1410
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1411
- style: { marginLeft: "auto" },
1412
- children: fmtTime(task.updatedAt)
1413
- })
1414
- ]
1415
- })]
1564
+ onKeyDown: (e) => {
1565
+ if (e.target !== e.currentTarget) return;
1566
+ if (e.key === "Enter" || e.key === " ") {
1567
+ e.preventDefault();
1568
+ controller.select(task.id);
1569
+ }
1570
+ },
1571
+ children: [
1572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1573
+ className: "dsh-atb-card-title",
1574
+ children: task.title
1575
+ }),
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1577
+ className: "dsh-atb-card-meta",
1578
+ children: [
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1580
+ className: "dsh-atb-badge",
1581
+ children: URGENCY_LABEL$1[task.urgency]
1582
+ }),
1583
+ task.blocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1584
+ className: "dsh-atb-badge",
1585
+ "data-kind": "blocked",
1586
+ children: "受阻"
1587
+ }),
1588
+ stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1589
+ className: "dsh-atb-badge",
1590
+ "data-kind": "stale",
1591
+ children: "⏱ 认领超时"
1592
+ }),
1593
+ task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1594
+ className: "dsh-atb-badge",
1595
+ "data-kind": "scheduled",
1596
+ children: ["", fmtTime(task.execution.nextRunAt)]
1597
+ }),
1598
+ task.model !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1599
+ className: "dsh-atb-badge",
1600
+ children: task.model.model
1601
+ }),
1602
+ task.status === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1603
+ className: "dsh-atb-badge",
1604
+ "data-kind": "done",
1605
+ children: "完成"
1606
+ }),
1607
+ last !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1608
+ className: "dsh-atb-badge",
1609
+ "data-kind": last.outcome === "running" ? "running" : last.outcome,
1610
+ children: OUTCOME_LABEL$1[last.outcome] ?? last.outcome
1611
+ }),
1612
+ task.comments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["💬 ", task.comments.length] }),
1613
+ task.trashedAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1614
+ className: "dsh-atb-badge",
1615
+ "data-kind": "trashed",
1616
+ children: "待清除"
1617
+ }),
1618
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1619
+ style: { marginLeft: "auto" },
1620
+ children: fmtTime(task.updatedAt)
1621
+ })
1622
+ ]
1623
+ }),
1624
+ reviewing && (rejectOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1625
+ className: "dsh-atb-quick-reject",
1626
+ onClick: (e) => e.stopPropagation(),
1627
+ children: [
1628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1629
+ className: "dsh-atb-input dsh-atb-quick-note",
1630
+ value: note,
1631
+ placeholder: "退回原因(可选,agent 开工前会读)…",
1632
+ autoFocus: true,
1633
+ spellCheck: false,
1634
+ onChange: (e) => setNote(e.target.value),
1635
+ onKeyDown: (e) => {
1636
+ if (e.key === "Enter") submitReject();
1637
+ else if (e.key === "Escape") {
1638
+ setRejectOpen(false);
1639
+ setNote("");
1640
+ }
1641
+ }
1642
+ }),
1643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1644
+ type: "button",
1645
+ className: "dsh-atb-quickbtn",
1646
+ "data-act": "reject-confirm",
1647
+ onClick: submitReject,
1648
+ children: "退回待办"
1649
+ }),
1650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1651
+ type: "button",
1652
+ className: "dsh-atb-quickbtn",
1653
+ "data-act": "reject-cancel",
1654
+ onClick: () => {
1655
+ setRejectOpen(false);
1656
+ setNote("");
1657
+ },
1658
+ children: "取消"
1659
+ })
1660
+ ]
1661
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1662
+ className: "dsh-atb-quick",
1663
+ onClick: (e) => e.stopPropagation(),
1664
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1665
+ type: "button",
1666
+ className: "dsh-atb-quickbtn",
1667
+ "data-act": "done",
1668
+ title: "验收完成:移至已完成",
1669
+ onClick: () => void controller.move(task.id, task.version, "done"),
1670
+ children: "✓ 完成"
1671
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1672
+ type: "button",
1673
+ className: "dsh-atb-quickbtn",
1674
+ "data-act": "reject",
1675
+ title: "退回待办,可附退回原因",
1676
+ onClick: () => setRejectOpen(true),
1677
+ children: "✗ 退回"
1678
+ })]
1679
+ }))
1680
+ ]
1416
1681
  });
1417
1682
  }
1418
1683
 
@@ -1540,6 +1805,215 @@ window.__ModuleLoader__.load({
1540
1805
  }), children]
1541
1806
  });
1542
1807
  }
1808
+ /** The most recent execution carrying isolation facts, newest first. */
1809
+ function latestIsolated(task) {
1810
+ return [...task.executions].reverse().find((e) => e.isolation !== void 0 || e.worktreePath !== void 0 || e.isolationNote !== void 0);
1811
+ }
1812
+ /** Short commit hash for display. */
1813
+ function shortHash(hash) {
1814
+ return hash === void 0 ? "" : hash.slice(0, 8);
1815
+ }
1816
+ /**
1817
+ * The 0.3.0 isolation block: branch / baseline→head commits / change stats /
1818
+ * uncommitted-changes warning, plus the user-only git actions (merge /
1819
+ * remove worktree — plan §3.3).
1820
+ */
1821
+ function IsolationBlock({ task, controller }) {
1822
+ const { alert: showAlert, el: alertEl } = useAlert();
1823
+ const [confirmMerge, setConfirmMerge] = (0, react.useState)(false);
1824
+ const [confirmRemove, setConfirmRemove] = (0, react.useState)(null);
1825
+ const [busy, setBusy] = (0, react.useState)(false);
1826
+ const execution = latestIsolated(task);
1827
+ const running = task.executions.some((e) => e.outcome === "running");
1828
+ if (execution === void 0) return null;
1829
+ const doMerge = () => {
1830
+ setBusy(true);
1831
+ controller.mergeBranch(task.id).then((result) => {
1832
+ setBusy(false);
1833
+ setConfirmMerge(false);
1834
+ if (!result.ok) showAlert(`合并失败:${result.error}`);
1835
+ else if (result.noop === true) showAlert("该分支没有领先主工作区的新提交,无需合并(可退回续跑或直接清理)");
1836
+ });
1837
+ };
1838
+ const doRemove = (deleteBranch) => {
1839
+ setBusy(true);
1840
+ controller.removeWorktree(task.id, deleteBranch).then((result) => {
1841
+ setBusy(false);
1842
+ setConfirmRemove(null);
1843
+ if (!result.ok) showAlert(`删除失败:${result.error}`);
1844
+ else if (result.branchError !== void 0) showAlert(`worktree 已删除,但分支删除失败:${result.branchError}`);
1845
+ });
1846
+ };
1847
+ if (execution.isolation !== "worktree" || execution.worktreePath === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1848
+ className: "dsh-atb-fieldcard",
1849
+ "data-kind": "isolation",
1850
+ children: [
1851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1852
+ className: "dsh-atb-fieldcard-label",
1853
+ children: "执行隔离"
1854
+ }),
1855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1856
+ className: "dsh-atb-iso-none",
1857
+ children: ["📁 原目录执行", execution.isolationNote !== void 0 ? ` · ${execution.isolationNote}` : ""]
1858
+ }),
1859
+ alertEl
1860
+ ]
1861
+ });
1862
+ const commits = execution.commits ?? [];
1863
+ const commitTotal = execution.commitsTotal ?? commits.length;
1864
+ const dirty = execution.dirtyFiles ?? [];
1865
+ const dirtyTotal = execution.dirtyFilesTotal ?? dirty.length;
1866
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1867
+ className: "dsh-atb-fieldcard",
1868
+ "data-kind": "isolation",
1869
+ children: [
1870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1871
+ className: "dsh-atb-fieldcard-label",
1872
+ children: "执行隔离 · Worktree"
1873
+ }),
1874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1875
+ className: "dsh-atb-iso-facts",
1876
+ children: [
1877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1878
+ className: "dsh-atb-iso-fact",
1879
+ title: execution.worktreePath,
1880
+ children: ["🌿 分支 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: execution.branch ?? task.branch })]
1881
+ }),
1882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1883
+ className: "dsh-atb-iso-fact",
1884
+ children: [
1885
+ "基线 ",
1886
+ shortHash(execution.baseCommit),
1887
+ " → ",
1888
+ shortHash(execution.headCommit)
1889
+ ]
1890
+ }),
1891
+ execution.changedFiles !== void 0 && execution.changedFiles > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1892
+ className: "dsh-atb-iso-fact",
1893
+ children: [
1894
+ "改动 ",
1895
+ execution.changedFiles,
1896
+ " 个文件"
1897
+ ]
1898
+ }),
1899
+ execution.diffStat !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1900
+ className: "dsh-atb-iso-fact",
1901
+ title: execution.diffStat,
1902
+ children: execution.diffStat
1903
+ })
1904
+ ]
1905
+ }),
1906
+ commits.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1907
+ className: "dsh-atb-iso-commits",
1908
+ children: [commits.slice(0, 10).map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1909
+ className: "dsh-atb-iso-commit",
1910
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: shortHash(c.hash) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: c.subject })]
1911
+ }, c.hash)), commitTotal > 10 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1912
+ className: "dsh-atb-iso-more",
1913
+ children: [
1914
+ "… 共 ",
1915
+ commitTotal,
1916
+ " 个提交"
1917
+ ]
1918
+ })]
1919
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1920
+ className: "dsh-atb-iso-nocommit",
1921
+ children: "该次执行没有产生提交(改动可能未提交,见下方警告)"
1922
+ }),
1923
+ dirtyTotal > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1924
+ className: "dsh-atb-iso-dirty",
1925
+ title: dirty.join("\n"),
1926
+ children: [
1927
+ "⚠ 有 ",
1928
+ dirtyTotal,
1929
+ " 处未提交修改(合并前请让 agent 提交,或手动处理)"
1930
+ ]
1931
+ }),
1932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1933
+ className: "dsh-atb-iso-actions",
1934
+ children: [
1935
+ running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1936
+ className: "dsh-atb-iso-hint",
1937
+ children: "执行中 — 结束后可合并或清理"
1938
+ }) : confirmMerge ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1939
+ className: "dsh-atb-confirm",
1940
+ children: [
1941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1942
+ className: "dsh-atb-confirm-label",
1943
+ children: "将分支以 --no-ff 合并到主工作区?"
1944
+ }),
1945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1946
+ type: "button",
1947
+ className: "dsh-atb-btn",
1948
+ "data-primary": "true",
1949
+ disabled: busy,
1950
+ onClick: doMerge,
1951
+ children: "确认合并"
1952
+ }),
1953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1954
+ type: "button",
1955
+ className: "dsh-atb-btn",
1956
+ onClick: () => setConfirmMerge(false),
1957
+ children: "取消"
1958
+ })
1959
+ ]
1960
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1961
+ type: "button",
1962
+ className: "dsh-atb-btn",
1963
+ disabled: busy,
1964
+ title: "在主工作区 git merge --no-ff 该任务分支(要求主区干净;冲突会原样报告)",
1965
+ onClick: () => setConfirmMerge(true),
1966
+ children: "⇥ 合并到主工作区"
1967
+ }),
1968
+ !running && (confirmRemove === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1969
+ type: "button",
1970
+ className: "dsh-atb-btn",
1971
+ "data-danger": "true",
1972
+ disabled: busy,
1973
+ title: "git worktree remove(有未提交修改时拒绝)",
1974
+ onClick: () => setConfirmRemove("wt"),
1975
+ children: "🗑 删除 worktree"
1976
+ }), task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1977
+ type: "button",
1978
+ className: "dsh-atb-btn",
1979
+ "data-danger": "true",
1980
+ disabled: busy,
1981
+ title: "删除 worktree 并删除任务分支(有未提交修改时拒绝)",
1982
+ onClick: () => setConfirmRemove("wtb"),
1983
+ children: "🗑 删 worktree + 分支"
1984
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1985
+ className: "dsh-atb-confirm",
1986
+ children: [
1987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1988
+ className: "dsh-atb-confirm-label",
1989
+ children: confirmRemove === "wtb" ? "删除 worktree 并删除分支?" : "删除 worktree 目录?"
1990
+ }),
1991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1992
+ type: "button",
1993
+ className: "dsh-atb-btn",
1994
+ "data-danger": "true",
1995
+ disabled: busy,
1996
+ onClick: () => doRemove(confirmRemove === "wtb"),
1997
+ children: "确认删除"
1998
+ }),
1999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2000
+ type: "button",
2001
+ className: "dsh-atb-btn",
2002
+ onClick: () => setConfirmRemove(null),
2003
+ children: "取消"
2004
+ })
2005
+ ]
2006
+ })),
2007
+ !running && confirmRemove === null && !confirmMerge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2008
+ className: "dsh-atb-iso-hint",
2009
+ children: "分支与 worktree 保留中 — 可退回继续修改"
2010
+ })
2011
+ ]
2012
+ }),
2013
+ alertEl
2014
+ ]
2015
+ });
2016
+ }
1543
2017
  /**
1544
2018
  * The detail view.
1545
2019
  * @param task - the task record.
@@ -1597,6 +2071,10 @@ window.__ModuleLoader__.load({
1597
2071
  icon: "✦",
1598
2072
  children: task.model.model
1599
2073
  }),
2074
+ task.presetId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
2075
+ icon: "🎛",
2076
+ children: task.presetId
2077
+ }),
1600
2078
  task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1601
2079
  icon: "⏰",
1602
2080
  children: [
@@ -1610,6 +2088,15 @@ window.__ModuleLoader__.load({
1610
2088
  tone: "urgent",
1611
2089
  children: "受阻"
1612
2090
  }),
2091
+ task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
2092
+ icon: "🌿",
2093
+ tone: void 0,
2094
+ children: ["Worktree · ", task.branch.length > 28 ? `${task.branch.slice(0, 28)}…` : task.branch]
2095
+ }),
2096
+ (task.isolation === void 0 || task.isolation === "worktree") && task.branch === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
2097
+ icon: "🌿",
2098
+ children: "Worktree 隔离"
2099
+ }),
1613
2100
  holder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1614
2101
  icon: stale ? "⏱" : "🔑",
1615
2102
  tone: stale ? "urgent" : void 0,
@@ -1653,6 +2140,13 @@ window.__ModuleLoader__.load({
1653
2140
  onClick: () => void controller.duplicate(task),
1654
2141
  children: "⧉ 复制"
1655
2142
  }),
2143
+ canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2144
+ type: "button",
2145
+ className: "dsh-atb-detail-run",
2146
+ title: "续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线",
2147
+ onClick: () => void controller.run(task.id, true),
2148
+ children: "↻ 续跑"
2149
+ }),
1656
2150
  canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1657
2151
  type: "button",
1658
2152
  className: "dsh-atb-detail-run",
@@ -1723,6 +2217,10 @@ window.__ModuleLoader__.load({
1723
2217
  children: task.prompt
1724
2218
  })]
1725
2219
  }),
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IsolationBlock, {
2221
+ task,
2222
+ controller
2223
+ }),
1726
2224
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1727
2225
  className: "dsh-atb-detail-actions",
1728
2226
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2025,6 +2523,10 @@ window.__ModuleLoader__.load({
2025
2523
  const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? "0 9 * * *");
2026
2524
  const [catalog, setCatalog] = (0, react.useState)([]);
2027
2525
  const [model, setModel] = (0, react.useState)(task?.model !== void 0 ? JSON.stringify(task.model) : "");
2526
+ const [presetId, setPresetId] = (0, react.useState)(task?.presetId ?? "");
2527
+ const [presets, setPresets] = (0, react.useState)([]);
2528
+ const [presetDefault, setPresetDefault] = (0, react.useState)(void 0);
2529
+ const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? loadDefaultIsolation());
2028
2530
  const titleRef = (0, react.useRef)(null);
2029
2531
  (0, react.useEffect)(() => {
2030
2532
  titleRef.current?.focus();
@@ -2039,14 +2541,36 @@ window.__ModuleLoader__.load({
2039
2541
  if (face === void 0) return;
2040
2542
  face().then(setCatalog).catch(() => setCatalog([]));
2041
2543
  }, [controller]);
2544
+ (0, react.useEffect)(() => {
2545
+ const face = controller.presetCatalog;
2546
+ if (face === void 0) return;
2547
+ face().then((roster) => {
2548
+ setPresets(roster.presets);
2549
+ setPresetDefault(roster.defaultId);
2550
+ if (task?.presetId === void 0 && roster.defaultId !== void 0) setPresetId(roster.defaultId);
2551
+ }).catch(() => setPresets([]));
2552
+ }, [controller, task?.presetId]);
2042
2553
  const cronMatch = mode === "scheduled" ? parseCron(cron.trim()) : null;
2043
2554
  const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
2044
2555
  const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
2045
2556
  const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
2046
2557
  const runBlocked = editing && task.status === "in_progress";
2558
+ const isolationLocked = editing && ((task.executions?.length ?? 0) > 0 || task.status === "in_progress");
2559
+ const gitOk = controller.gitAvailable(workspaceId);
2560
+ const isolationDisabled = isolationLocked || !gitOk;
2561
+ /** Isolation payload for submit: undefined keeps the default (degrades naturally). */
2562
+ const isolationPayload = () => {
2563
+ if (!gitOk) return void 0;
2564
+ if (!editing) saveDefaultIsolation(isolation);
2565
+ return isolation;
2566
+ };
2567
+ /** Preset payload: '' = follow the deployment default (submit omits). */
2568
+ const presetPayload = () => presetId.trim().length > 0 ? presetId.trim() : void 0;
2047
2569
  const submit = () => {
2048
2570
  if (!valid) return;
2049
2571
  const picked = model !== "" ? JSON.parse(model) : void 0;
2572
+ const isolationOut = isolationPayload();
2573
+ const presetOut = presetPayload();
2050
2574
  if (editing) controller.update(task.id, task.version, {
2051
2575
  title,
2052
2576
  description,
@@ -2057,7 +2581,9 @@ window.__ModuleLoader__.load({
2057
2581
  mode,
2058
2582
  cron: cron.trim()
2059
2583
  } : { mode },
2060
- model: picked ?? null
2584
+ model: picked ?? null,
2585
+ ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
2586
+ presetId: presetOut ?? null
2061
2587
  });
2062
2588
  else controller.create({
2063
2589
  title,
@@ -2069,13 +2595,17 @@ window.__ModuleLoader__.load({
2069
2595
  mode,
2070
2596
  cron: cron.trim()
2071
2597
  } : { mode },
2072
- model: picked
2598
+ model: picked,
2599
+ ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
2600
+ ...presetOut !== void 0 ? { presetId: presetOut } : {}
2073
2601
  });
2074
2602
  };
2075
2603
  /** Save the form, then immediately trigger a manual run of the task. */
2076
2604
  const submitAndRun = () => {
2077
2605
  if (!valid || runBlocked) return;
2078
2606
  const picked = model !== "" ? JSON.parse(model) : void 0;
2607
+ const isolationOut = isolationPayload();
2608
+ const presetOut = presetPayload();
2079
2609
  if (editing) (async () => {
2080
2610
  if (await controller.update(task.id, task.version, {
2081
2611
  title,
@@ -2087,7 +2617,9 @@ window.__ModuleLoader__.load({
2087
2617
  mode,
2088
2618
  cron: cron.trim()
2089
2619
  } : { mode },
2090
- model: picked ?? null
2620
+ model: picked ?? null,
2621
+ ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
2622
+ presetId: presetOut ?? null
2091
2623
  })) await controller.run(task.id);
2092
2624
  })();
2093
2625
  else (async () => {
@@ -2101,7 +2633,9 @@ window.__ModuleLoader__.load({
2101
2633
  mode,
2102
2634
  cron: cron.trim()
2103
2635
  } : { mode },
2104
- model: picked
2636
+ model: picked,
2637
+ ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
2638
+ ...presetOut !== void 0 ? { presetId: presetOut } : {}
2105
2639
  });
2106
2640
  if (id !== void 0) await controller.run(id);
2107
2641
  })();
@@ -2188,6 +2722,21 @@ window.__ModuleLoader__.load({
2188
2722
  }, `${m.provider}/${m.model}`))]
2189
2723
  })
2190
2724
  }),
2725
+ presets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2726
+ label: "执行模式(preset)",
2727
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2728
+ value: presetId,
2729
+ onChange: (e) => setPresetId(e.target.value),
2730
+ title: "执行会话按该 preset 组合(决定工具集与人设);默认 = 部署默认 preset",
2731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2732
+ value: "",
2733
+ children: ["跟随部署默认", presetDefault !== void 0 ? `(当前:${presets.find((p) => p.id === presetDefault)?.name ?? presetDefault})` : ""]
2734
+ }), presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2735
+ value: p.id,
2736
+ children: [p.name ?? p.id, p.id === presetDefault ? "(部署默认)" : ""]
2737
+ }, p.id))]
2738
+ })
2739
+ }),
2191
2740
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2192
2741
  label: "紧急度",
2193
2742
  full: true,
@@ -2285,6 +2834,46 @@ window.__ModuleLoader__.load({
2285
2834
  children: ["下次 ", fmtTime(nextRun)]
2286
2835
  })]
2287
2836
  })]
2837
+ }),
2838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
2839
+ label: "执行隔离",
2840
+ full: true,
2841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2842
+ className: "dsh-atb-mode-picker",
2843
+ "data-disabled": isolationDisabled ? "true" : void 0,
2844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2845
+ type: "button",
2846
+ className: "dsh-atb-mode-opt",
2847
+ "data-on": isolation === "worktree",
2848
+ disabled: isolationDisabled,
2849
+ title: isolationLocked ? "任务已有执行记录,隔离方式已锁定" : !gitOk ? "当前项目非 git 仓库" : "每次执行在独立 worktree 分支上进行",
2850
+ onClick: () => setIsolation("worktree"),
2851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2852
+ className: "dsh-atb-mode-name",
2853
+ children: "🌿 Worktree 隔离"
2854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2855
+ className: "dsh-atb-mode-hint",
2856
+ children: isolationLocked ? "已锁定(执行开始后不可更改)" : !gitOk ? "当前项目非 git 仓库" : "独立分支 task/标题+ID,互不污染"
2857
+ })]
2858
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2859
+ type: "button",
2860
+ className: "dsh-atb-mode-opt",
2861
+ "data-on": isolation === "none",
2862
+ disabled: isolationDisabled,
2863
+ title: isolationLocked ? "任务已有执行记录,隔离方式已锁定" : "直接在项目目录执行(不使用 git)",
2864
+ onClick: () => setIsolation("none"),
2865
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2866
+ className: "dsh-atb-mode-name",
2867
+ children: "📁 原目录执行"
2868
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2869
+ className: "dsh-atb-mode-hint",
2870
+ children: isolationLocked ? "已锁定(执行开始后不可更改)" : !gitOk ? "当前项目非 git 仓库,将在原目录执行" : "不使用 git,直接在项目目录工作"
2871
+ })]
2872
+ })]
2873
+ }), !gitOk && !isolationLocked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2874
+ className: "dsh-atb-isolation-note",
2875
+ children: "当前项目非 git 仓库,将在原目录执行(任务仍按默认配置创建,运行时自动降级)"
2876
+ })]
2288
2877
  })
2289
2878
  ]
2290
2879
  }),
@@ -2485,6 +3074,13 @@ window.__ModuleLoader__.load({
2485
3074
  onClick: () => controller.toggleSecondary(),
2486
3075
  children: state.secondaryOpen ? "返回看板" : "其它任务"
2487
3076
  }),
3077
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3078
+ type: "button",
3079
+ className: "dsh-atb-btn",
3080
+ title: "健康诊断:遗留 worktree、台账基本项",
3081
+ onClick: () => controller.openDiagnostics(),
3082
+ children: "⚙ 诊断"
3083
+ }),
2488
3084
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2489
3085
  type: "button",
2490
3086
  className: "dsh-atb-btn",
@@ -2585,10 +3181,138 @@ window.__ModuleLoader__.load({
2585
3181
  controller,
2586
3182
  task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
2587
3183
  }),
3184
+ state.diagOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsPanel, { controller }),
2588
3185
  alertEl
2589
3186
  ]
2590
3187
  });
2591
3188
  }
3189
+ /** ⚙ Health-diagnostics panel (plan §3.6): ledger basics + orphan worktrees + one-click cleanup. */
3190
+ function DiagnosticsPanel({ controller }) {
3191
+ const state = controller.getSnapshot();
3192
+ const diag = state.diagnostics;
3193
+ const wsName = (id) => {
3194
+ const ws = state.workspaces.find((w) => w.id === id);
3195
+ return ws?.title ?? ws?.path ?? id.slice(0, 8);
3196
+ };
3197
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3198
+ className: "dsh-atb-modal-backdrop",
3199
+ onClick: (e) => {
3200
+ if (e.target === e.currentTarget) controller.closeDiagnostics();
3201
+ },
3202
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3203
+ className: "dsh-atb-modal dsh-atb-diag",
3204
+ role: "dialog",
3205
+ "aria-modal": "true",
3206
+ "aria-label": "健康诊断",
3207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3208
+ className: "dsh-atb-modal-head",
3209
+ children: [
3210
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3211
+ className: "dsh-atb-modal-headicon",
3212
+ children: "⚙"
3213
+ }),
3214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3215
+ className: "dsh-atb-modal-headtext",
3216
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "健康诊断" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "台账基本项与 worktree 遗留清理" })]
3217
+ }),
3218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3219
+ type: "button",
3220
+ className: "dsh-atb-modal-close",
3221
+ "aria-label": "关闭",
3222
+ onClick: () => controller.closeDiagnostics(),
3223
+ children: "✕"
3224
+ })
3225
+ ]
3226
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3227
+ className: "dsh-atb-modal-body",
3228
+ children: diag === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3229
+ className: "dsh-atb-empty2",
3230
+ children: "读取中…"
3231
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3233
+ className: "dsh-atb-diag-grid",
3234
+ children: [
3235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3236
+ className: "dsh-atb-diag-item",
3237
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.revision }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "台账修订号" })]
3238
+ }),
3239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3240
+ className: "dsh-atb-diag-item",
3241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.tasks }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "任务总数" })]
3242
+ }),
3243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3244
+ className: "dsh-atb-diag-item",
3245
+ "data-bad": diag.staleRunning > 0 ? "true" : void 0,
3246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.staleRunning }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "执行中" })]
3247
+ }),
3248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3249
+ className: "dsh-atb-diag-item",
3250
+ "data-bad": diag.orphanWorktrees.length > 0 ? "true" : void 0,
3251
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: diag.orphanWorktrees.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "遗留 worktree" })]
3252
+ })
3253
+ ]
3254
+ }),
3255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3256
+ className: "dsh-atb-diag-sec",
3257
+ children: [
3258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "遗留 worktree(台账无主但目录存在)" }),
3259
+ diag.orphanWorktrees.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3260
+ className: "dsh-atb-empty2",
3261
+ children: "无遗留 — 各项目 .dsh-worktrees 目录干净"
3262
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3263
+ className: "dsh-atb-diag-orphans",
3264
+ children: diag.orphanWorktrees.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3265
+ className: "dsh-atb-diag-orphan",
3266
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3267
+ className: "dsh-atb-diag-orphan-path",
3268
+ title: o.path,
3269
+ children: [
3270
+ wsName(o.workspaceId),
3271
+ " · ",
3272
+ o.taskId
3273
+ ]
3274
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3275
+ type: "button",
3276
+ className: "dsh-atb-btn",
3277
+ "data-danger": "true",
3278
+ onClick: () => void controller.cleanupOrphan(o.workspaceId, o.taskId),
3279
+ children: "清理"
3280
+ })]
3281
+ }, o.path))
3282
+ }),
3283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3284
+ className: "dsh-atb-empty2",
3285
+ children: "提示:有未提交修改的遗留目录会被拒绝清理,请先手动处理其内容。live 任务的 worktree 请在任务详情页删除。"
3286
+ })
3287
+ ]
3288
+ }),
3289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3290
+ className: "dsh-atb-diag-sec",
3291
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "gitignore 建议" }), (diag.gitIgnoreSuggestions ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3292
+ className: "dsh-atb-empty2",
3293
+ children: "无待办 — 各 git 项目已忽略 .dsh-worktrees 目录"
3294
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3295
+ className: "dsh-atb-diag-orphans",
3296
+ children: diag.gitIgnoreSuggestions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3297
+ className: "dsh-atb-diag-orphan",
3298
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3299
+ className: "dsh-atb-diag-orphan-path",
3300
+ title: s.workspacePath,
3301
+ children: [
3302
+ wsName(s.workspaceId),
3303
+ " · 建议在 .gitignore 加入一行 ",
3304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: ".dsh-worktrees/" }),
3305
+ "(不会自动修改)"
3306
+ ]
3307
+ })
3308
+ }, s.workspaceId))
3309
+ })]
3310
+ })
3311
+ ] })
3312
+ })]
3313
+ })
3314
+ });
3315
+ }
2592
3316
  /** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
2593
3317
  function SecondaryTab({ controller, tasks }) {
2594
3318
  const trashed = tasks.filter((t) => t.trashedAt !== void 0);
@@ -2791,17 +3515,34 @@ window.__ModuleLoader__.load({
2791
3515
  injectStyles();
2792
3516
  const controller = new BoardController(createClient());
2793
3517
  const connection = ctx.get?.("connection");
2794
- if (connection !== void 0) controller.modelCatalog = async () => {
2795
- const response = await connection.api.llm.models({});
2796
- if (!response.result.ok) return [];
2797
- const out = [];
2798
- for (const group of response.result.value.groups) for (const model of group.models) out.push({
2799
- provider: group.id,
2800
- model: model.id,
2801
- name: model.name
2802
- });
2803
- return out;
2804
- };
3518
+ if (connection !== void 0) {
3519
+ controller.modelCatalog = async () => {
3520
+ const response = await connection.api.llm.models({});
3521
+ if (!response.result.ok) return [];
3522
+ const out = [];
3523
+ for (const group of response.result.value.groups) for (const model of group.models) out.push({
3524
+ provider: group.id,
3525
+ model: model.id,
3526
+ name: model.name
3527
+ });
3528
+ return out;
3529
+ };
3530
+ controller.presetCatalog = async () => {
3531
+ const list = connection.api.agentPresets;
3532
+ if (list === void 0) return { presets: [] };
3533
+ const response = await list.list({});
3534
+ if (!response.result.ok) return { presets: [] };
3535
+ const presets = response.result.value.presets.map((p) => ({
3536
+ id: p.id,
3537
+ name: p.name
3538
+ }));
3539
+ const def = response.result.value.presets.find((p) => p.isDefault);
3540
+ return {
3541
+ presets,
3542
+ ...def !== void 0 ? { defaultId: def.id } : {}
3543
+ };
3544
+ };
3545
+ }
2805
3546
  controller.installSessionJumper(createSessionJumper({
2806
3547
  getSessions: () => ctx.get?.("sessions"),
2807
3548
  getWorkspaces: () => ctx.get?.("workspaces")