dsh-taskboard 0.2.2 → 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
@@ -37,8 +37,15 @@ window.__ModuleLoader__.load({
37
37
  reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
38
38
  comment: (id, bodyText) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/comment`, { body: bodyText }),
39
39
  remove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/delete`, body),
40
- run: (id) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, {}),
40
+ run: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/run`, body ?? {}),
41
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
+ }),
42
49
  stream(onChange, onGap) {
43
50
  const es = new EventSource("/dsh-taskboard/events");
44
51
  let revision;
@@ -209,6 +216,22 @@ window.__ModuleLoader__.load({
209
216
  //#region src/client/controller.ts
210
217
  /** localStorage key for persisted view state (filters + sort). */
211
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
+ }
212
235
  /** Load the persisted view state (never throws; fresh on any parse error). */
213
236
  function loadView() {
214
237
  try {
@@ -245,7 +268,8 @@ window.__ModuleLoader__.load({
245
268
  search: "",
246
269
  sortBy: view.sortBy,
247
270
  composerOpen: false,
248
- secondaryOpen: false
271
+ secondaryOpen: false,
272
+ diagOpen: false
249
273
  };
250
274
  }
251
275
  /**
@@ -403,6 +427,11 @@ window.__ModuleLoader__.load({
403
427
  toggleSecondary() {
404
428
  this.setState({ secondaryOpen: !this.state.secondaryOpen });
405
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
+ }
406
435
  /**
407
436
  * Install the session-jump bridge (built from the runtime sessions service
408
437
  * by the client entry). Without it openSession reports 'unavailable'.
@@ -517,10 +546,10 @@ window.__ModuleLoader__.load({
517
546
  this.setState({ error: error instanceof Error ? error.message : String(error) });
518
547
  }
519
548
  }
520
- /** Trigger a manual run (fresh in-project session, pinned model). */
521
- async run(id) {
549
+ /** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
550
+ async run(id, reuse = false) {
522
551
  try {
523
- await this.client.run(id);
552
+ await this.client.run(id, reuse ? { reuse: true } : {});
524
553
  await this.refresh();
525
554
  } catch (error) {
526
555
  this.setState({ error: error instanceof Error ? error.message : String(error) });
@@ -535,6 +564,66 @@ window.__ModuleLoader__.load({
535
564
  this.setState({ error: error instanceof Error ? error.message : String(error) });
536
565
  }
537
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
+ }
538
627
  /** Soft-delete (agent parity) then optional purge. */
539
628
  async remove(id, ifVersion, purge) {
540
629
  try {
@@ -545,7 +634,7 @@ window.__ModuleLoader__.load({
545
634
  this.setState({ error: error instanceof Error ? error.message : String(error) });
546
635
  }
547
636
  }
548
- /** 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). */
549
638
  async duplicate(task) {
550
639
  try {
551
640
  await this.client.create({
@@ -558,7 +647,9 @@ window.__ModuleLoader__.load({
558
647
  mode: "scheduled",
559
648
  cron: task.execution.cron
560
649
  } : { mode: "claim" },
561
- model: task.model
650
+ model: task.model,
651
+ isolation: task.isolation,
652
+ ...task.presetId !== void 0 ? { presetId: task.presetId } : {}
562
653
  });
563
654
  await this.refresh();
564
655
  } catch (error) {
@@ -1105,6 +1196,51 @@ window.__ModuleLoader__.load({
1105
1196
  color: var(--dsw-alias-label-primary, inherit);
1106
1197
  }
1107
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; }
1108
1244
  `;
1109
1245
  let injected = false;
1110
1246
  /** Inject the stylesheet once (idempotent). */
