dsh-plugin-teamflow 0.1.7 → 0.1.8

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
@@ -218,6 +218,60 @@ window.__ModuleLoader__.load({
218
218
  p("sessionId")
219
219
  ],
220
220
  result: strict
221
+ },
222
+ {
223
+ id: "dsh-plugin-teamflow#teamflow/products",
224
+ service: "teamflow",
225
+ namespace: "teamflow",
226
+ method: "products",
227
+ invocation: { kind: "direct" },
228
+ parameters: [p("sessionId")],
229
+ result: strict
230
+ },
231
+ {
232
+ id: "dsh-plugin-teamflow#teamflow/productView",
233
+ service: "teamflow",
234
+ namespace: "teamflow",
235
+ method: "productView",
236
+ invocation: { kind: "direct" },
237
+ parameters: [p("product")],
238
+ result: strict
239
+ },
240
+ {
241
+ id: "dsh-plugin-teamflow#teamflow/productRunDetail",
242
+ service: "teamflow",
243
+ namespace: "teamflow",
244
+ method: "productRunDetail",
245
+ invocation: { kind: "direct" },
246
+ parameters: [p("product"), p("runId")],
247
+ result: strict
248
+ },
249
+ {
250
+ id: "dsh-plugin-teamflow#teamflow/productStageDetail",
251
+ service: "teamflow",
252
+ namespace: "teamflow",
253
+ method: "productStageDetail",
254
+ invocation: { kind: "direct" },
255
+ parameters: [
256
+ p("product"),
257
+ p("runId"),
258
+ p("seq")
259
+ ],
260
+ result: strict
261
+ },
262
+ {
263
+ id: "dsh-plugin-teamflow#teamflow/productItemDetail",
264
+ service: "teamflow",
265
+ namespace: "teamflow",
266
+ method: "productItemDetail",
267
+ invocation: { kind: "direct" },
268
+ parameters: [
269
+ p("product"),
270
+ p("kind"),
271
+ p("id"),
272
+ p("sessionId")
273
+ ],
274
+ result: strict
221
275
  }
222
276
  ]);
223
277
  /** client 端 $mount 使用的贡献对象。 */
@@ -226,27 +280,14 @@ window.__ModuleLoader__.load({
226
280
  descriptors: TEAMFLOW_DESCRIPTORS
227
281
  });
228
282
  //#endregion
229
- //#region client/index.tsx
283
+ //#region client/shared.tsx
230
284
  /**
231
- * dsh-plugin-teamflow — browser half(v0.7 视觉重构:「精密工程控制台」)。
232
- *
233
- * 注册 conversation.view tab「🏭 团队工作台」(与 chat / 轨迹并列):
234
- * - 流水线图形工作流:阶段泳道 + 节点卡片(状态/耗时/token/子代理会话)+ 实时轮询
235
- * - Backlog 拖拽看板:需求/任务/缺陷三组状态泳道,卡片拖拽流转(原生 HTML5 DnD,零依赖)
236
- * - 成本中心:每阶段 token + 总计 + 运行时长
237
- * - 人工介入中心:needs-human 项聚合 + 一键终态
238
- * - 历史 run 切换
285
+ * dsh-plugin-teamflow — browser half 共享展示层(主题 token / 状态词表 / 格式化)。
239
286
  *
240
- * 视觉:全部使用 DSH 主题 token(--dsw-alias-*,自动适配深浅色)。
241
- * Remote 接入:$mount ctx.get('remote.teamflow') 取全局实例传给组件
242
- * (不能 inject 'remote.teamflow'——自挂载自注入会让 fiber 死锁)。
287
+ * 会话内工作台(index.tsx)与全局面板(panel.tsx)共用:**只放无状态纯展示件**,
288
+ * 不放任何 Remote 调用或会话上下文逻辑——两边数据来源不同(sessionId / productKey),
289
+ * 展示语言必须一致。
243
290
  */
