dsh-plugin-teamflow 0.1.9 → 0.2.0

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
@@ -29,11 +29,15 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  //#region descriptors.ts
32
+ /** 恒等 parse:接受任意 JSON 值,原样返回。 */
33
+ const JSON_SCHEMA = { parse: (value) => value };
32
34
  /** 统一 strict codec(本插件所有参数/结果均为自由 JSON)。 */
33
35
  const strict = {
34
36
  mode: "strict",
35
37
  typeSymbol: "dsh-plugin-teamflow/types#Json",
36
- schema: { parse: (value) => value }
38
+ schema: JSON_SCHEMA,
39
+ /** 首次跨边界使用时物化 schema(宿主按需调用,只调一次并缓存)。 */
40
+ create: () => JSON_SCHEMA
37
41
  };
38
42
  const p = (name) => ({
39
43
  name,
@@ -334,6 +338,14 @@ window.__ModuleLoader__.load({
334
338
  }
335
339
  const stText = (s) => vocab("status", s);
336
340
  const runStatusText = (s) => vocab("runStatus", s);
341
+ /**
342
+ * **阶段**状态文案:只有 `cancelled` 与 backlog 词表分道。
343
+ *
344
+ * `stText` 是 backlog 卡片词表(`status.cancelled`/`closed`/`verified` 都译「已关闭」),阶段渲染若直接复用它,
345
+ * 被中断的阶段会显示成「已关闭」(en:Closed)——2026-09-16 中断功能实测截图里,同一屏 run 行写「已取消」、
346
+ * 阶段节点写「已关闭」。阶段是被**中止**而不是被关闭,故单独取词;其余阶段状态仍走同一张表(不开两套词表)。
347
+ */
348
+ const stageStatusText = (s) => s === "cancelled" ? t("stageStatus.cancelled") : stText(s);
337
349
  const kindTitle = (k) => vocab("kind", k);
338
350
  const roleName = (r) => vocab("role", r);
339
351
  /** 角色 chip(带图标;未知角色回退「⚙️ <raw>」)。 */
@@ -413,6 +425,7 @@ window.__ModuleLoader__.load({
413
425
  function stageLabelOf(s) {
414
426
  const raw = String(s && s.label || "");
415
427
  if (s && s.taskKey) return raw || String(s.taskKey);
428
+ if (s && phaseKeyOf(s.phase) === "dev" && raw) return raw;
416
429
  return (s && s.phase ? phaseNameOf(s.phase) : "") || raw;
417
430
  }
418
431
  const COLUMNS = {
@@ -527,6 +540,70 @@ window.__ModuleLoader__.load({
527
540
  }
528
541
  }, `${t("common.expandFull")}${more}`));
529
542
  }
543
+ /**
544
+ * 中断运行按钮(**两段式内联确认**):首次点击进入待确认态,3 秒内再点一次才真正执行。
545
+ *
546
+ * 为什么不自造对话框:客户端全目录 grep `confirm|dialog` 0 命中——目前没有宿主确认能力依赖,
547
+ * 内联两段式不引新依赖、也不改宿主能力面,且天然贴合「误点一个跑了几十分钟的 run」的防护诉求。
548
+ *
549
+ * 数据面由调用方给(`onConfirm(runId)`):会话内工作台与全局面板/右栏共用同一个 `teamflow/cancel`
550
+ * 方法,本件只负责待确认窗口、忙碌态与**点击不冒泡**(面板 run 行整行可点开详情)。
551
+ * `onConfirm` **必须自行消化错误**(它由调用方 try/catch 并落到可见的 err 提示);本件兜底只记 console,
552
+ * 不让点击处理里出现未处理的 rejection。
553
+ */
554
+ function CancelButton({ runId, label, title, onConfirm, style }) {
555
+ const [arm, setArm] = react.default.useState(false);
556
+ const [busy, setBusy] = react.default.useState(false);
557
+ react.default.useEffect(() => {
558
+ if (!arm) return void 0;
559
+ const timer = setTimeout(() => setArm(false), 3e3);
560
+ return () => clearTimeout(timer);
561
+ }, [arm]);
562
+ const onClick = async (e) => {
563
+ if (e && typeof e.stopPropagation === "function") e.stopPropagation();
564
+ if (busy) return;
565
+ if (!arm) {
566
+ setArm(true);
567
+ return;
568
+ }
569
+ setArm(false);
570
+ setBusy(true);
571
+ try {
572
+ await onConfirm(runId);
573
+ } catch (err) {
574
+ console.warn("[teamflow] cancel failed", err);
575
+ } finally {
576
+ setBusy(false);
577
+ }
578
+ };
579
+ const tone = busy ? {
580
+ opacity: .6,
581
+ cursor: "default"
582
+ } : arm ? {
583
+ background: T.error,
584
+ color: "#fff",
585
+ borderColor: T.error
586
+ } : null;
587
+ return h("button", {
588
+ onClick,
589
+ disabled: busy,
590
+ title: title || void 0,
591
+ style: {
592
+ font: "inherit",
593
+ fontSize: 12,
594
+ padding: "4px 12px",
595
+ borderRadius: 8,
596
+ cursor: "pointer",
597
+ border: `1px solid ${T.error}`,
598
+ background: "transparent",
599
+ color: T.error,
600
+ fontWeight: 600,
601
+ transition: "background .12s ease, color .12s ease",
602
+ ...style || {},
603
+ ...tone || {}
604
+ }
605
+ }, busy ? t("cancel.busy") : arm ? t("cancel.arm") : label || t("cancel.btn"));
606
+ }
530
607
  function fmtTime(tm) {
531
608
  if (!tm) return "—";
532
609
  const d = new Date(tm);
@@ -690,6 +767,7 @@ window.__ModuleLoader__.load({
690
767
  "runStatus.cancelled": "已取消",
691
768
  "runStatus.interrupted": "已中断",
692
769
  "runStatus.superseded": "已取代",
770
+ "stageStatus.cancelled": "已中止",
693
771
  "phase.prd": "PRD 产品需求",
694
772
  "phase.design": "UI/UX 设计",
695
773
  "phase.scaffold": "架构规划",
@@ -770,6 +848,13 @@ window.__ModuleLoader__.load({
770
848
  "workbench.history": "历史",
771
849
  "workbench.openRightBarTip": "在右侧栏打开该 run 详情(与任务夹产物并排看)",
772
850
  "workbench.openRightBarBtn": "⇥ 右栏打开",
851
+ "cancel.btn": "⏹ 中断",
852
+ "cancel.btnWithId": "⏹ 中断 #{id}",
853
+ "cancel.arm": "确认中断?",
854
+ "cancel.busy": "中断中…",
855
+ "cancel.sent": "已请求中断,等待当前阶段收尾…",
856
+ "cancel.tip": "中断 {id}\n立即中止当前阶段子代理;已完成阶段保留,之后可用「从断点重跑」继续;取消的 run 不产生提交",
857
+ "cancel.failed": "中断未生效:该流水线已不在运行中(可能刚结束)",
773
858
  "board.empty": "backlog 为空(还没有流水线运行过)",
774
859
  "board.dragReason": "看板拖拽流转",
775
860
  "board.cardTip": "{id} · {status}{summary}(点击查看详情)",
@@ -951,6 +1036,7 @@ window.__ModuleLoader__.load({
951
1036
  "runStatus.cancelled": "Cancelled",
952
1037
  "runStatus.interrupted": "Interrupted",
953
1038
  "runStatus.superseded": "Superseded",
1039
+ "stageStatus.cancelled": "Stopped",
954
1040
  "phase.prd": "PRD",
955
1041
  "phase.design": "UI/UX design",
956
1042
  "phase.scaffold": "Architecture",
@@ -1031,6 +1117,13 @@ window.__ModuleLoader__.load({
1031
1117
  "workbench.history": "History",
1032
1118
  "workbench.openRightBarTip": "Open this run's detail in the right sidebar (side by side with task-folder artifacts)",
1033
1119
  "workbench.openRightBarBtn": "⇥ Open in right bar",
1120
+ "cancel.btn": "⏹ Stop",
1121
+ "cancel.btnWithId": "⏹ Stop #{id}",
1122
+ "cancel.arm": "Confirm stop?",
1123
+ "cancel.busy": "Stopping…",
1124
+ "cancel.sent": "Stop requested — waiting for the current stage to wind down…",
1125
+ "cancel.tip": "Stop {id}\nAborts the subagent of the current stage immediately; finished stages are kept and can be resumed from the checkpoint later; a cancelled run never commits",
1126
+ "cancel.failed": "Stop had no effect: the pipeline is no longer running (it may have just finished)",
1034
1127
  "board.empty": "Backlog is empty (no pipeline has run yet)",
1035
1128
  "board.dragReason": "kanban drag transition",
1036
1129
  "board.cardTip": "{id} · {status}{summary} (click for details)",
@@ -1400,7 +1493,7 @@ window.__ModuleLoader__.load({
1400
1493
  } }, p.lastRequirement) : null, p.lastVerdict ? h("div", { style: { marginTop: 4 } }, chip(t("panel.verdict", { v: p.lastVerdict }), stColor(p.lastVerdict === "accepted" ? "accepted" : p.lastVerdict))) : null);
1401
1494
  })));
1402
1495
  }
1403
- function RunList({ runs, activeRunId, onOpenRun, onInlineRun }) {
1496
+ function RunList({ runs, activeRunId, onOpenRun, onInlineRun, onCancel }) {
1404
1497
  if (!runs.length) return muted(t("runList.empty"), { padding: "2px 2px 8px" });
1405
1498
  return h("div", { style: {
1406
1499
  display: "flex",
@@ -1453,7 +1546,15 @@ window.__ModuleLoader__.load({
1453
1546
  } }, h("span", null, t("runList.stageProgress", {
1454
1547
  done: r.doneStages,
1455
1548
  total: r.stageCount
1456
- })), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), h("button", {
1549
+ })), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), r.status === "running" && onCancel ? h(CancelButton, {
1550
+ runId: r.id,
1551
+ title: t("cancel.tip", { id: r.id }),
1552
+ onConfirm: onCancel,
1553
+ style: {
1554
+ fontSize: 11,
1555
+ padding: "2px 9px"
1556
+ }
1557
+ }) : null, h("button", {
1457
1558
  style: brandBtn,
1458
1559
  title: t("runList.openRightBarTip"),
1459
1560
  onClick: (e) => {
@@ -1796,7 +1897,7 @@ window.__ModuleLoader__.load({
1796
1897
  color: T.text2
1797
1898
  } }, det.events.slice(-30).map((e, i) => h("div", { key: i }, `${fmtTime(e.at)} ${e.from || "—"} → ${e.to || "—"}${e.by ? ` · ${e.by}` : ""}${e.reason ? ` · ${e.reason}` : ""}`)))) : null);
1798
1899
  }
1799
- function RunDetailPane({ snap, product, api }) {
1900
+ function RunDetailPane({ snap, product, api, onCancel }) {
1800
1901
  const [sel, setSel] = react.default.useState(null);
1801
1902
  const [err, setErr] = react.default.useState(null);
1802
1903
  react.default.useEffect(() => {
@@ -1850,7 +1951,16 @@ window.__ModuleLoader__.load({
1850
1951
  fontSize: 10,
1851
1952
  color: T.text2,
1852
1953
  fontFamily: MONO
1853
- } }, product) : null), h("div", { style: {
1954
+ } }, product) : null, snap.status === "running" && onCancel ? h(CancelButton, {
1955
+ runId: snap.id,
1956
+ title: t("cancel.tip", { id: snap.id }),
1957
+ onConfirm: onCancel,
1958
+ style: {
1959
+ marginLeft: "auto",
1960
+ fontSize: 10.5,
1961
+ padding: "2px 9px"
1962
+ }
1963
+ }) : null), h("div", { style: {
1854
1964
  fontSize: 12,
1855
1965
  color: T.text,
1856
1966
  lineHeight: 1.5
@@ -1902,7 +2012,7 @@ window.__ModuleLoader__.load({
1902
2012
  fontSize: 11.5,
1903
2013
  color: T.text,
1904
2014
  fontWeight: 500
1905
- } }, stageLabelOf(s)), chip(stText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
2015
+ } }, stageLabelOf(s)), chip(stageStatusText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" && s.outcome !== "cancelled" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
1906
2016
  marginLeft: "auto",
1907
2017
  fontFamily: MONO,
1908
2018
  fontSize: 10,
@@ -1938,7 +2048,7 @@ window.__ModuleLoader__.load({
1938
2048
  fontSize: 11.5,
1939
2049
  fontWeight: 700,
1940
2050
  color: T.text
1941
- } }, t("detail.stageTitle", { seq: sel.seq })), chip(stText(sel.status), stColor(sel.status))), h("button", {
2051
+ } }, t("detail.stageTitle", { seq: sel.seq })), chip(stageStatusText(sel.status), stColor(sel.status))), h("button", {
1942
2052
  style: panelBtn,
1943
2053
  onClick: () => setSel(null)
1944
2054
  }, t("common.collapse"))), h("div", { style: {
@@ -1991,7 +2101,7 @@ window.__ModuleLoader__.load({
1991
2101
  fontFamily: MONO,
1992
2102
  color: T.text2
1993
2103
  }
1994
- }, h("span", null, `#${a.seq}`), chip(stText(a.status), stColor(a.status)), a.outcome ? h("span", null, a.outcome) : null, h("span", { style: { marginLeft: "auto" } }, `${fmtTime(a.startedAt)} · ${fmtDur(a.startedAt, a.endedAt)}`))))) : null) : null, logs.length ? h("div", null, sectionTitle(t("detail.logs", { n: logs.length })), h("div", { style: {
2104
+ }, h("span", null, `#${a.seq}`), chip(stageStatusText(a.status), stColor(a.status)), a.outcome && a.outcome !== "cancelled" ? h("span", null, a.outcome) : null, h("span", { style: { marginLeft: "auto" } }, `${fmtTime(a.startedAt)} · ${fmtDur(a.startedAt, a.endedAt)}`))))) : null) : null, logs.length ? h("div", null, sectionTitle(t("detail.logs", { n: logs.length })), h("div", { style: {
1995
2105
  display: "flex",
1996
2106
  flexDirection: "column",
1997
2107
  gap: 2,
@@ -2078,6 +2188,16 @@ window.__ModuleLoader__.load({
2078
2188
  nonce,
2079
2189
  readTab
2080
2190
  ]);
2191
+ /** 中断后重新拉快照(nonce 触发上面那个 effect 重跑;右栏 tab 是会话级地址,与产品线选择无关)。 */
2192
+ const onCancel = async (rid) => {
2193
+ if (!api) return;
2194
+ try {
2195
+ await api.cancel(rid);
2196
+ setNonce((n) => n + 1);
2197
+ } catch (e) {
2198
+ setErr(String(e && e.message || e));
2199
+ }
2200
+ };
2081
2201
  if (err && !snap) {
2082
2202
  const pending = !address && !!readTab;
2083
2203
  return h("div", { style: {
@@ -2097,7 +2217,8 @@ window.__ModuleLoader__.load({
2097
2217
  return h(RunDetailPane, {
2098
2218
  snap,
2099
2219
  product,
2100
- api
2220
+ api,
2221
+ onCancel
2101
2222
  });
2102
2223
  }
2103
2224
  function productApi(remote, product) {
@@ -2105,7 +2226,13 @@ window.__ModuleLoader__.load({
2105
2226
  view: async () => unwrap$1(await remote.productView(product), "productView"),
2106
2227
  runDetail: async (runId) => unwrap$1(await remote.productRunDetail(product, runId), "productRunDetail"),
2107
2228
  stageDetail: async (runId, seq) => unwrap$1(await remote.productStageDetail(product, runId, seq), "productStageDetail"),
2108
- itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail")
2229
+ itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail"),
2230
+ /** 中断运行:runId 全局寻址(与产品线无关),放这里是为了让面板行/详情/右栏共用同一处解包与报错。 */
2231
+ cancel: async (runId) => {
2232
+ const r = unwrap$1(await remote.cancel(runId), "cancel");
2233
+ if (!r || r.ok !== true) throw new Error(t("cancel.failed"));
2234
+ return true;
2235
+ }
2109
2236
  };
2110
2237
  }
2111
2238
  /** 全局面板 props:root scope 标准 props(useSessions 取当前会话)+ 插件注入(remote/打开回调)。 */
@@ -2165,8 +2292,10 @@ window.__ModuleLoader__.load({
2165
2292
  /**
2166
2293
  * **跳到资源所属的会话,再在那个会话的右栏打开**(全局面板的正确语义)。
2167
2294
  * 右侧栏是会话级的:从全局面板看 tetris 的 run 却把 tab 挂到"用户当前所在会话"上没有意义
2168
- * (用户 2026-09-11 提出)。所以先 `sessions.open(ownerSession)`,等当前会话真的切过去、
2169
- * 且对话 seat 挂载 bind 之后再 openResource(两者都要等,故小步重试 + 就绪判据)。
2295
+ * (用户 2026-09-11 提出)。所以先 `uiWorkspace.openSession(ownerSession)`,再小步重试等右栏 seat 就绪后 openResource。
2296
+ * **2026-09-23 迁移(宿主 0.1.7-alpha.1)**:`sessions.open` `sessions.openSubagent` 同批被移除,
2297
+ * 跳会话唯一入口改为 `uiWorkspace.openSession(target)`;同时 `SessionListState` 已无 `current` 字段,
2298
+ * 故原先"等当前会话真的切过去"的判据删除(契约变更后它恒为 undefined,等于死代码),只留时间维度的重试。
2170
2299
  * @param target.ownerSession - 资源所属会话(run 的发起会话 / 产物地址里的会话)
2171
2300
  * @param target.address - host 生成的 dsh-resource 地址
2172
2301
  * @param target.label - 提示用的名字
@@ -2174,13 +2303,13 @@ window.__ModuleLoader__.load({
2174
2303
  */
2175
2304
  const goOwnerSessionAndOpen = (target) => {
2176
2305
  const { ownerSession, address, label, fallback } = target || {};
2177
- const sessions = props.sessions;
2178
- if (!ownerSession || !sessions || typeof sessions.open !== "function") {
2306
+ const ws = props.uiWorkspace;
2307
+ if (!ownerSession || !ws || typeof ws.openSession !== "function") {
2179
2308
  openInConversationRightbar(address, label, fallback);
2180
2309
  return;
2181
2310
  }
2182
2311
  try {
2183
- sessions.open(ownerSession);
2312
+ ws.openSession(ownerSession);
2184
2313
  } catch (e) {
2185
2314
  setHint(t("panel.hintSessionGone", { sid: String(ownerSession).slice(0, 8) }));
2186
2315
  if (fallback) fallback();
@@ -2192,13 +2321,7 @@ window.__ModuleLoader__.load({
2192
2321
  let tries = 0;
2193
2322
  const tick = () => {
2194
2323
  tries += 1;
2195
- let nowCurrent = null;
2196
- try {
2197
- nowCurrent = sessions.list && sessions.list.getSnapshot ? sessions.list.getSnapshot().current : null;
2198
- } catch (e) {
2199
- nowCurrent = null;
2200
- }
2201
- if ((nowCurrent === ownerSession || tries >= 6) && props.openResource && address && props.openResource(address, label, tries < 6)) return;
2324
+ if (props.openResource && address && props.openResource(address, label, tries < 6)) return;
2202
2325
  if (tries < 14) {
2203
2326
  setTimeout(tick, 130);
2204
2327
  return;
@@ -2328,6 +2451,25 @@ window.__ModuleLoader__.load({
2328
2451
  }
2329
2452
  };
2330
2453
  const closeDetail = () => setDetail(null);
2454
+ /** 中断运行(面板内两个入口共用):成功后刷新产品线视图;详情浮层开着就顺手把快照也换新。 */
2455
+ const cancelRun = async (runId) => {
2456
+ if (!api) return;
2457
+ try {
2458
+ await api.cancel(runId);
2459
+ loadView(state.current, true);
2460
+ if (detail && detail.kind === "run" && detail.data && detail.data.id === runId) setDetail({
2461
+ kind: "run",
2462
+ data: await api.runDetail(runId),
2463
+ run: detail.run
2464
+ });
2465
+ setHint(t("cancel.sent"));
2466
+ } catch (e) {
2467
+ setState((s) => ({
2468
+ ...s,
2469
+ err: String(e && e.message || e)
2470
+ }));
2471
+ }
2472
+ };
2331
2473
  const openRun = (r) => {
2332
2474
  goOwnerSessionAndOpen({
2333
2475
  ownerSession: r.ownerSession,
@@ -2516,7 +2658,8 @@ window.__ModuleLoader__.load({
2516
2658
  runs: visibleRuns,
2517
2659
  activeRunId: detail && detail.kind === "run" && detail.run ? detail.run.id : null,
2518
2660
  onOpenRun: openRun,
2519
- onInlineRun: showInline
2661
+ onInlineRun: showInline,
2662
+ onCancel: cancelRun
2520
2663
  }) : muted(t("panel.emptyRunFilter"), { fontSize: 10.5 }), muted(t("panel.runHint"), {
2521
2664
  fontSize: 10,
2522
2665
  marginTop: 8
@@ -2570,7 +2713,8 @@ window.__ModuleLoader__.load({
2570
2713
  }, t("common.close")))), h(RunDetailPane, {
2571
2714
  snap: detail && detail.data,
2572
2715
  product: state.current,
2573
- api
2716
+ api,
2717
+ onCancel: cancelRun
2574
2718
  }))) : null));
2575
2719
  }
2576
2720
  //#endregion
@@ -2592,7 +2736,7 @@ window.__ModuleLoader__.load({
2592
2736
  const inject = [
2593
2737
  "remote",
2594
2738
  "slots",
2595
- "sessions",
2739
+ "uiWorkspace",
2596
2740
  "locale"
2597
2741
  ];
2598
2742
  const NODE_W = 300;
@@ -2613,7 +2757,7 @@ window.__ModuleLoader__.load({
2613
2757
  let maxH = 0;
2614
2758
  groups.forEach((g, i) => {
2615
2759
  const anyRun = g.stages.some((s) => s.status === "running");
2616
- const anyFail = g.stages.some((s) => s.status === "failed" || s.status === "needs-human" || s.status === "cancelled");
2760
+ const anyFail = g.stages.some((s) => s.status === "failed" || s.status === "needs-human");
2617
2761
  const allDone = g.stages.length > 0 && g.stages.every((s) => s.status === "done");
2618
2762
  const headColor = anyRun ? T.brand : anyFail ? T.error : allDone ? T.success : T.text2;
2619
2763
  const h = 48 + g.stages.reduce((a, s) => a + cardH(s), 0) + Math.max(0, g.stages.length - 1) * 7;
@@ -2725,7 +2869,7 @@ window.__ModuleLoader__.load({
2725
2869
  lineHeight: "15px",
2726
2870
  flex: "0 0 auto"
2727
2871
  }
2728
- }, `↻${s.attempts.length - 1}`) : null, chip(stText(s.status), color, { dot: true }), h("span", { style: {
2872
+ }, `↻${s.attempts.length - 1}`) : null, chip(stageStatusText(s.status), color, { dot: true }), h("span", { style: {
2729
2873
  color: T.text2,
2730
2874
  fontSize: 11,
2731
2875
  opacity: .5
@@ -2838,7 +2982,7 @@ window.__ModuleLoader__.load({
2838
2982
  /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。
2839
2983
  * 2026-09-06 状态机化:同任务多次尝试 → 顶部尝试时间线 + 选中展开(默认最新);
2840
2984
  * 单次尝试保持现状(不渲染时间线)。 */
2841
- function StageDetailDrawer({ det, onClose, sessionId, sessions }) {
2985
+ function StageDetailDrawer({ det, onClose, sessionId, uiWorkspace }) {
2842
2986
  const [sel, setSel] = react.default.useState(null);
2843
2987
  const st = det.stage;
2844
2988
  const d = det.data;
@@ -2849,11 +2993,11 @@ window.__ModuleLoader__.load({
2849
2993
  const ownerSession = d && d.ownerSession ? String(d.ownerSession) : null;
2850
2994
  const mySession = sessionId ? String(sessionId) : null;
2851
2995
  const crossSession = !!ownerSession && !!mySession && ownerSession !== mySession;
2852
- const hasChild = !!(!crossSession && cur && cur.childId && sessions && typeof sessions.openSubagent === "function");
2996
+ const hasChild = !!(!crossSession && cur && cur.childId && uiWorkspace && typeof uiWorkspace.openSession === "function");
2853
2997
  const openChild = () => {
2854
2998
  if (crossSession || !hasChild) return;
2855
2999
  try {
2856
- sessions.openSubagent({
3000
+ uiWorkspace.openSession({
2857
3001
  parentSessionId: ownerSession || sessionId,
2858
3002
  childSessionId: cur.childId,
2859
3003
  mode: "one-shot"
@@ -2928,7 +3072,7 @@ window.__ModuleLoader__.load({
2928
3072
  marginTop: 1,
2929
3073
  fontFamily: MONO,
2930
3074
  fontVariantNumeric: "tabular-nums"
2931
- } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stText(st.status), color, { dot: true }) : null, h("button", {
3075
+ } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stageStatusText(st.status), color, { dot: true }) : null, h("button", {
2932
3076
  onClick: onClose,
2933
3077
  style: closeBtn,
2934
3078
  title: t("common.close")
@@ -2993,7 +3137,7 @@ window.__ModuleLoader__.load({
2993
3137
  color: aColor,
2994
3138
  flex: "0 0 64px",
2995
3139
  fontWeight: 700
2996
- } }, a.status === "done" ? t("stage.attemptDone") : a.status === "failed" ? t("stage.attemptFailed", { outcome: a.outcome || t("common.failed") }) : t("stage.attemptRunning")), h("span", { style: {
3140
+ } }, a.status === "done" ? t("stage.attemptDone") : a.status === "failed" ? t("stage.attemptFailed", { outcome: a.outcome || t("common.failed") }) : a.status === "cancelled" ? t("stageStatus.cancelled") : t("stage.attemptRunning")), h("span", { style: {
2997
3141
  flex: 1,
2998
3142
  minWidth: 0,
2999
3143
  fontSize: 10.5,
@@ -3099,7 +3243,7 @@ window.__ModuleLoader__.load({
3099
3243
  overflowY: "auto"
3100
3244
  } }, outText))));
3101
3245
  }
3102
- function PipelinePanel({ active, api, runId, sessionId, sessions }) {
3246
+ function PipelinePanel({ active, api, runId, sessionId, uiWorkspace }) {
3103
3247
  if (!active) return h("div", { style: {
3104
3248
  color: T.text2,
3105
3249
  fontSize: 13,
@@ -3438,7 +3582,7 @@ window.__ModuleLoader__.load({
3438
3582
  det,
3439
3583
  onClose: closeDet,
3440
3584
  sessionId,
3441
- sessions
3585
+ uiWorkspace
3442
3586
  }) : null);
3443
3587
  }
3444
3588
  function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun, openArtifact }) {
@@ -4456,6 +4600,26 @@ window.__ModuleLoader__.load({
4456
4600
  setBusy(false);
4457
4601
  };
4458
4602
  const anyRunning = runs.some((r) => r.status === "running" || r.status === "pending");
4603
+ const runningRun = !!(activeRun && activeRun.status === "running");
4604
+ /** 中断请求已发出:按钮先隐藏,等 2s 轮询把状态刷成 cancelled(避免重复点出「未生效」提示)。 */
4605
+ const [cancelSentFor, setCancelSentFor] = react.default.useState(null);
4606
+ react.default.useEffect(() => {
4607
+ setCancelSentFor(null);
4608
+ }, [activeRun && activeRun.id, activeRun && activeRun.status]);
4609
+ const onCancel = async (id) => {
4610
+ if (!api) return;
4611
+ try {
4612
+ const r = unwrap(await api.cancel(id), "cancel");
4613
+ if (!r || r.ok !== true) throw new Error(t("cancel.failed"));
4614
+ setCancelSentFor(id);
4615
+ refresh();
4616
+ } catch (e) {
4617
+ setState((s) => ({
4618
+ ...s,
4619
+ err: String(e && e.message || e)
4620
+ }));
4621
+ }
4622
+ };
4459
4623
  const btn = {
4460
4624
  font: "inherit",
4461
4625
  fontSize: 12,
@@ -4545,7 +4709,12 @@ window.__ModuleLoader__.load({
4545
4709
  alignItems: "center",
4546
4710
  gap: 5
4547
4711
  }
4548
- }, t("workbench.refresh")), canResume ? h("button", {
4712
+ }, t("workbench.refresh")), runningRun && cancelSentFor !== activeRun.id ? h(CancelButton, {
4713
+ runId: activeRun.id,
4714
+ label: t("cancel.btnWithId", { id: String(activeRun.id).slice(-6) }),
4715
+ title: t("cancel.tip", { id: activeRun.id }),
4716
+ onConfirm: onCancel
4717
+ }) : null, canResume ? h("button", {
4549
4718
  onClick: onResume,
4550
4719
  disabled: busy,
4551
4720
  title: t("workbench.resumeTip", {
@@ -4690,7 +4859,7 @@ window.__ModuleLoader__.load({
4690
4859
  api,
4691
4860
  runId: activeRun ? activeRun.id : null,
4692
4861
  sessionId: props.sessionId,
4693
- sessions: props.sessions
4862
+ uiWorkspace: props.uiWorkspace
4694
4863
  }) : h(BoardPanel, {
4695
4864
  backlog,
4696
4865
  api,
@@ -4769,7 +4938,7 @@ window.__ModuleLoader__.load({
4769
4938
  locale: NS,
4770
4939
  inject: () => ({
4771
4940
  remote: teamflow,
4772
- sessions: ctx.get("sessions"),
4941
+ uiWorkspace: ctx.get("uiWorkspace"),
4773
4942
  layout: ctx.get("layout"),
4774
4943
  openResource: openResourceSafe,
4775
4944
  openArtifact
@@ -4800,7 +4969,7 @@ window.__ModuleLoader__.load({
4800
4969
  inject: (sessionId) => ({
4801
4970
  sessionId,
4802
4971
  remote: teamflow,
4803
- sessions: ctx.get("sessions"),
4972
+ uiWorkspace: ctx.get("uiWorkspace"),
4804
4973
  openArtifact,
4805
4974
  openResource: openResourceSafe
4806
4975
  })
@@ -1,9 +1,13 @@
1
1
  //#region descriptors.ts
2
+ /** 恒等 parse:接受任意 JSON 值,原样返回。 */
3
+ const JSON_SCHEMA = { parse: (value) => value };
2
4
  /** 统一 strict codec(本插件所有参数/结果均为自由 JSON)。 */
3
5
  const strict = {
4
6
  mode: "strict",
5
7
  typeSymbol: "dsh-plugin-teamflow/types#Json",
6
- schema: { parse: (value) => value }
8
+ schema: JSON_SCHEMA,
9
+ /** 首次跨边界使用时物化 schema(宿主按需调用,只调一次并缓存)。 */
10
+ create: () => JSON_SCHEMA
7
11
  };
8
12
  const p = (name) => ({
9
13
  name,