@@ -1349,7 +1485,7 @@ window.__ModuleLoader__.load({
1349
1485
  * @module dsh-taskboard/shared/version
1350
1486
  */
1351
1487
  /** The package version (must equal package.json "version"). */
1352
- const PLUGIN_VERSION = "0.2.2";
1488
+ const PLUGIN_VERSION = "0.3.3";
1353
1489
 
1354
1490
  //#endregion
1355
1491
  //#region src/client/board/TaskCard.tsx
@@ -1669,6 +1805,215 @@ window.__ModuleLoader__.load({
1669
1805
  }), children]
1670
1806
  });
1671
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
+ }
1672
2017
  /**
1673
2018
  * The detail view.
1674
2019
  * @param task - the task record.
@@ -1726,6 +2071,10 @@ window.__ModuleLoader__.load({
1726
2071
  icon: "✦",
1727
2072
  children: task.model.model
1728
2073
  }),
2074
+ task.presetId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
2075
+ icon: "🎛",
2076
+ children: task.presetId
2077
+ }),
1729
2078
  task.execution.mode === "scheduled" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1730
2079
  icon: "⏰",
1731
2080
  children: [
@@ -1739,6 +2088,15 @@ window.__ModuleLoader__.load({
1739
2088
  tone: "urgent",
1740
2089
  children: "受阻"
1741
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
+ }),
1742
2100
  holder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
1743
2101
  icon: stale ? "⏱" : "🔑",
1744
2102
  tone: stale ? "urgent" : void 0,
@@ -1782,6 +2140,13 @@ window.__ModuleLoader__.load({
1782
2140
  onClick: () => void controller.duplicate(task),
1783
2141
  children: "⧉ 复制"
1784
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
+ }),
1785
2150
  canRun && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1786
2151
  type: "button",
1787
2152
  className: "dsh-atb-detail-run",
@@ -1852,6 +2217,10 @@ window.__ModuleLoader__.load({
1852
2217
  children: task.prompt
1853
2218
  })]
1854
2219
  }),
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IsolationBlock, {
2221
+ task,
2222
+ controller
2223
+ }),
1855
2224
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1856
2225
  className: "dsh-atb-detail-actions",
1857
2226
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2154,6 +2523,10 @@ window.__ModuleLoader__.load({
2154
2523
  const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? "0 9 * * *");
2155
2524
  const [catalog, setCatalog] = (0, react.useState)([]);
2156
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());
2157
2530
  const titleRef = (0, react.useRef)(null);
2158
2531
  (0, react.useEffect)(() => {
2159
2532
  titleRef.current?.focus();
@@ -2168,14 +2541,36 @@ window.__ModuleLoader__.load({
2168
2541
  if (face === void 0) return;
2169
2542
  face().then(setCatalog).catch(() => setCatalog([]));
2170
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]);
2171
2553
  const cronMatch = mode === "scheduled" ? parseCron(cron.trim()) : null;
2172
2554
  const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
2173
2555
  const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
2174
2556
  const valid = title.trim().length > 0 && workspaceId !== "" && !cronBad;
2175
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;
2176
2569
  const submit = () => {
2177
2570
  if (!valid) return;
2178
2571
  const picked = model !== "" ? JSON.parse(model) : void 0;
2572
+ const isolationOut = isolationPayload();
2573
+ const presetOut = presetPayload();
2179
2574
  if (editing) controller.update(task.id, task.version, {
2180
2575
  title,
2181
2576
  description,
@@ -2186,7 +2581,9 @@ window.__ModuleLoader__.load({
2186
2581
  mode,
2187
2582
  cron: cron.trim()
2188
2583
  } : { mode },
2189
- model: picked ?? null
2584
+ model: picked ?? null,
2585
+ ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
2586
+ presetId: presetOut ?? null
2190
2587
  });
2191
2588
  else controller.create({
2192
2589
  title,
@@ -2198,13 +2595,17 @@ window.__ModuleLoader__.load({
2198
2595
  mode,
2199
2596
  cron: cron.trim()
2200
2597
  } : { mode },
2201
- model: picked
2598
+ model: picked,
2599
+ ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
2600
+ ...presetOut !== void 0 ? { presetId: presetOut } : {}
2202
2601
  });
2203
2602
  };
2204
2603
  /** Save the form, then immediately trigger a manual run of the task. */