244
- const inject = [
245
- "remote",
246
- "slots",
247
- "sessions",
248
- "locale"
249
- ];
250
291
  const T = {
251
292
  bg: "var(--dsw-alias-bg-base)",
252
293
  layer1: "var(--dsw-alias-bg-layer-1)",
@@ -391,7 +432,7 @@ window.__ModuleLoader__.load({
391
432
  gap: 6,
392
433
  flexWrap: "wrap"
393
434
  };
394
- /** 状态徽章:半透明底 + 状态色文字 + 圆角 pill */
435
+ /** 状态徽章:半透明底 + 状态色文字 + 圆角 pill(超宽可省略:窄列/窄卡里不顶破外层)。 */
395
436
  const chip = (text, color, opts = {}) => h("span", { style: {
396
437
  display: "inline-flex",
397
438
  alignItems: "center",
@@ -404,13 +445,17 @@ window.__ModuleLoader__.load({
404
445
  background: `color-mix(in srgb, ${color} 14%, transparent)`,
405
446
  color,
406
447
  whiteSpace: "nowrap",
448
+ maxWidth: "100%",
449
+ overflow: "hidden",
450
+ textOverflow: "ellipsis",
407
451
  ...opts.style || {}
408
452
  } }, opts.dot ? h("span", { style: {
409
453
  width: 5,
410
454
  height: 5,
411
455
  borderRadius: 999,
412
456
  background: color,
413
- display: "inline-block"
457
+ display: "inline-block",
458
+ flex: "0 0 auto"
414
459
  } }) : null, text);
415
460
  /** 可折叠长文本:默认只显示前几行预览,「展开全文」/「收起」双向切换(数据不动,纯展示层——summary/需求原文等富文本不再铺满抽屉)。 */
416
461
  function FoldableText({ text, charLimit = 280, lineLimit = 5, style }) {
@@ -459,97 +504,1496 @@ window.__ModuleLoader__.load({
459
504
  padding: 0,
460
505
  cursor: "pointer"
461
506
  }
462
- }, `展开全文${more}`));
463
- }
464
- function fmtDur(a, b) {
465
- if (!a) return "";
466
- const ms = (b || Date.now()) - a;
467
- if (ms < 0) return "";
468
- const s = Math.floor(ms / 1e3);
469
- if (s < 60) return `${s}s`;
470
- return `${Math.floor(s / 60)}m${s % 60}s`;
471
- }
472
- function fmtTokens(n) {
473
- if (n === null || n === void 0) return "";
474
- if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
475
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
476
- return String(n);
477
- }
478
- /** 官方口径计费合计(billed input + output)。 */
479
- function totalTokens(u) {
480
- return (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0);
507
+ }, `展开全文${more}`));
508
+ }
509
+ function fmtTime(t) {
510
+ if (!t) return "";
511
+ const d = new Date(t);
512
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
513
+ }
514
+ function fmtDur(a, b) {
515
+ if (!a) return "";
516
+ const ms = (b || Date.now()) - a;
517
+ if (ms < 0) return "";
518
+ const s = Math.floor(ms / 1e3);
519
+ if (s < 60) return `${s}s`;
520
+ return `${Math.floor(s / 60)}m${s % 60}s`;
521
+ }
522
+ function fmtTokens(n) {
523
+ if (n === null || n === void 0) return "";
524
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
525
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
526
+ return String(n);
527
+ }
528
+ /** 官方口径计费合计(billed input + output)。 */
529
+ function totalTokens(u) {
530
+ return (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0);
531
+ }
532
+ /** 官方口径工具函数(与 DeepSeek usage 账单对齐,零额外概念):
533
+ * - u.input : 输入(缓存未命中)
534
+ * - u.cacheRead : 输入(缓存命中)
535
+ * - u.cacheWrite : 输入写入缓存
536
+ * - u.output : 输出
537
+ * billed input = input + cacheRead + cacheWrite;命中率 = cacheRead / (input + cacheRead)。
538
+ */
539
+ function hitRate(u) {
540
+ const total = (u.input || 0) + (u.cacheRead || 0);
541
+ return total > 0 ? Math.round((u.cacheRead || 0) / total * 100) : null;
542
+ }
543
+ function usageDetail(s) {
544
+ const k = (n) => n === null || n === void 0 ? "—" : fmtTokens(n);
545
+ if (s.usage) {
546
+ const u = s.usage;
547
+ const hit = hitRate(u);
548
+ return `输入(未命中) ${k(u.input)} / 输入(命中) ${k(u.cacheRead)} / 写缓存 ${k(u.cacheWrite)} / 输出 ${k(u.output)} · ${u.calls} 次调用${hit !== null ? ` · 缓存命中 ${hit}%` : ""}`;
549
+ }
550
+ return "无 usage 明细";
551
+ }
552
+ /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
553
+ function stageUsageLine(s) {
554
+ const u = s && s.usage;
555
+ if (u && (u.input || u.cacheRead || u.cacheWrite || u.output)) {
556
+ const hit = hitRate(u);
557
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""}`;
558
+ }
559
+ return null;
560
+ }
561
+ const ROLE_NAME = {
562
+ pm: "产品",
563
+ design: "设计",
564
+ arch: "架构",
565
+ tech: "方案",
566
+ dev: "开发",
567
+ qa: "测试",
568
+ acceptance: "验收",
569
+ other: "其他"
570
+ };
571
+ const roleUsage = (u) => {
572
+ if (!u) return "";
573
+ const hit = hitRate(u);
574
+ return `⇅${fmtTokens(u.input || 0)}/${fmtTokens(u.cacheRead || 0)}·⬆${fmtTokens(u.output || 0)}${hit !== null ? `·${hit}%` : ""}`;
575
+ };
576
+ /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
577
+ function byRoleLine(task) {
578
+ const roles = task && task.byRole || {};
579
+ return Object.keys(roles).filter((k) => roles[k] && roles[k].input + roles[k].output + roles[k].cacheRead + roles[k].cacheWrite > 0).map((k) => `${ROLE_NAME[k] || k} ${roleUsage(roles[k])}`).join(" · ");
580
+ }
581
+ /** 多阶段 usage 汇总(官方口径)。 */
582
+ function totalUsage(stages) {
583
+ const t = {
584
+ input: 0,
585
+ cacheRead: 0,
586
+ cacheWrite: 0,
587
+ output: 0,
588
+ calls: 0
589
+ };
590
+ for (const s of stages || []) {
591
+ const u = s && s.usage;
592
+ if (!u) continue;
593
+ t.input += u.input || 0;
594
+ t.cacheRead += u.cacheRead || 0;
595
+ t.cacheWrite += u.cacheWrite || 0;
596
+ t.output += u.output || 0;
597
+ t.calls += u.calls || 0;
598
+ }
599
+ return t;
600
+ }
601
+ const stText = (s) => STATUS_TEXT[s] || s;
602
+ const stColor = (s) => STATUS_COLOR[s] || T.text2;
603
+ //#endregion
604
+ //#region client/panel.tsx
605
+ /**
606
+ * dsh-plugin-teamflow — 全局面板 + 右栏 run 详情 tab(v0.1.8 ①)。
607
+ *
608
+ * 与 conversation.view tab(会话内工作台)的分工:
609
+ * - 全局面板挂在**应用外壳**的 root slot 上:`sidebar.panellist`(侧边栏图标)+
610
+ * `main`(中央主区,keyed,**必须与 panellist 同 id**——宿主 `layout.selectPanel`
611
+ * 对未注册的 main key 直接抛错)。它没有会话上下文,因此数据面按**产品线 key**
612
+ * 寻址(`remote.products/productView/...`),跨会话可用。
613
+ * - 右栏 run 详情 tab:tab 类型进 `ctx.sidebarRightTabs`,正文进 keyed seat
614
+ * `sidebar.right.pane.tab`;地址由 host 生成(`dsh-resource://teamflow/run/<产品线>/<runId>`),
615
+ * 与产物预览同一条原则——client 不拼地址。右侧栏是 AppFrame 级 root slot,
616
+ * 全局面板占着中央区时它依然挂载(唯一失败态是"没有挂载会话",此时降级为面板内联详情)。
617
+ */
618
+ const RUN_TAB_ID = "dsh-plugin-teamflow/run";
619
+ const RUN_TAB_KIND = "teamflow-run";
620
+ const RUN_TAB_PATTERN = "dsh-resource://teamflow/run/**";
621
+ /** 解析 host 生成的 run 地址 → { product, runId }(不合法 → null)。 */
622
+ function parseRunAddress(address) {
623
+ const m = /^dsh-resource:\/\/teamflow\/run\/([^/]+)\/([^/?#]+)$/.exec(String(address || ""));
624
+ if (!m) return null;
625
+ try {
626
+ return {
627
+ product: decodeURIComponent(m[1]),
628
+ runId: decodeURIComponent(m[2])
629
+ };
630
+ } catch (e) {
631
+ return null;
632
+ }
633
+ }
634
+ /** 右栏 tab 类型定义(类型本身;正文在 sidebar.right.pane.tab 的 keyed seat)。 */
635
+ function runTabDefinition() {
636
+ return {
637
+ id: RUN_TAB_ID,
638
+ kind: RUN_TAB_KIND,
639
+ priority: "extension",
640
+ patterns: [RUN_TAB_PATTERN],
641
+ title: (address) => {
642
+ const p = parseRunAddress(address);
643
+ return p ? `TeamFlow · ${p.runId.slice(0, 14)}` : "TeamFlow Run";
644
+ }
645
+ };
646
+ }
647
+ /** Typert remote 信封解包(与会话内工作台同一约定)。
648
+ * 空信封(`{ok:true}` 但没有 `value`)是**真实故障态**:host 方法返回了 `undefined` / 结果被丢弃——
649
+ * 此时不能静默返回 undefined(调用方会把它再包成「未知错误」,丢掉定位信息),故显式报出原始信封。 */
650
+ function unwrap$1(res, what) {
651
+ if (!res || !res.ok) {
652
+ let detail = "未知错误";
653
+ try {
654
+ detail = res && res.error && (res.error.message || res.error.code) || JSON.stringify(res) || "未知错误";
655
+ } catch (e) {}
656
+ try {
657
+ console.warn("[teamflow] remote 调用失败", what, res);
658
+ } catch (e) {}
659
+ throw new Error(`${what || "remote"} 调用失败:${detail}`);
660
+ }
661
+ if (res.value === void 0) {
662
+ try {
663
+ console.warn("[teamflow] remote 返回空信封(ok 但无 value)", what, res);
664
+ } catch (e) {}
665
+ let raw = "";
666
+ try {
667
+ raw = JSON.stringify(res);
668
+ } catch (e) {
669
+ raw = String(res);
670
+ }
671
+ throw new Error(`${what || "remote"} 返回空结果(ok=true 但无 value;原始信封=${raw})`);
672
+ }
673
+ return res.value;
674
+ }
675
+ /** 全局面板图标:三条泳道 + 节点(与工作台「流水线」语义一致),跟随选中态配色。 */
676
+ function TeamflowPanelIcon({ size = 16, active = false }) {
677
+ return h("svg", {
678
+ width: size,
679
+ height: size,
680
+ viewBox: "0 0 24 24",
681
+ fill: "none",
682
+ stroke: active ? "currentColor" : "currentColor",
683
+ strokeWidth: 1.6,
684
+ strokeLinecap: "round",
685
+ strokeLinejoin: "round",
686
+ "aria-hidden": "true",
687
+ focusable: "false"
688
+ }, h("rect", {
689
+ x: 3,
690
+ y: 4,
691
+ width: 5.2,
692
+ height: 4,
693
+ rx: 1.2,
694
+ opacity: active ? 1 : .85
695
+ }), h("rect", {
696
+ x: 9.4,
697
+ y: 10,
698
+ width: 5.2,
699
+ height: 4,
700
+ rx: 1.2,
701
+ opacity: active ? 1 : .85
702
+ }), h("rect", {
703
+ x: 3,
704
+ y: 16,
705
+ width: 5.2,
706
+ height: 4,
707
+ rx: 1.2,
708
+ opacity: active ? 1 : .85
709
+ }), h("path", { d: "M8.4 6h3.3a1 1 0 0 1 1 1v3.4" }), h("path", { d: "M8.4 18h3.3a1 1 0 0 0 1-1v-3.4" }), h("path", { d: "M14.6 12h4.4" }));
710
+ }
711
+ const panelBtn = {
712
+ font: "inherit",
713
+ fontSize: 11,
714
+ fontWeight: 600,
715
+ padding: "3px 9px",
716
+ borderRadius: 7,
717
+ cursor: "pointer",
718
+ border: `1px solid ${T.border}`,
719
+ background: T.layer1,
720
+ color: T.text,
721
+ whiteSpace: "nowrap",
722
+ lineHeight: "16px"
723
+ };
724
+ const brandBtn = {
725
+ ...panelBtn,
726
+ border: `1px solid color-mix(in srgb, ${T.brand} 40%, transparent)`,
727
+ background: `color-mix(in srgb, ${T.brand} 12%, transparent)`,
728
+ color: T.brand
729
+ };
730
+ const sectionTitle = (text, extra) => h("div", { style: {
731
+ ...flexRow,
732
+ justifyContent: "space-between",
733
+ margin: "14px 0 8px"
734
+ } }, h("div", { style: {
735
+ fontSize: 12,
736
+ fontWeight: 700,
737
+ color: T.text,
738
+ letterSpacing: ".02em"
739
+ } }, text), extra || null);
740
+ const muted = (text, style = {}) => h("div", { style: {
741
+ fontSize: 11,
742
+ color: T.text2,
743
+ lineHeight: 1.6,
744
+ ...style
745
+ } }, text);
746
+ const runUsageText = (u) => {
747
+ if (!u || !(u.input || u.cacheRead || u.cacheWrite || u.output)) return "无 token 数据";
748
+ const hit = hitRate(u);
749
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""} · ${u.calls} 次`;
750
+ };
751
+ const RUN_PREVIEW = 8;
752
+ const TERMINAL_STATUSES = [
753
+ "accepted",
754
+ "closed",
755
+ "verified",
756
+ "cancelled"
757
+ ];
758
+ function ProductRail({ products, current, loadingKey, onSelect, onRefresh, busy }) {
759
+ return h("div", { style: {
760
+ width: 236,
761
+ flex: "0 0 236px",
762
+ minHeight: 0,
763
+ display: "flex",
764
+ flexDirection: "column",
765
+ borderRight: `1px solid ${T.border}`,
766
+ background: `color-mix(in srgb, ${T.layer1} 40%, transparent)`
767
+ } }, h("div", { style: {
768
+ ...flexRow,
769
+ justifyContent: "space-between",
770
+ padding: "10px 12px 8px"
771
+ } }, h("span", { style: {
772
+ fontSize: 11.5,
773
+ fontWeight: 700,
774
+ color: T.text2
775
+ } }, `产品线 · ${products.length}`), h("button", {
776
+ style: panelBtn,
777
+ onClick: onRefresh,
778
+ disabled: busy,
779
+ title: "重新扫描 $DSH_HOME/teamflow"
780
+ }, busy ? "刷新中" : "刷新")), h("div", { style: {
781
+ flex: 1,
782
+ minHeight: 0,
783
+ overflowY: "auto",
784
+ padding: "0 8px 12px"
785
+ } }, products.length === 0 ? muted("还没有产品线。在某个工作区跑过一次流水线后,这里会出现对应产品线($DSH_HOME/teamflow/<key>)。", { padding: "10px 4px" }) : products.map((p) => {
786
+ const on = p.key === current;
787
+ return h("div", {
788
+ key: p.key,
789
+ onClick: () => onSelect(p.key),
790
+ title: p.path || p.key,
791
+ style: {
792
+ padding: "8px 10px",
793
+ marginBottom: 6,
794
+ borderRadius: 9,
795
+ cursor: "pointer",
796
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
797
+ background: on ? `color-mix(in srgb, ${T.brand} 10%, ${T.layer1})` : "transparent"
798
+ }
799
+ }, h("div", { style: {
800
+ ...flexRow,
801
+ justifyContent: "space-between"
802
+ } }, h("span", { style: {
803
+ fontSize: 12,
804
+ fontWeight: 600,
805
+ color: T.text,
806
+ overflow: "hidden",
807
+ textOverflow: "ellipsis",
808
+ whiteSpace: "nowrap"
809
+ } }, p.title || p.key), p.activeRuns > 0 ? chip(`运行 ${p.activeRuns}`, T.brand, { dot: true }) : null), h("div", { style: {
810
+ fontSize: 10,
811
+ color: T.text2,
812
+ fontFamily: MONO,
813
+ marginTop: 2,
814
+ overflow: "hidden",
815
+ textOverflow: "ellipsis",
816
+ whiteSpace: "nowrap"
817
+ } }, p.key), h("div", { style: {
818
+ ...flexRow,
819
+ gap: 8,
820
+ marginTop: 3,
821
+ fontSize: 10,
822
+ color: T.text2
823
+ } }, h("span", null, `run ${p.totalRuns}`), p.updatedAt ? h("span", null, `更新 ${fmtTime(p.updatedAt)}`) : null, p.key === loadingKey ? h("span", { style: {
824
+ color: T.brand,
825
+ fontWeight: 600
826
+ } }, "读取中…") : null), p.lastRequirement ? h("div", { style: {
827
+ fontSize: 10.5,
828
+ color: T.text2,
829
+ marginTop: 3,
830
+ lineHeight: 1.45,
831
+ display: "-webkit-box",
832
+ WebkitLineClamp: 2,
833
+ WebkitBoxOrient: "vertical",
834
+ overflow: "hidden"
835
+ } }, p.lastRequirement) : null, p.lastVerdict ? h("div", { style: { marginTop: 4 } }, chip(`验收 ${p.lastVerdict}`, stColor(p.lastVerdict === "accepted" ? "accepted" : p.lastVerdict))) : null);
836
+ })));
837
+ }
838
+ function RunList({ runs, activeRunId, onOpenRun, onInlineRun }) {
839
+ if (!runs.length) return muted("该产品线还没有 run 记录。", { padding: "2px 2px 8px" });
840
+ return h("div", { style: {
841
+ display: "flex",
842
+ flexDirection: "column",
843
+ gap: 6
844
+ } }, runs.map((r) => {
845
+ const on = r.id === activeRunId;
846
+ const active = r.status === "running" || r.status === "pending";
847
+ return h("div", {
848
+ key: r.id,
849
+ style: {
850
+ padding: "8px 11px",
851
+ borderRadius: 9,
852
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
853
+ borderLeft: `3px solid ${stColor(r.status)}`,
854
+ background: on ? `color-mix(in srgb, ${T.brand} 8%, ${T.layer1})` : T.layer1,
855
+ display: "flex",
856
+ alignItems: "center",
857
+ gap: 10,
858
+ cursor: "pointer"
859
+ },
860
+ onClick: () => onInlineRun(r)
861
+ }, h("div", { style: {
862
+ flex: 1,
863
+ minWidth: 0
864
+ } }, h("div", { style: {
865
+ ...flexRow,
866
+ gap: 6
867
+ } }, chip(RUN_STATUS_TEXT[r.status] || r.status, stColor(r.status), { dot: true }), r.mode ? chip(String(r.mode), T.text2) : null, h("span", { style: {
868
+ fontFamily: MONO,
869
+ fontSize: 10,
870
+ color: T.text2
871
+ } }, r.id), active ? h("span", { style: {
872
+ fontSize: 10,
873
+ color: T.brand
874
+ } }, "进行中") : null), h("div", { style: {
875
+ fontSize: 11.5,
876
+ color: T.text,
877
+ marginTop: 3,
878
+ overflow: "hidden",
879
+ textOverflow: "ellipsis",
880
+ whiteSpace: "nowrap"
881
+ } }, r.requirement || "(无需求描述)"), h("div", { style: {
882
+ ...flexRow,
883
+ gap: 10,
884
+ marginTop: 3,
885
+ fontSize: 10,
886
+ color: T.text2,
887
+ fontFamily: MONO
888
+ } }, h("span", null, `阶段 ${r.doneStages}/${r.stageCount}`), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), h("button", {
889
+ style: brandBtn,
890
+ title: "跳到该 run 的发起会话,并在那个会话的右侧栏打开详情(右侧栏是会话级的:挂到无关会话上没有意义)",
891
+ onClick: (e) => {
892
+ e.stopPropagation();
893
+ onOpenRun(r);
894
+ }
895
+ }, "去会话右栏"));
896
+ }));
897
+ }
898
+ function BacklogCard({ kind, item, onOpen }) {
899
+ const color = stColor(item.status);
900
+ return h("div", {
901
+ onClick: () => onOpen(kind, item.id),
902
+ style: {
903
+ padding: "6px 9px",
904
+ borderRadius: 8,
905
+ border: `1px solid ${T.border}`,
906
+ borderLeft: `3px solid ${color}`,
907
+ background: T.layer1,
908
+ cursor: "pointer",
909
+ minWidth: 0,
910
+ boxSizing: "border-box",
911
+ maxWidth: "100%",
912
+ overflow: "hidden"
913
+ }
914
+ }, h("div", { style: {
915
+ ...flexRow,
916
+ gap: 5,
917
+ minWidth: 0
918
+ } }, h("span", { style: {
919
+ fontFamily: MONO,
920
+ fontSize: 10,
921
+ color: T.text2,
922
+ minWidth: 0,
923
+ overflow: "hidden",
924
+ textOverflow: "ellipsis",
925
+ whiteSpace: "nowrap"
926
+ } }, item.id), chip(stText(item.status), color, { dot: true }), kind === "bug" && item.severity ? chip(String(item.severity), stColor("rework")) : null), h("div", { style: {
927
+ fontSize: 11.5,
928
+ color: T.text,
929
+ marginTop: 3,
930
+ overflow: "hidden",
931
+ textOverflow: "ellipsis",
932
+ whiteSpace: "nowrap"
933
+ } }, item.title || item.id), (() => {
934
+ const roles = kind === "task" ? byRoleLine(item) : "";
935
+ return roles ? h("div", {
936
+ title: roles,
937
+ style: {
938
+ fontSize: 10,
939
+ color: T.text2,
940
+ fontFamily: MONO,
941
+ marginTop: 3,
942
+ minWidth: 0,
943
+ overflowWrap: "anywhere",
944
+ wordBreak: "break-word"
945
+ }
946
+ }, roles) : null;
947
+ })(), h("div", { style: {
948
+ ...flexRow,
949
+ gap: 6,
950
+ marginTop: 3,
951
+ fontSize: 10,
952
+ color: T.text2
953
+ } }, item.devAssign ? h("span", null, `dev ${item.devAssign}`) : null, item.qaAssign ? h("span", null, `qa ${item.qaAssign}`) : null, item.acceptBy ? h("span", null, `验收 ${item.acceptBy}`) : null, item.retries ? h("span", { style: { color: T.warn } }, `重试 ${item.retries}`) : null, item.humanIntervention ? h("span", { style: { color: T.error } }, "需人工") : null));
954
+ }
955
+ /** 可点筛选徽章(多选):单击切换该状态,选中态用状态色实心;再点取消。 */
956
+ const filterChip = (text, color, on, onToggle) => h("button", {
957
+ onClick: onToggle,
958
+ title: on ? "点击取消该状态筛选" : "点击只看该状态(可多选)",
959
+ style: {
960
+ font: "inherit",
961
+ fontSize: 11,
962
+ fontWeight: 600,
963
+ padding: "1px 8px",
964
+ borderRadius: 999,
965
+ cursor: "pointer",
966
+ lineHeight: "16px",
967
+ whiteSpace: "nowrap",
968
+ border: `1px solid ${on ? `color-mix(in srgb, ${color} 55%, transparent)` : T.border}`,
969
+ background: on ? `color-mix(in srgb, ${color} 26%, transparent)` : "transparent",
970
+ color: on ? color : T.text2
971
+ }
972
+ }, text);
973
+ /** run 状态展示顺序(可行动的在前)。 */
974
+ const RUN_STATUS_ORDER = [
975
+ "running",
976
+ "pending",
977
+ "interrupted",
978
+ "failed",
979
+ "cancelled",
980
+ "completed",
981
+ "superseded"
982
+ ];
983
+ function BacklogGroups({ backlog, onOpen, productKey }) {
984
+ const [showDone, setShowDone] = react.default.useState({});
985
+ const [filters, setFilters] = react.default.useState({});
986
+ react.default.useEffect(() => {
987
+ setShowDone({});
988
+ setFilters({});
989
+ }, [productKey]);
990
+ if (!backlog) return null;
991
+ const groups = [
992
+ ["req", backlog.requirements || []],
993
+ ["task", (backlog.tasks || []).filter((t) => t.type !== "subtask")],
994
+ ["bug", backlog.bugs || []]
995
+ ];
996
+ if (!groups.reduce((a, [, arr]) => a + arr.length, 0)) return muted("backlog 为空(该产品线还没有立项卡片)。", { padding: "2px 2px 8px" });
997
+ return h("div", null, groups.map(([kind, list]) => {
998
+ if (!list.length) return null;
999
+ const done = list.filter((it) => TERMINAL_STATUSES.indexOf(it.status) !== -1 && !it.humanIntervention);
1000
+ const active = list.filter((it) => done.indexOf(it) === -1);
1001
+ const open = showDone[kind] === true;
1002
+ const sel = filters[kind] || [];
1003
+ const selSet = new Set(sel);
1004
+ const filtering = sel.length > 0;
1005
+ const matched = filtering ? list.filter((it) => selSet.has(it.status)) : list;
1006
+ const byStatus = {};
1007
+ for (const it of list) byStatus[it.status] = (byStatus[it.status] || 0) + 1;
1008
+ const toggle = (st) => setFilters((m) => {
1009
+ const cur = new Set(m[kind] || []);
1010
+ if (cur.has(st)) cur.delete(st);
1011
+ else cur.add(st);
1012
+ return {
1013
+ ...m,
1014
+ [kind]: [...cur]
1015
+ };
1016
+ });
1017
+ const visible = filtering ? matched : open ? list : active;
1018
+ return h("div", {
1019
+ key: kind,
1020
+ style: { marginBottom: 12 }
1021
+ }, h("div", { style: {
1022
+ ...flexRow,
1023
+ gap: 6,
1024
+ marginBottom: 6,
1025
+ minWidth: 0
1026
+ } }, h("span", { style: {
1027
+ fontSize: 11.5,
1028
+ fontWeight: 700,
1029
+ color: T.text,
1030
+ flex: "0 0 auto"
1031
+ } }, `${KIND_TITLE[kind]} · ${list.length}`), h("span", { style: {
1032
+ fontSize: 10,
1033
+ color: T.text2,
1034
+ flex: "0 0 auto"
1035
+ } }, `活动 ${active.length}`), ...Object.keys(byStatus).map((s) => filterChip(`${stText(s)} ${byStatus[s]}`, stColor(s), selSet.has(s), () => toggle(s))), filtering ? h("button", {
1036
+ style: {
1037
+ ...panelBtn,
1038
+ marginLeft: "auto",
1039
+ flex: "0 0 auto"
1040
+ },
1041
+ title: "清除本组筛选",
1042
+ onClick: () => setFilters((m) => ({
1043
+ ...m,
1044
+ [kind]: []
1045
+ }))
1046
+ }, `筛选中 ${sel.length} 项 · 显示 ${matched.length}/${list.length} × 清除`) : done.length ? h("button", {
1047
+ style: {
1048
+ ...panelBtn,
1049
+ marginLeft: "auto",
1050
+ flex: "0 0 auto"
1051
+ },
1052
+ title: open ? "收起已完成/已关闭卡片" : "展开已完成/已关闭卡片",
1053
+ onClick: () => setShowDone((m) => ({
1054
+ ...m,
1055
+ [kind]: !open
1056
+ }))
1057
+ }, open ? `收起已完成 ${done.length}` : `已完成 ${done.length} ▸`) : null), visible.length ? h("div", { style: {
1058
+ display: "grid",
1059
+ gridTemplateColumns: "repeat(auto-fill, minmax(228px, 1fr))",
1060
+ gap: 6
1061
+ } }, visible.map((it) => h(BacklogCard, {
1062
+ key: it.id,
1063
+ kind,
1064
+ item: it,
1065
+ onOpen
1066
+ }))) : muted("该筛选下没有卡片。", { fontSize: 10.5 }));
1067
+ }));
1068
+ }
1069
+ function ItemDetailPane({ det, openArtifact, onClose }) {
1070
+ if (!det) return null;
1071
+ const row = (k, v) => v === null || v === void 0 || v === "" ? null : h("div", { style: {
1072
+ ...flexRow,
1073
+ gap: 6,
1074
+ fontSize: 11
1075
+ } }, h("span", { style: {
1076
+ color: T.text2,
1077
+ flex: "0 0 auto"
1078
+ } }, k), h("span", { style: {
1079
+ color: T.text,
1080
+ minWidth: 0,
1081
+ wordBreak: "break-word"
1082
+ } }, String(v)));
1083
+ return h("div", { style: {
1084
+ padding: "10px 12px",
1085
+ display: "flex",
1086
+ flexDirection: "column",
1087
+ gap: 8,
1088
+ minHeight: 0
1089
+ } }, h("div", { style: {
1090
+ ...flexRow,
1091
+ justifyContent: "space-between"
1092
+ } }, h("div", { style: {
1093
+ ...flexRow,
1094
+ gap: 6
1095
+ } }, h("span", { style: {
1096
+ fontFamily: MONO,
1097
+ fontSize: 11,
1098
+ color: T.text2
1099
+ } }, det.id), chip(stText(det.status), stColor(det.status), { dot: true }), h("span", { style: {
1100
+ fontSize: 10.5,
1101
+ color: T.text2
1102
+ } }, KIND_TITLE[det.kind] || det.kind)), h("button", {
1103
+ style: panelBtn,
1104
+ onClick: onClose
1105
+ }, "关闭")), h("div", { style: {
1106
+ fontSize: 13,
1107
+ fontWeight: 600,
1108
+ color: T.text,
1109
+ lineHeight: 1.45
1110
+ } }, det.title), h("div", { style: {
1111
+ display: "flex",
1112
+ flexDirection: "column",
1113
+ gap: 3
1114
+ } }, row("需求", det.reqId), row("负责人", det.owner), row("开发", det.devAssign), row("测试", det.qaAssign), row("验收", det.assignBy), row("重试", det.retries), row("任务夹", det.runDocs)), det.spec ? h("div", null, h("div", { style: {
1115
+ fontSize: 11,
1116
+ color: T.text2,
1117
+ marginBottom: 3
1118
+ } }, "规格"), h(FoldableText, { text: det.spec })) : null, det.summary ? h("div", null, h("div", { style: {
1119
+ fontSize: 11,
1120
+ color: T.text2,
1121
+ marginBottom: 3
1122
+ } }, "结论摘要"), h(FoldableText, { text: det.summary })) : null, det.artifacts && det.artifacts.length ? h("div", null, h("div", { style: {
1123
+ fontSize: 11,
1124
+ color: T.text2,
1125
+ marginBottom: 4
1126
+ } }, `任务夹产物 · ${det.artifacts.length}`), h("div", { style: {
1127
+ ...flexRow,
1128
+ gap: 5
1129
+ } }, det.artifacts.map((a) => h("button", {
1130
+ key: a.name,
1131
+ style: brandBtn,
1132
+ title: `${a.address}\n(跳到产物所属会话后在该会话右侧栏打开)`,
1133
+ onClick: () => openArtifact && openArtifact(a.address, a.name, det.runInfo && det.runInfo.ownerSession || null)
1134
+ }, a.name)))) : muted("该条目没有可预览的任务夹产物(或缺少会话上下文,无法生成文件地址)。"), det.subtasks && det.subtasks.length ? h("div", null, h("div", { style: {
1135
+ fontSize: 11,
1136
+ color: T.text2,
1137
+ marginBottom: 4
1138
+ } }, `子卡 · ${det.subtasks.length}`), h("div", { style: {
1139
+ display: "flex",
1140
+ flexDirection: "column",
1141
+ gap: 4
1142
+ } }, det.subtasks.map((s) => h("div", {
1143
+ key: s.id,
1144
+ style: {
1145
+ ...flexRow,
1146
+ gap: 6,
1147
+ fontSize: 11,
1148
+ padding: "4px 7px",
1149
+ borderRadius: 7,
1150
+ background: T.layer1,
1151
+ border: `1px solid ${T.border}`
1152
+ }
1153
+ }, h("span", { style: {
1154
+ fontFamily: MONO,
1155
+ fontSize: 10,
1156
+ color: T.text2
1157
+ } }, s.id), chip(stText(s.status), stColor(s.status)), h("span", { style: {
1158
+ color: T.text,
1159
+ minWidth: 0,
1160
+ overflow: "hidden",
1161
+ textOverflow: "ellipsis",
1162
+ whiteSpace: "nowrap"
1163
+ } }, s.title), s.failed ? chip("失败", T.error) : null)))) : null, det.bugs && det.bugs.length ? h("div", null, h("div", { style: {
1164
+ fontSize: 11,
1165
+ color: T.text2,
1166
+ marginBottom: 4
1167
+ } }, `关联缺陷 · ${det.bugs.length}`), h("div", { style: {
1168
+ display: "flex",
1169
+ flexDirection: "column",
1170
+ gap: 4
1171
+ } }, det.bugs.map((b) => h("div", {
1172
+ key: b.id,
1173
+ style: {
1174
+ ...flexRow,
1175
+ gap: 6,
1176
+ fontSize: 11,
1177
+ padding: "4px 7px",
1178
+ borderRadius: 7,
1179
+ background: T.layer1,
1180
+ border: `1px solid ${T.border}`
1181
+ }
1182
+ }, h("span", { style: {
1183
+ fontFamily: MONO,
1184
+ fontSize: 10,
1185
+ color: T.text2
1186
+ } }, b.id), b.severity ? chip(String(b.severity), T.warn) : null, chip(stText(b.status), stColor(b.status)), h("span", { style: {
1187
+ color: T.text,
1188
+ minWidth: 0,
1189
+ overflow: "hidden",
1190
+ textOverflow: "ellipsis",
1191
+ whiteSpace: "nowrap"
1192
+ } }, b.title))))) : null, det.events && det.events.length ? h("div", null, h("div", { style: {
1193
+ fontSize: 11,
1194
+ color: T.text2,
1195
+ marginBottom: 4
1196
+ } }, `流转时间线 · 最近 ${Math.min(det.events.length, 30)} 条`), h("div", { style: {
1197
+ display: "flex",
1198
+ flexDirection: "column",
1199
+ gap: 3,
1200
+ fontFamily: MONO,
1201
+ fontSize: 10,
1202
+ color: T.text2
1203
+ } }, 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);
1204
+ }
1205
+ function RunDetailPane({ snap, product, api }) {
1206
+ const [sel, setSel] = react.default.useState(null);
1207
+ const [err, setErr] = react.default.useState(null);
1208
+ react.default.useEffect(() => {
1209
+ setSel(null);
1210
+ setErr(null);
1211
+ }, [snap && snap.id]);
1212
+ if (!snap) return muted("未找到该 run(可能已被清理,或地址已过期)。", { padding: 12 });
1213
+ const stages = snap.stages || [];
1214
+ const totals = stages.reduce((a, s) => {
1215
+ const u = s.usage;
1216
+ if (u) {
1217
+ a.input += u.input || 0;
1218
+ a.cacheRead += u.cacheRead || 0;
1219
+ a.cacheWrite += u.cacheWrite || 0;
1220
+ a.output += u.output || 0;
1221
+ a.calls += u.calls || 0;
1222
+ }
1223
+ return a;
1224
+ }, {
1225
+ input: 0,
1226
+ cacheRead: 0,
1227
+ cacheWrite: 0,
1228
+ output: 0,
1229
+ calls: 0
1230
+ });
1231
+ const openStage = async (s) => {
1232
+ if (!api) return;
1233
+ try {
1234
+ setSel(await api.stageDetail(snap.id, s.seq));
1235
+ setErr(null);
1236
+ } catch (e) {
1237
+ setErr(String(e && e.message || e));
1238
+ }
1239
+ };
1240
+ const logs = (snap.logs || []).slice(-60);
1241
+ return h("div", { style: {
1242
+ padding: "10px 12px",
1243
+ display: "flex",
1244
+ flexDirection: "column",
1245
+ gap: 10,
1246
+ minHeight: 0,
1247
+ overflowY: "auto"
1248
+ } }, h("div", { style: {
1249
+ ...flexRow,
1250
+ gap: 6
1251
+ } }, chip(RUN_STATUS_TEXT[snap.status] || snap.status, stColor(snap.status), { dot: true }), snap.options && snap.options.mode ? chip(String(snap.options.mode), T.text2) : null, h("span", { style: {
1252
+ fontFamily: MONO,
1253
+ fontSize: 10.5,
1254
+ color: T.text2
1255
+ } }, snap.id), product ? h("span", { style: {
1256
+ fontSize: 10,
1257
+ color: T.text2,
1258
+ fontFamily: MONO
1259
+ } }, product) : null), h("div", { style: {
1260
+ fontSize: 12,
1261
+ color: T.text,
1262
+ lineHeight: 1.5
1263
+ } }, snap.requirement || "(无需求描述)"), h("div", { style: {
1264
+ ...flexRow,
1265
+ gap: 12,
1266
+ fontSize: 10.5,
1267
+ color: T.text2,
1268
+ fontFamily: MONO
1269
+ } }, h("span", null, `${fmtTime(snap.startedAt)}${snap.endedAt ? ` → ${fmtTime(snap.endedAt)}` : " → 进行中"} · ${fmtDur(snap.startedAt, snap.endedAt)}`), h("span", null, `阶段 ${stages.filter((s) => s.status === "done").length}/${stages.length}`), h("span", null, `子代理 ${snap.agentsStarted || 0}`)), h("div", { style: {
1270
+ ...flexRow,
1271
+ gap: 10,
1272
+ fontSize: 10.5,
1273
+ color: T.text2,
1274
+ fontFamily: MONO,
1275
+ padding: "6px 8px",
1276
+ borderRadius: 8,
1277
+ background: `color-mix(in srgb, ${T.layer2} 60%, transparent)`,
1278
+ border: `1px solid ${T.border}`
1279
+ } }, h("span", null, `输入(未命中) ${fmtTokens(totals.input) || "0"}`), h("span", null, `输入(命中) ${fmtTokens(totals.cacheRead) || "0"}`), h("span", null, `写缓存 ${fmtTokens(totals.cacheWrite) || "0"}`), h("span", null, `输出 ${fmtTokens(totals.output) || "0"}`), h("span", null, `${totals.calls} 次调用`), h("span", null, `合计 ${fmtTokens(totalTokens(totals)) || "0"}`)), sectionTitle(`阶段 · ${stages.length}`), h("div", { style: {
1280
+ display: "flex",
1281
+ flexDirection: "column",
1282
+ gap: 4
1283
+ } }, stages.map((s) => {
1284
+ const on = sel && Number(sel.seq) === Number(s.seq);
1285
+ const color = stColor(s.status);
1286
+ return h("div", {
1287
+ key: s.seq,
1288
+ onClick: () => openStage(s),
1289
+ style: {
1290
+ padding: "6px 9px",
1291
+ borderRadius: 8,
1292
+ cursor: "pointer",
1293
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
1294
+ borderLeft: `3px solid ${color}`,
1295
+ background: on ? `color-mix(in srgb, ${T.brand} 7%, ${T.layer1})` : T.layer1
1296
+ }
1297
+ }, h("div", { style: {
1298
+ ...flexRow,
1299
+ gap: 6
1300
+ } }, h("span", { style: {
1301
+ fontFamily: MONO,
1302
+ fontSize: 10,
1303
+ color: T.text2
1304
+ } }, `#${s.seq}`), h("span", null, phaseIconOf(s.phase)), h("span", { style: {
1305
+ fontSize: 11.5,
1306
+ color: T.text,
1307
+ fontWeight: 500
1308
+ } }, s.label || phaseNameOf(s.phase)), chip(stText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
1309
+ marginLeft: "auto",
1310
+ fontFamily: MONO,
1311
+ fontSize: 10,
1312
+ color: T.text2
1313
+ } }, `${fmtTime(s.startedAt)}${s.endedAt ? ` · ${fmtDur(s.startedAt, s.endedAt)}` : ""}`)), h("div", { style: {
1314
+ ...flexRow,
1315
+ gap: 10,
1316
+ marginTop: 3,
1317
+ fontFamily: MONO,
1318
+ fontSize: 10,
1319
+ color: T.text2
1320
+ } }, h("span", null, stageUsageLine(s) || "无 token 数据"), s.childId ? h("span", { title: s.childId }, `子代理 ${String(s.childId).slice(0, 14)}`) : null), s.summary ? h("div", { style: {
1321
+ fontSize: 10.5,
1322
+ color: T.text2,
1323
+ marginTop: 3,
1324
+ lineHeight: 1.45
1325
+ } }, String(s.summary).slice(0, 200)) : null);
1326
+ })), err ? muted(`阶段详情读取失败:${err}`, { color: T.error }) : null, sel ? h("div", { style: {
1327
+ padding: "9px 10px",
1328
+ borderRadius: 9,
1329
+ border: `1px solid ${T.border}`,
1330
+ background: T.layer2,
1331
+ display: "flex",
1332
+ flexDirection: "column",
1333
+ gap: 8
1334
+ } }, h("div", { style: {
1335
+ ...flexRow,
1336
+ justifyContent: "space-between"
1337
+ } }, h("div", { style: {
1338
+ ...flexRow,
1339
+ gap: 6
1340
+ } }, h("span", { style: {
1341
+ fontSize: 11.5,
1342
+ fontWeight: 700,
1343
+ color: T.text
1344
+ } }, `阶段 #${sel.seq} 详情`), chip(stText(sel.status), stColor(sel.status))), h("button", {
1345
+ style: panelBtn,
1346
+ onClick: () => setSel(null)
1347
+ }, "收起")), h("div", { style: {
1348
+ fontSize: 10.5,
1349
+ color: T.text2,
1350
+ fontFamily: MONO
1351
+ } }, `阶段 token:${stageUsageLine(sel) || "无"}`), sel.verifyEvidence ? h("div", null, h("div", { style: {
1352
+ fontSize: 11,
1353
+ color: T.success,
1354
+ marginBottom: 3
1355
+ } }, "🔬 验证证据"), h("div", { style: {
1356
+ whiteSpace: "pre-wrap",
1357
+ wordBreak: "break-word",
1358
+ fontSize: 11,
1359
+ lineHeight: 1.6,
1360
+ color: T.text,
1361
+ background: `color-mix(in srgb, ${T.layer1} 70%, transparent)`,
1362
+ border: `1px solid ${T.border}`,
1363
+ borderRadius: 8,
1364
+ padding: "7px 9px",
1365
+ maxHeight: 200,
1366
+ overflowY: "auto",
1367
+ fontFamily: MONO
1368
+ } }, sel.verifyEvidence)) : muted("(缺失——契约未兑现,host 已记警告)", { color: T.warn }), sel.output ? h("div", null, h("div", { style: {
1369
+ fontSize: 11,
1370
+ color: T.text2,
1371
+ marginBottom: 3
1372
+ } }, "阶段产出"), h(FoldableText, {
1373
+ text: sel.output,
1374
+ charLimit: 400,
1375
+ lineLimit: 8,
1376
+ style: {
1377
+ fontFamily: MONO,
1378
+ fontSize: 11
1379
+ }
1380
+ })) : null, sel.attempts && sel.attempts.length > 1 ? h("div", null, h("div", { style: {
1381
+ fontSize: 11,
1382
+ color: T.text2,
1383
+ marginBottom: 3
1384
+ } }, `同任务尝试 · ${sel.attempts.length}`), h("div", { style: {
1385
+ display: "flex",
1386
+ flexDirection: "column",
1387
+ gap: 3
1388
+ } }, sel.attempts.map((a) => h("div", {
1389
+ key: a.seq,
1390
+ style: {
1391
+ ...flexRow,
1392
+ gap: 6,
1393
+ fontSize: 10.5,
1394
+ fontFamily: MONO,
1395
+ color: T.text2
1396
+ }
1397
+ }, 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(`日志 · 最近 ${logs.length} 条`), h("div", { style: {
1398
+ display: "flex",
1399
+ flexDirection: "column",
1400
+ gap: 2,
1401
+ fontFamily: MONO,
1402
+ fontSize: 10,
1403
+ color: T.text2,
1404
+ maxHeight: 220,
1405
+ overflowY: "auto"
1406
+ } }, logs.map((l, i) => h("div", {
1407
+ key: i,
1408
+ style: { color: l.level === "error" ? T.error : l.level === "warn" ? T.warn : T.text2 }
1409
+ }, `${fmtTime(l.t)} [${l.level}] ${l.message}`)))) : null);
1410
+ }
1411
+ /** 右栏 run 详情 tab 正文(地址来自宿主导航记录;host 只认自己的地址格式)。
1412
+ *
1413
+ * ⚠️ prop 名:宿主渲染器把 slot 的 `hooks: { tabInfo }` 绑成 **`useTabInfo`**(约定 `hooks.<name>` → `use<Name>`;
1414
+ * 官方 `ui-sidebar-documentpreview/TextPreview.tsx` 即用 `useTabInfo`)。2026-09-11 实锤:写成 `tabInfo`
1415
+ * 会恒 undefined → 地址取不到、错误也没设 → 永远停在「读取 run 详情中…」。
1416
+ */
1417
+ function RunDetailTab(props) {
1418
+ const readTab = props && (typeof props.useTabInfo === "function" ? props.useTabInfo : typeof props.tabInfo === "function" ? props.tabInfo : null);
1419
+ let info = null;
1420
+ try {
1421
+ info = readTab ? readTab() : null;
1422
+ } catch (e) {
1423
+ info = null;
1424
+ }
1425
+ const tab = info && info.tab ? info.tab : null;
1426
+ const address = tab && tab.navigation && tab.navigation.address || tab && tab.contentId || null;
1427
+ const parsed = parseRunAddress(address);
1428
+ const remote = props && props.remote;
1429
+ const [snap, setSnap] = react.default.useState(null);
1430
+ const [err, setErr] = react.default.useState(null);
1431
+ const [nonce, setNonce] = react.default.useState(0);
1432
+ const product = parsed ? parsed.product : null;
1433
+ const runId = parsed ? parsed.runId : null;
1434
+ const api = react.default.useMemo(() => remote && product ? productApi(remote, product) : null, [remote, product]);
1435
+ const [, bump] = react.default.useReducer((n) => n + 1, 0);
1436
+ react.default.useEffect(() => {
1437
+ if (address) return void 0;
1438
+ let tries = 0;
1439
+ const timer = setInterval(() => {
1440
+ tries += 1;
1441
+ bump();
1442
+ if (tries >= 12) clearInterval(timer);
1443
+ }, 150);
1444
+ return () => clearInterval(timer);
1445
+ }, [address]);
1446
+ react.default.useEffect(() => {
1447
+ let alive = true;
1448
+ if (!runId) {
1449
+ setSnap(null);
1450
+ setErr(address ? `无法解析 run 地址:${address}` : readTab ? "读取 tab 地址中…" : "宿主 tab 信息钩子不可用(useTabInfo 缺失)");
1451
+ return;
1452
+ }
1453
+ if (!api) {
1454
+ setSnap(null);
1455
+ setErr("remote 不可用(插件未挂载或版本过旧)");
1456
+ return;
1457
+ }
1458
+ api.runDetail(runId).then((v) => {
1459
+ if (alive) {
1460
+ setSnap(v);
1461
+ setErr(null);
1462
+ }
1463
+ }, (e) => {
1464
+ if (alive) setErr(String(e && e.message || e));
1465
+ });
1466
+ const timer = setInterval(() => {
1467
+ api.runDetail(runId).then((v) => {
1468
+ if (!alive) return;
1469
+ setSnap(v);
1470
+ if (!v || !(v.status === "running" || v.status === "pending")) clearInterval(timer);
1471
+ }, () => {});
1472
+ }, 5e3);
1473
+ return () => {
1474
+ alive = false;
1475
+ clearInterval(timer);
1476
+ };
1477
+ }, [
1478
+ api,
1479
+ runId,
1480
+ address,
1481
+ nonce,
1482
+ readTab
1483
+ ]);
1484
+ if (err && !snap) {
1485
+ const pending = !address && !!readTab;
1486
+ return h("div", { style: {
1487
+ padding: 12,
1488
+ display: "flex",
1489
+ flexDirection: "column",
1490
+ gap: 8
1491
+ } }, muted(pending ? "读取 tab 地址中…" : err, { color: pending ? T.text2 : T.error }), pending ? null : h("button", {
1492
+ style: panelBtn,
1493
+ onClick: () => {
1494
+ setErr(null);
1495
+ setNonce((n) => n + 1);
1496
+ }
1497
+ }, "重试"));
1498
+ }
1499
+ if (!snap) return muted("读取 run 详情中…", { padding: 12 });
1500
+ return h(RunDetailPane, {
1501
+ snap,
1502
+ product,
1503
+ api
1504
+ });
1505
+ }
1506
+ function productApi(remote, product) {
1507
+ return {
1508
+ view: async () => unwrap$1(await remote.productView(product), "productView"),
1509
+ runDetail: async (runId) => unwrap$1(await remote.productRunDetail(product, runId), "productRunDetail"),
1510
+ stageDetail: async (runId, seq) => unwrap$1(await remote.productStageDetail(product, runId, seq), "productStageDetail"),
1511
+ itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail")
1512
+ };
1513
+ }
1514
+ /** 全局面板 props:root scope 标准 props(useSessions 取当前会话)+ 插件注入(remote/打开回调)。 */
1515
+ function GlobalPanel(props) {
1516
+ const remote = props.remote;
1517
+ const useSessions = props.useSessions;
1518
+ const currentSessionId = (() => {
1519
+ try {
1520
+ return useSessions ? useSessions((s) => s.current) : void 0;
1521
+ } catch (e) {
1522
+ return;
1523
+ }
1524
+ })();
1525
+ const [state, setState] = react.default.useState({
1526
+ products: [],
1527
+ current: null,
1528
+ view: null,
1529
+ err: null,
1530
+ busy: false
1531
+ });
1532
+ const [detail, setDetail] = react.default.useState(null);
1533
+ const [hint, setHint] = react.default.useState(null);
1534
+ const [runsExpanded, setRunsExpanded] = react.default.useState(false);
1535
+ const [panelTab, setPanelTab] = react.default.useState("run");
1536
+ const [viewTick, setViewTick] = react.default.useState(0);
1537
+ const [runFilter, setRunFilter] = react.default.useState([]);
1538
+ /**
1539
+ * 右侧栏的**会话内容**宿主只在「对话被选中」时渲染(`RightbarRoot` 门控 `activePanelId === null`),
1540
+ * 而全局面板占着 `main` → seat 不存在 → `ctx.sidebarRight` 没有 binding(抛 `no session surface
1541
+ * is mounted`,2026-09-11 用户实测 tf-mtvrsakj-l2vj5u)。所以:先切回对话,再小步重试
1542
+ * (seat 在切换后的 effect 里才 `bindService`);彻底失败才降级,并把原因显示在面板里。
1543
+ */
1544
+ const openInConversationRightbar = (address, label, fallback) => {
1545
+ const attempt = (quiet) => !!(props.openResource && address && props.openResource(address, label, quiet));
1546
+ if (attempt(false)) return;
1547
+ if (!address) {
1548
+ setHint(`${label}:host 未生成可打开的地址(可能缺少会话上下文)`);
1549
+ if (fallback) fallback();
1550
+ return;
1551
+ }
1552
+ try {
1553
+ if (props.layout && typeof props.layout.selectPanel === "function") props.layout.selectPanel(null);
1554
+ } catch (e) {}
1555
+ let tries = 0;
1556
+ const tick = () => {
1557
+ tries += 1;
1558
+ if (attempt(true)) return;
1559
+ if (tries < 12) {
1560
+ setTimeout(tick, 120);
1561
+ return;
1562
+ }
1563
+ setHint(`右侧栏打开失败(宿主只在对话视图挂载它):${label}${fallback ? "——已在本面板内联显示" : ""}`);
1564
+ if (fallback) fallback();
1565
+ };
1566
+ setTimeout(tick, 140);
1567
+ };
1568
+ /**
1569
+ * **跳到资源所属的会话,再在那个会话的右栏打开**(全局面板的正确语义)。
1570
+ * 右侧栏是会话级的:从全局面板看 tetris 的 run 却把 tab 挂到"用户当前所在会话"上没有意义
1571
+ * (用户 2026-09-11 提出)。所以先 `sessions.open(ownerSession)`,等当前会话真的切过去、
1572
+ * 且对话 seat 挂载 bind 之后再 openResource(两者都要等,故小步重试 + 就绪判据)。
1573
+ * @param target.ownerSession - 资源所属会话(run 的发起会话 / 产物地址里的会话)
1574
+ * @param target.address - host 生成的 dsh-resource 地址
1575
+ * @param target.label - 提示用的名字
1576
+ * @param target.fallback - 没有 ownerSession 或跳转失败时的降级(通常是面板内联详情)
1577
+ */
1578
+ const goOwnerSessionAndOpen = (target) => {
1579
+ const { ownerSession, address, label, fallback } = target || {};
1580
+ const sessions = props.sessions;
1581
+ if (!ownerSession || !sessions || typeof sessions.open !== "function") {
1582
+ openInConversationRightbar(address, label, fallback);
1583
+ return;
1584
+ }
1585
+ try {
1586
+ sessions.open(ownerSession);
1587
+ } catch (e) {
1588
+ setHint(`发起会话 ${String(ownerSession).slice(0, 8)}… 不在会话列表里(可能已被清理)——已在本面板展示详情`);
1589
+ if (fallback) fallback();
1590
+ return;
1591
+ }
1592
+ try {
1593
+ if (props.layout && typeof props.layout.selectPanel === "function") props.layout.selectPanel(null);
1594
+ } catch (e) {}
1595
+ let tries = 0;
1596
+ const tick = () => {
1597
+ tries += 1;
1598
+ let nowCurrent = null;
1599
+ try {
1600
+ nowCurrent = sessions.list && sessions.list.getSnapshot ? sessions.list.getSnapshot().current : null;
1601
+ } catch (e) {
1602
+ nowCurrent = null;
1603
+ }
1604
+ if ((nowCurrent === ownerSession || tries >= 6) && props.openResource && address && props.openResource(address, label, tries < 6)) return;
1605
+ if (tries < 14) {
1606
+ setTimeout(tick, 130);
1607
+ return;
1608
+ }
1609
+ setHint(`已切到发起会话,但右栏没打开(${label})——可在该会话里用「⇥ 右栏打开」重试`);
1610
+ };
1611
+ setTimeout(tick, 140);
1612
+ };
1613
+ const loadProducts = react.default.useCallback(async (preferKey) => {
1614
+ if (!remote || typeof remote.products !== "function") {
1615
+ setState((s) => ({
1616
+ ...s,
1617
+ err: "remote.products 不可用(插件未挂载或版本过旧)"
1618
+ }));
1619
+ return;
1620
+ }
1621
+ setState((s) => ({
1622
+ ...s,
1623
+ busy: true
1624
+ }));
1625
+ try {
1626
+ const v = unwrap$1(await remote.products(currentSessionId || null), "products") || {};
1627
+ const list = v.products || [];
1628
+ const key = preferKey || state.current || (v.current && list.some((p) => p.key === v.current) ? v.current : list[0] && list[0].key || null);
1629
+ setState({
1630
+ products: list,
1631
+ current: key,
1632
+ view: key === state.current ? state.view : null,
1633
+ err: null,
1634
+ busy: false
1635
+ });
1636
+ setViewTick((t) => t + 1);
1637
+ } catch (e) {
1638
+ setState((s) => ({
1639
+ ...s,
1640
+ busy: false,
1641
+ err: String(e && e.message || e)
1642
+ }));
1643
+ }
1644
+ }, [
1645
+ remote,
1646
+ currentSessionId,
1647
+ state.current,
1648
+ state.view
1649
+ ]);
1650
+ const loadView = react.default.useCallback(async (key, silent) => {
1651
+ if (!remote || !key) return;
1652
+ const api = productApi(remote, key);
1653
+ try {
1654
+ const v = await api.view();
1655
+ setState((s) => ({
1656
+ ...s,
1657
+ view: v,
1658
+ err: null
1659
+ }));
1660
+ } catch (e) {
1661
+ if (!silent) setState((s) => ({
1662
+ ...s,
1663
+ err: String(e && e.message || e)
1664
+ }));
1665
+ }
1666
+ }, [remote]);
1667
+ /** 选中产品线:**同一个**产品线 = 刷新(保留现有视图,不清空 → 不会卡"读取中"且不闪);不同产品线 = 清空 + 重新拉。 */
1668
+ const selectProduct = (k) => {
1669
+ closeDetail();
1670
+ setRunsExpanded(false);
1671
+ setRunFilter([]);
1672
+ setState((s) => ({
1673
+ ...s,
1674
+ current: k,
1675
+ view: s.current === k ? s.view : null
1676
+ }));
1677
+ setViewTick((t) => t + 1);
1678
+ };
1679
+ /** run 状态筛选开关(多选)。 */
1680
+ const toggleRunStatus = (st) => setRunFilter((cur) => cur.indexOf(st) === -1 ? cur.concat([st]) : cur.filter((x) => x !== st));
1681
+ react.default.useEffect(() => {
1682
+ loadProducts();
1683
+ }, [currentSessionId]);
1684
+ react.default.useEffect(() => {
1685
+ if (state.current) loadView(state.current);
1686
+ }, [
1687
+ state.current,
1688
+ viewTick,
1689
+ loadView
1690
+ ]);
1691
+ react.default.useEffect(() => {
1692
+ if (!(state.view && state.view.runs || []).some((r) => r.status === "running" || r.status === "pending") || !state.current) return void 0;
1693
+ const timer = setInterval(() => {
1694
+ loadView(state.current, true);
1695
+ }, 8e3);
1696
+ return () => clearInterval(timer);
1697
+ }, [
1698
+ state.current,
1699
+ state.view,
1700
+ loadView
1701
+ ]);
1702
+ const api = state.current && remote ? productApi(remote, state.current) : null;
1703
+ const openItem = async (kind, id) => {
1704
+ if (!api) return;
1705
+ try {
1706
+ setDetail({
1707
+ kind: "item",
1708
+ data: await api.itemDetail(kind, id, currentSessionId || null)
1709
+ });
1710
+ } catch (e) {
1711
+ setState((s) => ({
1712
+ ...s,
1713
+ err: String(e && e.message || e)
1714
+ }));
1715
+ }
1716
+ };
1717
+ const showInline = async (r) => {
1718
+ if (!api) return;
1719
+ try {
1720
+ const snap = await api.runDetail(r.id);
1721
+ setDetail({
1722
+ kind: "run",
1723
+ data: snap,
1724
+ run: r
1725
+ });
1726
+ } catch (e) {
1727
+ setState((s) => ({
1728
+ ...s,
1729
+ err: String(e && e.message || e)
1730
+ }));
1731
+ }
1732
+ };
1733
+ const closeDetail = () => setDetail(null);
1734
+ const openRun = (r) => {
1735
+ goOwnerSessionAndOpen({
1736
+ ownerSession: r.ownerSession,
1737
+ address: r.address,
1738
+ label: r.id,
1739
+ fallback: () => {
1740
+ showInline(r);
1741
+ }
1742
+ });
1743
+ };
1744
+ const openArtifactInPanel = (address, name, ownerSession) => {
1745
+ goOwnerSessionAndOpen({
1746
+ ownerSession,
1747
+ address,
1748
+ label: name
1749
+ });
1750
+ };
1751
+ const view = state.view;
1752
+ const product = view && view.product;
1753
+ const runs = view && view.runs || [];
1754
+ const detailOpen = !!detail;
1755
+ const runSel = runFilter || [];
1756
+ const runSelSet = new Set(runSel);
1757
+ const runFiltering = runSel.length > 0;
1758
+ const runsMatched = runFiltering ? runs.filter((r) => runSelSet.has(r.status)) : runs;
1759
+ const runStatusCounts = {};
1760
+ for (const r of runs) runStatusCounts[r.status] = (runStatusCounts[r.status] || 0) + 1;
1761
+ const headRuns = runsMatched.slice(0, RUN_PREVIEW);
1762
+ const headIds = new Set(headRuns.map((r) => r.id));
1763
+ const pinnedActive = runsExpanded ? [] : runsMatched.filter((r) => (r.status === "running" || r.status === "pending") && !headIds.has(r.id));
1764
+ const visibleRuns = runsExpanded || runsMatched.length <= RUN_PREVIEW ? runsMatched : pinnedActive.concat(headRuns);
1765
+ const bl = view && view.backlog;
1766
+ const backlogCount = bl ? (bl.requirements || []).length + (bl.tasks || []).filter((t) => t.type !== "subtask").length + (bl.bugs || []).length : 0;
1767
+ /** 主区标签按钮(选中态用品牌色下划线)。 */
1768
+ const panelTabBtn = (key, label) => h("button", {
1769
+ key,
1770
+ onClick: () => setPanelTab(key),
1771
+ style: {
1772
+ font: "inherit",
1773
+ fontSize: 12,
1774
+ fontWeight: panelTab === key ? 700 : 500,
1775
+ padding: "6px 12px",
1776
+ marginBottom: -1,
1777
+ cursor: "pointer",
1778
+ whiteSpace: "nowrap",
1779
+ background: "transparent",
1780
+ border: "none",
1781
+ borderBottom: `2px solid ${panelTab === key ? T.brand : "transparent"}`,
1782
+ color: panelTab === key ? T.text : T.text2
1783
+ }
1784
+ }, label);
1785
+ return h("div", { style: {
1786
+ height: "100%",
1787
+ minHeight: 0,
1788
+ display: "flex",
1789
+ flexDirection: "column",
1790
+ background: T.bg,
1791
+ color: T.text,
1792
+ fontFamily: SANS,
1793
+ fontSize: 12
1794
+ } }, h("div", { style: {
1795
+ ...flexRow,
1796
+ justifyContent: "space-between",
1797
+ padding: "10px 14px",
1798
+ borderBottom: `1px solid ${T.border}`
1799
+ } }, h("div", { style: {
1800
+ ...flexRow,
1801
+ gap: 8
1802
+ } }, h("span", { style: {
1803
+ fontSize: 13,
1804
+ fontWeight: 700
1805
+ } }, "🏭 团队工作台"), h("span", { style: {
1806
+ fontSize: 11,
1807
+ color: T.text2
1808
+ } }, "全局面板 · 按产品线"), currentSessionId ? h("span", { style: {
1809
+ fontSize: 10,
1810
+ color: T.text2,
1811
+ fontFamily: MONO
1812
+ } }, `当前会话 ${String(currentSessionId).slice(0, 8)}`) : h("span", { style: {
1813
+ fontSize: 10,
1814
+ color: T.text2
1815
+ } }, "无当前会话")), h("div", { style: {
1816
+ ...flexRow,
1817
+ gap: 6
1818
+ } }, product ? h("button", {
1819
+ style: panelBtn,
1820
+ onClick: () => loadView(state.current)
1821
+ }, "刷新数据") : null, h("button", {
1822
+ style: panelBtn,
1823
+ title: "回到对话(再点侧边栏图标即可切回本面板)",
1824
+ onClick: () => {
1825
+ try {
1826
+ const layout = props.layout;
1827
+ if (layout && layout.selectPanel) layout.selectPanel(null);
1828
+ } catch (e) {}
1829
+ }
1830
+ }, "回到对话"))), state.err ? h("div", { style: {
1831
+ padding: "6px 14px",
1832
+ fontSize: 11,
1833
+ color: T.error,
1834
+ borderBottom: `1px solid ${T.border}`
1835
+ } }, state.err) : null, hint ? h("div", { style: {
1836
+ ...flexRow,
1837
+ justifyContent: "space-between",
1838
+ gap: 8,
1839
+ padding: "6px 14px",
1840
+ fontSize: 11,
1841
+ color: T.warn,
1842
+ borderBottom: `1px solid ${T.border}`,
1843
+ background: `color-mix(in srgb, ${T.warn} 8%, transparent)`
1844
+ } }, h("span", null, hint), h("button", {
1845
+ style: panelBtn,
1846
+ onClick: () => setHint(null)
1847
+ }, "知道了")) : null, h("div", { style: {
1848
+ flex: 1,
1849
+ minHeight: 0,
1850
+ display: "flex",
1851
+ position: "relative"
1852
+ } }, h(ProductRail, {
1853
+ products: state.products,
1854
+ current: state.current,
1855
+ loadingKey: state.view ? null : state.current,
1856
+ busy: state.busy,
1857
+ onRefresh: () => loadProducts(state.current),
1858
+ onSelect: selectProduct
1859
+ }), h("div", { style: {
1860
+ flex: 1,
1861
+ minWidth: 0,
1862
+ minHeight: 0,
1863
+ display: "flex",
1864
+ flexDirection: "column"
1865
+ } }, !state.current ? h("div", { style: { padding: "12px 14px" } }, muted("选择左侧产品线查看 backlog 与 run(首次进入默认选最近更新的产品线)。")) : !view ? h("div", { style: { padding: "12px 14px" } }, muted("读取产品线数据中…")) : h(react.default.Fragment, null, h("div", { style: {
1866
+ ...flexRow,
1867
+ justifyContent: "space-between",
1868
+ gap: 10,
1869
+ padding: "12px 14px 6px"
1870
+ } }, h("div", { style: { minWidth: 0 } }, h("div", { style: {
1871
+ fontSize: 15,
1872
+ fontWeight: 700,
1873
+ color: T.text
1874
+ } }, product.title || product.key), h("div", { style: {
1875
+ fontSize: 10.5,
1876
+ color: T.text2,
1877
+ fontFamily: MONO,
1878
+ marginTop: 2,
1879
+ overflow: "hidden",
1880
+ textOverflow: "ellipsis",
1881
+ whiteSpace: "nowrap"
1882
+ } }, product.path || product.key)), h("div", { style: {
1883
+ ...flexRow,
1884
+ gap: 6,
1885
+ flex: "0 0 auto"
1886
+ } }, chip(`run ${product.totalRuns}`, T.text2), product.activeRuns > 0 ? chip(`活跃 ${product.activeRuns}`, T.brand, { dot: true }) : null, product.lastVerdict ? chip(`验收 ${product.lastVerdict}`, stColor("accepted")) : null)), h("div", { style: {
1887
+ display: "flex",
1888
+ alignItems: "flex-end",
1889
+ gap: 2,
1890
+ padding: "0 14px",
1891
+ borderBottom: `1px solid ${T.border}`
1892
+ } }, panelTabBtn("run", `🚀 流水线 run · ${runs.length}`), panelTabBtn("backlog", `📋 Backlog · ${backlogCount}`), h("div", { style: {
1893
+ marginLeft: "auto",
1894
+ ...flexRow,
1895
+ gap: 6,
1896
+ paddingBottom: 7
1897
+ } }, panelTab === "run" && !runsExpanded && pinnedActive.length > 0 ? chip(`已置顶进行中 ${pinnedActive.length}`, T.brand, { dot: true }) : null, panelTab === "run" && runsMatched.length > RUN_PREVIEW ? h("button", {
1898
+ style: panelBtn,
1899
+ onClick: () => setRunsExpanded((v) => !v)
1900
+ }, runsExpanded ? `只看最近 ${RUN_PREVIEW} 条` : `展开全部 ${runsMatched.length} 条`) : null)), h("div", { style: {
1901
+ flex: 1,
1902
+ minHeight: 0,
1903
+ overflowY: "auto",
1904
+ overflowX: "hidden",
1905
+ padding: "12px 14px 18px"
1906
+ } }, panelTab === "run" ? h(react.default.Fragment, null, h("div", { style: {
1907
+ ...flexRow,
1908
+ gap: 6,
1909
+ marginBottom: 8
1910
+ } }, ...RUN_STATUS_ORDER.filter((st) => runStatusCounts[st]).map((st) => filterChip(`${RUN_STATUS_TEXT[st] || st} ${runStatusCounts[st]}`, stColor(st), runSelSet.has(st), () => toggleRunStatus(st))), runFiltering ? h("button", {
1911
+ style: panelBtn,
1912
+ title: "清除 run 状态筛选",
1913
+ onClick: () => setRunFilter([])
1914
+ }, `筛选中 ${runSel.length} 项 · 显示 ${runsMatched.length}/${runs.length} × 清除`) : null), visibleRuns.length ? h(RunList, {
1915
+ runs: visibleRuns,
1916
+ activeRunId: detail && detail.kind === "run" && detail.run ? detail.run.id : null,
1917
+ onOpenRun: openRun,
1918
+ onInlineRun: showInline
1919
+ }) : muted("该筛选下没有 run。", { fontSize: 10.5 }), muted("点一行看详情浮层;「去会话右栏」= 跳到该 run 的发起会话并在其右侧栏打开(与任务夹产物并排)", {
1920
+ fontSize: 10,
1921
+ marginTop: 8
1922
+ })) : h(react.default.Fragment, null, muted("点状态徽章可筛选(多选);终态卡片默认收起,筛选时自动显示", {
1923
+ fontSize: 10,
1924
+ marginBottom: 8
1925
+ }), h(BacklogGroups, {
1926
+ backlog: view.backlog,
1927
+ onOpen: openItem,
1928
+ productKey: state.current
1929
+ }))))), detailOpen ? h("div", { style: {
1930
+ position: "absolute",
1931
+ top: 8,
1932
+ right: 12,
1933
+ bottom: 8,
1934
+ width: 440,
1935
+ zIndex: 9,
1936
+ overflowY: "auto",
1937
+ overflowX: "hidden",
1938
+ borderRadius: 12,
1939
+ border: `1px solid ${T.border}`,
1940
+ background: `color-mix(in srgb, ${T.layer1} 96%, transparent)`,
1941
+ backdropFilter: "blur(12px)",
1942
+ boxShadow: "0 14px 44px rgba(0,0,0,.28)"
1943
+ } }, detail && detail.kind === "item" ? h(ItemDetailPane, {
1944
+ det: detail.data,
1945
+ openArtifact: openArtifactInPanel,
1946
+ onClose: closeDetail
1947
+ }) : h("div", null, h("div", { style: {
1948
+ ...flexRow,
1949
+ justifyContent: "space-between",
1950
+ padding: "10px 12px 0"
1951
+ } }, h("span", { style: {
1952
+ fontSize: 11.5,
1953
+ fontWeight: 700,
1954
+ color: T.text
1955
+ } }, "run 详情"), h("div", { style: {
1956
+ ...flexRow,
1957
+ gap: 6
1958
+ } }, detail && detail.data && detail.data.address ? h("button", {
1959
+ style: brandBtn,
1960
+ title: "跳到该 run 的发起会话,并在那个会话的右侧栏打开(与任务夹产物并排看)",
1961
+ onClick: () => goOwnerSessionAndOpen({
1962
+ ownerSession: detail.data.ownerSession,
1963
+ address: detail.data.address,
1964
+ label: detail.data.id
1965
+ })
1966
+ }, "去发起会话") : null, h("button", {
1967
+ style: panelBtn,
1968
+ onClick: closeDetail
1969
+ }, "关闭"))), h(RunDetailPane, {
1970
+ snap: detail && detail.data,
1971
+ product: state.current,
1972
+ api
1973
+ }))) : null));
481
1974
  }
482
- /** 官方口径工具函数(与 DeepSeek usage 账单对齐,零额外概念):
483
- * - u.input : 输入(缓存未命中)
484
- * - u.cacheRead : 输入(缓存命中)
485
- * - u.cacheWrite : 输入写入缓存
486
- * - u.output : 输出
487
- * billed input = input + cacheRead + cacheWrite;命中率 = cacheRead / (input + cacheRead)。
1975
+ //#endregion
1976
+ //#region client/index.tsx
1977
+ /**
1978
+ * dsh-plugin-teamflow — browser half(v0.7 视觉重构:「精密工程控制台」)。
1979
+ *
1980
+ * 注册 conversation.view tab「🏭 团队工作台」(与 chat / 轨迹并列):
1981
+ * - 流水线图形工作流:阶段泳道 + 节点卡片(状态/耗时/token/子代理会话)+ 实时轮询
1982
+ * - Backlog 拖拽看板:需求/任务/缺陷三组状态泳道,卡片拖拽流转(原生 HTML5 DnD,零依赖)
1983
+ * - 成本中心:每阶段 token + 总计 + 运行时长
1984
+ * - 人工介入中心:needs-human 项聚合 + 一键终态
1985
+ * - 历史 run 切换
1986
+ *
1987
+ * 视觉:全部使用 DSH 主题 token(--dsw-alias-*,自动适配深浅色)。
1988
+ * Remote 接入:$mount 后 ctx.get('remote.teamflow') 取全局实例传给组件
1989
+ * (不能 inject 'remote.teamflow'——自挂载自注入会让 fiber 死锁)。
488
1990
  */
489
- function hitRate(u) {
490
- const total = (u.input || 0) + (u.cacheRead || 0);
491
- return total > 0 ? Math.round((u.cacheRead || 0) / total * 100) : null;
492
- }
493
- function usageDetail(s) {
494
- const k = (n) => n === null || n === void 0 ? "—" : fmtTokens(n);
495
- if (s.usage) {
496
- const u = s.usage;
497
- const hit = hitRate(u);
498
- return `输入(未命中) ${k(u.input)} / 输入(命中) ${k(u.cacheRead)} / 写缓存 ${k(u.cacheWrite)} / 输出 ${k(u.output)} · ${u.calls} 次调用${hit !== null ? ` · 缓存命中 ${hit}%` : ""}`;
499
- }
500
- return "无 usage 明细";
501
- }
502
- /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
503
- function stageUsageLine(s) {
504
- const u = s && s.usage;
505
- if (u && (u.input || u.cacheRead || u.cacheWrite || u.output)) {
506
- const hit = hitRate(u);
507
- return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""}`;
508
- }
509
- return null;
510
- }
511
- const ROLE_NAME = {
512
- pm: "产品",
513
- design: "设计",
514
- arch: "架构",
515
- tech: "方案",
516
- dev: "开发",
517
- qa: "测试",
518
- acceptance: "验收",
519
- other: "其他"
520
- };
521
- const roleUsage = (u) => {
522
- if (!u) return "";
523
- const hit = hitRate(u);
524
- return `⇅${fmtTokens(u.input || 0)}/${fmtTokens(u.cacheRead || 0)}·⬆${fmtTokens(u.output || 0)}${hit !== null ? `·${hit}%` : ""}`;
525
- };
526
- /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
527
- function byRoleLine(task) {
528
- const roles = task && task.byRole || {};
529
- return Object.keys(roles).filter((k) => roles[k] && roles[k].input + roles[k].output + roles[k].cacheRead + roles[k].cacheWrite > 0).map((k) => `${ROLE_NAME[k] || k} ${roleUsage(roles[k])}`).join(" · ");
530
- }
531
- /** 多阶段 usage 汇总(官方口径)。 */
532
- function totalUsage(stages) {
533
- const t = {
534
- input: 0,
535
- cacheRead: 0,
536
- cacheWrite: 0,
537
- output: 0,
538
- calls: 0
539
- };
540
- for (const s of stages || []) {
541
- const u = s && s.usage;
542
- if (!u) continue;
543
- t.input += u.input || 0;
544
- t.cacheRead += u.cacheRead || 0;
545
- t.cacheWrite += u.cacheWrite || 0;
546
- t.output += u.output || 0;
547
- t.calls += u.calls || 0;
548
- }
549
- return t;
550
- }
551
- const stText = (s) => STATUS_TEXT[s] || s;
552
- const stColor = (s) => STATUS_COLOR[s] || T.text2;
1991
+ const inject = [
1992
+ "remote",
1993
+ "slots",
1994
+ "sessions",
1995
+ "locale"
1996
+ ];
553
1997
  const NODE_W = 300;
554
1998
  const PHASE_GAP = 88;
555
1999
  const WAVE = 56;
@@ -1261,7 +2705,8 @@ window.__ModuleLoader__.load({
1261
2705
  onMouseLeave: onUp,
1262
2706
  style: {
1263
2707
  position: "relative",
1264
- height: 560,
2708
+ flex: 1,
2709
+ minHeight: 320,
1265
2710
  borderRadius: 12,
1266
2711
  overflow: "hidden",
1267
2712
  touchAction: "none",
@@ -1458,6 +2903,10 @@ window.__ModuleLoader__.load({
1458
2903
  padding: "7px 9px",
1459
2904
  cursor: "grab",
1460
2905
  fontSize: 12,
2906
+ boxSizing: "border-box",
2907
+ minWidth: 0,
2908
+ maxWidth: "100%",
2909
+ overflow: "hidden",
1461
2910
  opacity: drag && drag.id === item.id ? .35 : 1,
1462
2911
  transition: "opacity .1s ease, transform .12s ease",
1463
2912
  boxShadow: "0 1px 2px rgba(0,0,0,.05)"
@@ -1466,25 +2915,36 @@ window.__ModuleLoader__.load({
1466
2915
  }, h("div", { style: {
1467
2916
  display: "flex",
1468
2917
  alignItems: "center",
1469
- gap: 4
2918
+ gap: 4,
2919
+ minWidth: 0
1470
2920
  } }, h("span", { style: {
1471
2921
  fontFamily: MONO,
1472
2922
  color: T.text2,
1473
- fontSize: 10.5
2923
+ fontSize: 10.5,
2924
+ minWidth: 0,
2925
+ overflow: "hidden",
2926
+ textOverflow: "ellipsis",
2927
+ whiteSpace: "nowrap"
1474
2928
  } }, item.id), item.severity ? chip(item.severity, item.severity === "P0" ? T.error : item.severity === "P1" ? T.warn : T.text2) : null, item.owner ? h("span", { style: {
1475
2929
  marginLeft: "auto",
1476
2930
  fontSize: 10.5,
1477
- color: T.text2
2931
+ color: T.text2,
2932
+ minWidth: 0,
2933
+ overflow: "hidden",
2934
+ textOverflow: "ellipsis",
2935
+ whiteSpace: "nowrap"
1478
2936
  } }, `👤 ${item.owner}`) : null), h("div", {
1479
2937
  title: item.title || void 0,
1480
2938
  style: {
1481
2939
  fontWeight: 500,
1482
2940
  margin: "3px 0 4px",
1483
- lineHeight: 1.4
2941
+ lineHeight: 1.4,
2942
+ overflowWrap: "anywhere"
1484
2943
  }
1485
2944
  }, (item.title || "").slice(0, 30)), h("div", { style: {
1486
2945
  ...flexRow,
1487
- marginTop: 2
2946
+ marginTop: 2,
2947
+ minWidth: 0
1488
2948
  } }, chip(stText(item.status), stColor(item.status)), typeof item.retries === "number" && item.retries > 0 ? h("span", { style: {
1489
2949
  fontSize: 10.5,
1490
2950
  color: T.warn,
@@ -1498,7 +2958,8 @@ window.__ModuleLoader__.load({
1498
2958
  marginTop: 3,
1499
2959
  fontSize: 10.5,
1500
2960
  color: T.text2,
1501
- fontFamily: MONO
2961
+ fontFamily: MONO,
2962
+ minWidth: 0
1502
2963
  } }, item.devAssign ? h("span", {
1503
2964
  title: `dev 分配\n${item.devAssign}`,
1504
2965
  style: {
@@ -1507,7 +2968,8 @@ window.__ModuleLoader__.load({
1507
2968
  gap: 3,
1508
2969
  overflow: "hidden",
1509
2970
  textOverflow: "ellipsis",
1510
- maxWidth: 130,
2971
+ maxWidth: "100%",
2972
+ minWidth: 0,
1511
2973
  whiteSpace: "nowrap"
1512
2974
  }
1513
2975
  }, `👨‍💻${item.devAssign}`) : null, item.qaAssign ? h("span", {
@@ -1518,7 +2980,8 @@ window.__ModuleLoader__.load({
1518
2980
  gap: 3,
1519
2981
  overflow: "hidden",
1520
2982
  textOverflow: "ellipsis",
1521
- maxWidth: 130,
2983
+ maxWidth: "100%",
2984
+ minWidth: 0,
1522
2985
  whiteSpace: "nowrap"
1523
2986
  }
1524
2987
  }, `🧪${item.qaAssign}`) : null, item.acceptBy ? h("span", {
@@ -1529,7 +2992,8 @@ window.__ModuleLoader__.load({
1529
2992
  gap: 3,
1530
2993
  overflow: "hidden",
1531
2994
  textOverflow: "ellipsis",
1532
- maxWidth: 130,
2995
+ maxWidth: "100%",
2996
+ minWidth: 0,
1533
2997
  whiteSpace: "nowrap"
1534
2998
  }
1535
2999
  }, `✅${item.acceptBy}`) : null) : null, kind === "task" && byRoleLine(item) ? h("div", { style: {
@@ -1537,8 +3001,21 @@ window.__ModuleLoader__.load({
1537
3001
  marginTop: 3,
1538
3002
  fontSize: 10.5,
1539
3003
  color: T.warn,
1540
- fontFamily: MONO
1541
- } }, h("span", { style: { color: T.text2 } }, "⛽"), byRoleLine(item)) : null, kind === "task" && (item.subtaskIds || []).length > 0 ? (() => {
3004
+ fontFamily: MONO,
3005
+ minWidth: 0,
3006
+ maxWidth: "100%"
3007
+ } }, h("span", { style: {
3008
+ color: T.text2,
3009
+ flex: "0 0 auto"
3010
+ } }, "⛽"), h("span", {
3011
+ title: byRoleLine(item),
3012
+ style: {
3013
+ minWidth: 0,
3014
+ flex: "1 1 auto",
3015
+ overflowWrap: "anywhere",
3016
+ wordBreak: "break-word"
3017
+ }
3018
+ }, byRoleLine(item))) : null, kind === "task" && (item.subtaskIds || []).length > 0 ? (() => {
1542
3019
  const subs = item.subtaskIds.map((id) => subtaskMap[id]).filter(Boolean);
1543
3020
  if (subs.length === 0) return null;
1544
3021
  const done = subs.filter((s) => s.status === "done").length;
@@ -1561,29 +3038,33 @@ window.__ModuleLoader__.load({
1561
3038
  padding: "2px 6px",
1562
3039
  borderRadius: 4,
1563
3040
  marginBottom: 2,
3041
+ minWidth: 0,
1564
3042
  background: sub.status === "failed" ? `color-mix(in srgb, ${T.error} 7%, transparent)` : T.layer2,
1565
3043
  border: `1px solid ${stColor(sub.status)}30`
1566
3044
  }
1567
3045
  }, h("span", { style: {
1568
3046
  color: stColor(sub.status),
1569
3047
  fontWeight: 600,
1570
- minWidth: 12
3048
+ minWidth: 12,
3049
+ flex: "0 0 auto"
1571
3050
  } }, sub.status === "done" ? "✓" : sub.status === "failed" ? "✗" : sub.status === "running" ? "⟳" : "…"), h("span", {
1572
3051
  title: sub.title || void 0,
1573
3052
  style: {
3053
+ flex: "1 1 auto",
3054
+ minWidth: 0,
1574
3055
  overflow: "hidden",
1575
3056
  textOverflow: "ellipsis",
1576
- whiteSpace: "nowrap",
1577
- maxWidth: 130
3057
+ whiteSpace: "nowrap"
1578
3058
  }
1579
3059
  }, (sub.title || "").replace(/^开发 · /, "")), sub.devAssign ? h("span", {
1580
3060
  title: `dev 分配\n${sub.devAssign}`,
1581
3061
  style: {
1582
- marginLeft: "auto",
3062
+ flex: "0 1 auto",
1583
3063
  color: T.text2,
1584
3064
  overflow: "hidden",
1585
3065
  textOverflow: "ellipsis",
1586
3066
  maxWidth: 70,
3067
+ minWidth: 0,
1587
3068
  whiteSpace: "nowrap"
1588
3069
  }
1589
3070
  }, sub.devAssign) : null)));
@@ -1603,23 +3084,34 @@ window.__ModuleLoader__.load({
1603
3084
  }
1604
3085
  ];
1605
3086
  return h("div", { style: {
3087
+ position: "relative",
3088
+ flex: 1,
3089
+ minHeight: 0,
3090
+ display: "flex",
3091
+ flexDirection: "column"
3092
+ } }, h("div", { style: {
3093
+ flex: 1,
3094
+ minHeight: 0,
3095
+ overflowY: "auto",
1606
3096
  display: "flex",
1607
3097
  flexDirection: "column",
1608
3098
  gap: 14,
1609
- maxHeight: "72vh",
1610
- overflowY: "auto",
1611
3099
  paddingRight: 4
1612
3100
  } }, groups.map(({ kind, list }) => {
1613
3101
  const counts = {};
1614
3102
  for (const s of COLUMNS[kind]) counts[s] = 0;
1615
3103
  for (const item of list) counts[item.status] = (counts[item.status] || 0) + 1;
1616
3104
  return h("div", { key: kind }, h("div", { style: {
3105
+ position: "sticky",
3106
+ top: 0,
3107
+ zIndex: 3,
3108
+ background: T.bg,
1617
3109
  display: "flex",
1618
3110
  alignItems: "center",
1619
3111
  gap: 8,
1620
3112
  fontWeight: 700,
1621
3113
  fontSize: 13,
1622
- marginBottom: 8
3114
+ padding: "6px 0 8px"
1623
3115
  } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), KIND_TITLE[kind], h("span", { style: {
1624
3116
  fontSize: 11,
1625
3117
  fontWeight: 600,
@@ -1636,6 +3128,7 @@ window.__ModuleLoader__.load({
1636
3128
  } }, COLUMNS[kind].map((s) => {
1637
3129
  const isOver = over === `${kind}:${s}`;
1638
3130
  const color = stColor(s);
3131
+ const colBg = isOver ? `color-mix(in srgb, ${color} 8%, ${T.layer1})` : T.layer2;
1639
3132
  return h("div", {
1640
3133
  key: s,
1641
3134
  onDragOver: (e) => {
@@ -1653,11 +3146,12 @@ window.__ModuleLoader__.load({
1653
3146
  maxWidth: 172,
1654
3147
  flex: "0 0 auto",
1655
3148
  borderRadius: 10,
1656
- padding: 7,
3149
+ padding: 0,
1657
3150
  minHeight: 84,
1658
3151
  maxHeight: 340,
1659
3152
  overflowY: "auto",
1660
- background: isOver ? `color-mix(in srgb, ${color} 8%, ${T.layer1})` : T.layer2,
3153
+ overflowX: "hidden",
3154
+ background: colBg,
1661
3155
  border: `1px dashed ${isOver ? color : T.border}`,
1662
3156
  transition: "background .12s ease, border-color .12s ease"
1663
3157
  }
@@ -1666,8 +3160,13 @@ window.__ModuleLoader__.load({
1666
3160
  fontSize: 11,
1667
3161
  fontWeight: 600,
1668
3162
  color,
1669
- marginBottom: 6,
1670
- padding: "0 2px"
3163
+ position: "sticky",
3164
+ top: 0,
3165
+ zIndex: 2,
3166
+ background: colBg,
3167
+ padding: "7px 9px 5px",
3168
+ borderTopLeftRadius: 10,
3169
+ borderTopRightRadius: 10
1671
3170
  } }, h("span", null, stText(s)), h("span", { style: {
1672
3171
  marginLeft: "auto",
1673
3172
  fontFamily: MONO,
@@ -1676,10 +3175,12 @@ window.__ModuleLoader__.load({
1676
3175
  } }, counts[s] || 0)), h("div", { style: {
1677
3176
  display: "flex",
1678
3177
  flexDirection: "column",
1679
- gap: 6
3178
+ gap: 6,
3179
+ padding: "0 7px 7px",
3180
+ minWidth: 0
1680
3181
  } }, list.filter((item) => item.status === s).map((item) => card(item, kind))));
1681
3182
  })));
1682
- }), det ? h(ItemDetailDrawer, {
3183
+ })), det ? h(ItemDetailDrawer, {
1683
3184
  det,
1684
3185
  onClose: () => setDet(null),
1685
3186
  onShowRun,
@@ -2360,7 +3861,10 @@ window.__ModuleLoader__.load({
2360
3861
  display: "flex",
2361
3862
  flexDirection: "column",
2362
3863
  gap: 10,
2363
- padding: "4px 16px"
3864
+ padding: "4px 16px 10px",
3865
+ height: "100%",
3866
+ minHeight: 0,
3867
+ overflow: "hidden"
2364
3868
  } }, h("div", { style: {
2365
3869
  display: "flex",
2366
3870
  alignItems: "center",
@@ -2531,7 +4035,18 @@ window.__ModuleLoader__.load({
2531
4035
  }) : h("span", { style: {
2532
4036
  color: T.text2,
2533
4037
  fontSize: 11.5
2534
- } }, "(暂无)")) : null), tab === "pipeline" ? h(PipelinePanel, {
4038
+ } }, "(暂无)")) : null, tab === "pipeline" && activeRun && activeRun.address ? h("button", {
4039
+ style: chipBtn(false),
4040
+ title: "在右侧栏打开该 run 详情(与任务夹产物并排看)",
4041
+ onClick: () => {
4042
+ if (!(props.openResource && props.openResource(activeRun.address, activeRun.id))) console.warn("[teamflow] 右侧栏不可用,run 详情请在画布节点里查看");
4043
+ }
4044
+ }, "⇥ 右栏打开") : null), h("div", { style: {
4045
+ flex: 1,
4046
+ minHeight: 0,
4047
+ display: "flex",
4048
+ flexDirection: "column"
4049
+ } }, tab === "pipeline" ? h(PipelinePanel, {
2535
4050
  active,
2536
4051
  api,
2537
4052
  runId: activeRun ? activeRun.id : null,
@@ -2547,7 +4062,7 @@ window.__ModuleLoader__.load({
2547
4062
  setTab("pipeline");
2548
4063
  setRunId(rid);
2549
4064
  }
2550
- }));
4065
+ })));
2551
4066
  }
2552
4067
  if (typeof document !== "undefined") {
2553
4068
  const styleEl = document.createElement("style");
@@ -2557,22 +4072,65 @@ window.__ModuleLoader__.load({
2557
4072
  @keyframes tf-flow { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -28 } }`;
2558
4073
  document.head.appendChild(styleEl);
2559
4074
  }
2560
- /** 注册 conversation.view tab「团队工作台」+ input.right 团队选择按钮。 */
4075
+ /**
4076
+ * 注册三处:
4077
+ * 1. `sidebar.panellist` + `main`(key = 'teamflow'):**全局面板**——侧边栏图标 + 中央主区。
4078
+ * 两者 id 必须一致:宿主 `layout.selectPanel(id)` 对未注册的 main key 直接抛错。
4079
+ * 2. `conversation.view` tab(会话内工作台)+ `conversation.input.right`(团队选择按钮)。
4080
+ * 3. 右栏 run 详情 tab 类型 + body(`sidebarRightTabs` + `sidebar.right.pane.tab`)。
4081
+ */
2561
4082
  async function apply(ctx) {
2562
4083
  await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
2563
4084
  const teamflow = ctx.get("remote.teamflow");
2564
- const openArtifact = (address, name) => {
4085
+ const openResourceSafe = (address, label, quiet) => {
2565
4086
  try {
2566
4087
  const sidebarRight = ctx.get("sidebarRight");
2567
4088
  if (!sidebarRight || typeof sidebarRight.openResource !== "function") {
2568
- console.warn("[teamflow] 右侧栏服务不可用,无法预览产物", name);
2569
- return;
4089
+ if (!quiet) console.warn("[teamflow] 右侧栏服务不可用", label);
4090
+ return false;
2570
4091
  }
2571
4092
  sidebarRight.openResource(address);
4093
+ return true;
2572
4094
  } catch (e) {
2573
- console.warn("[teamflow] 打开产物失败", name, e && e.message);
4095
+ if (!quiet) console.warn("[teamflow] 打开右侧栏失败", label, e && e.message);
4096
+ return false;
2574
4097
  }
2575
4098
  };
4099
+ const openArtifact = (address, name) => {
4100
+ openResourceSafe(address, name);
4101
+ };
4102
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
4103
+ name: "sidebar.panellist",
4104
+ id: "teamflow",
4105
+ order: 60,
4106
+ label: "团队工作台"
4107
+ }, TeamflowPanelIcon));
4108
+ ctx.slots.inject("main", () => ctx.slots.register({
4109
+ name: "main",
4110
+ key: "teamflow",
4111
+ inject: () => ({
4112
+ remote: teamflow,
4113
+ sessions: ctx.get("sessions"),
4114
+ layout: ctx.get("layout"),
4115
+ openResource: openResourceSafe,
4116
+ openArtifact
4117
+ })
4118
+ }, GlobalPanel));
4119
+ try {
4120
+ const tabs = ctx.get("sidebarRightTabs");
4121
+ if (tabs && typeof tabs.register === "function") ctx.effect(() => tabs.register(runTabDefinition()), "teamflow: run tab type");
4122
+ else console.warn("[teamflow] sidebarRightTabs 不可用,run 详情右栏 tab 未注册(面板内联详情仍可用)");
4123
+ } catch (e) {
4124
+ console.warn("[teamflow] 注册 run tab 类型失败", e && e.message);
4125
+ }
4126
+ ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
4127
+ name: "sidebar.right.pane.tab",
4128
+ key: RUN_TAB_ID,
4129
+ inject: () => ({
4130
+ remote: teamflow,
4131
+ openArtifact
4132
+ })
4133
+ }, RunDetailTab));
2576
4134
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
2577
4135
  name: "conversation.view",
2578
4136
  id: "teamflow",
@@ -2582,7 +4140,8 @@ window.__ModuleLoader__.load({
2582
4140
  sessionId,
2583
4141
  remote: teamflow,
2584
4142
  sessions: ctx.get("sessions"),
2585
- openArtifact
4143
+ openArtifact,
4144
+ openResource: openResourceSafe
2586
4145
  })
2587
4146
  }, TeamFlowView));
2588
4147
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({