2205
2604
  const submitAndRun = () => {
2206
2605
  if (!valid || runBlocked) return;
2207
2606
  const picked = model !== "" ? JSON.parse(model) : void 0;
2607
+ const isolationOut = isolationPayload();
2608
+ const presetOut = presetPayload();
2208
2609
  if (editing) (async () => {
2209
2610
  if (await controller.update(task.id, task.version, {
2210
2611
  title,
@@ -2216,7 +2617,9 @@ window.__ModuleLoader__.load({
2216
2617
  mode,
2217
2618
  cron: cron.trim()
2218
2619
  } : { mode },
2219
- model: picked ?? null
2620
+ model: picked ?? null,
2621
+ ...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
2622
+ presetId: presetOut ?? null
2220
2623
  })) await controller.run(task.id);
2221
2624
  })();
2222
2625
  else (async () => {
@@ -2230,7 +2633,9 @@ window.__ModuleLoader__.load({
2230
2633
  mode,
2231
2634
  cron: cron.trim()
2232
2635
  } : { mode },
2233
- model: picked
2636
+ model: picked,
2637
+ ...isolationOut !== void 0 ? { isolation: isolationOut } : {},
2638
+ ...presetOut !== void 0 ? { presetId: presetOut } : {}
2234
2639
  });
2235
2640
  if (id !== void 0) await controller.run(id);
2236
2641
  })();
@@ -2317,6 +2722,21 @@ window.__ModuleLoader__.load({
2317
2722
  }, `${m.provider}/${m.model}`))]
2318
2723
  })
2319
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
+ }),
2320
2740
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2321
2741
  label: "紧急度",
2322
2742
  full: true,
@@ -2414,6 +2834,46 @@ window.__ModuleLoader__.load({
2414
2834
  children: ["下次 ", fmtTime(nextRun)]
2415
2835
  })]
2416
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
+ })]
2417
2877
  })
2418
2878
  ]
2419
2879
  }),
@@ -2614,6 +3074,13 @@ window.__ModuleLoader__.load({
2614
3074
  onClick: () => controller.toggleSecondary(),
2615
3075
  children: state.secondaryOpen ? "返回看板" : "其它任务"
2616
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
+ }),
2617
3084
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2618
3085
  type: "button",
2619
3086
  className: "dsh-atb-btn",
@@ -2714,10 +3181,138 @@ window.__ModuleLoader__.load({
2714
3181
  controller,
2715
3182
  task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
2716
3183
  }),
3184
+ state.diagOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsPanel, { controller }),
2717
3185
  alertEl
2718
3186
  ]
2719
3187
  });
2720
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
+ }
2721
3316
  /** Secondary tab: tasks grouped into canceled / archived / trashed columns. */
2722
3317
  function SecondaryTab({ controller, tasks }) {
2723
3318
  const trashed = tasks.filter((t) => t.trashedAt !== void 0);
@@ -2920,17 +3515,34 @@ window.__ModuleLoader__.load({
2920
3515
  injectStyles();
2921
3516
  const controller = new BoardController(createClient());
2922
3517
  const connection = ctx.get?.("connection");
2923
- if (connection !== void 0) controller.modelCatalog = async () => {
2924
- const response = await connection.api.llm.models({});
2925
- if (!response.result.ok) return [];
2926
- const out = [];
2927
- for (const group of response.result.value.groups) for (const model of group.models) out.push({
2928
- provider: group.id,
2929
- model: model.id,
2930
- name: model.name
2931
- });
2932
- return out;
2933
- };
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
+ }
2934
3546
  controller.installSessionJumper(createSessionJumper({
2935
3547
  getSessions: () => ctx.get?.("sessions"),
2936
3548
  getWorkspaces: () => ctx.get?.("workspaces")