dsh-plugin-teamflow 0.1.6 → 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/CHANGELOG.md +51 -0
- package/README.en.md +6 -5
- package/README.md +35 -21
- package/lib/client.js +1740 -143
- package/lib/descriptors.mjs +54 -0
- package/lib/host.mjs +891 -314
- package/package.json +27 -6
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/
|
|
283
|
+
//#region client/shared.tsx
|
|
230
284
|
/**
|
|
231
|
-
* dsh-plugin-teamflow — browser half
|
|
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
|
-
*
|
|
241
|
-
* Remote
|
|
242
|
-
*
|
|
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
|
|
465
|
-
if (!
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
2708
|
+
flex: 1,
|
|
2709
|
+
minHeight: 320,
|
|
1265
2710
|
borderRadius: 12,
|
|
1266
2711
|
overflow: "hidden",
|
|
1267
2712
|
touchAction: "none",
|
|
@@ -1392,7 +2837,7 @@ window.__ModuleLoader__.load({
|
|
|
1392
2837
|
sessions
|
|
1393
2838
|
}) : null);
|
|
1394
2839
|
}
|
|
1395
|
-
function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun }) {
|
|
2840
|
+
function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun, openArtifact }) {
|
|
1396
2841
|
const [drag, setDrag] = react.default.useState(null);
|
|
1397
2842
|
const [over, setOver] = react.default.useState(null);
|
|
1398
2843
|
const [det, setDet] = react.default.useState(null);
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3149
|
+
padding: 0,
|
|
1657
3150
|
minHeight: 84,
|
|
1658
3151
|
maxHeight: 340,
|
|
1659
3152
|
overflowY: "auto",
|
|
1660
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
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,13 +3175,16 @@ 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
|
-
onShowRun
|
|
3186
|
+
onShowRun,
|
|
3187
|
+
openArtifact
|
|
1686
3188
|
}) : null);
|
|
1687
3189
|
}
|
|
1688
3190
|
function fmtAt(ts) {
|
|
@@ -1691,7 +3193,7 @@ window.__ModuleLoader__.load({
|
|
|
1691
3193
|
minute: "2-digit"
|
|
1692
3194
|
}) : "—";
|
|
1693
3195
|
}
|
|
1694
|
-
function ItemDetailDrawer({ det, onClose, onShowRun }) {
|
|
3196
|
+
function ItemDetailDrawer({ det, onClose, onShowRun, openArtifact }) {
|
|
1695
3197
|
const d = det && det.data;
|
|
1696
3198
|
const loading = det && det.loading;
|
|
1697
3199
|
const err = det && det.err;
|
|
@@ -1950,7 +3452,30 @@ window.__ModuleLoader__.load({
|
|
|
1950
3452
|
borderRadius: 8,
|
|
1951
3453
|
padding: "7px 10px",
|
|
1952
3454
|
wordBreak: "break-all"
|
|
1953
|
-
} }, d.runDocs + "/")) :
|
|
3455
|
+
} }, d.runDocs + "/"), Array.isArray(d.artifacts) && d.artifacts.length > 0 ? h("div", { style: {
|
|
3456
|
+
display: "flex",
|
|
3457
|
+
flexWrap: "wrap",
|
|
3458
|
+
gap: 6,
|
|
3459
|
+
marginTop: 1
|
|
3460
|
+
} }, d.artifacts.map((a) => h("button", {
|
|
3461
|
+
key: a.name,
|
|
3462
|
+
onClick: () => {
|
|
3463
|
+
if (openArtifact) openArtifact(a.address, a.name);
|
|
3464
|
+
},
|
|
3465
|
+
title: `在右侧栏预览 ${d.runDocs}/${a.name}`,
|
|
3466
|
+
style: {
|
|
3467
|
+
fontSize: 10.5,
|
|
3468
|
+
fontWeight: 600,
|
|
3469
|
+
padding: "3px 9px",
|
|
3470
|
+
borderRadius: 6,
|
|
3471
|
+
cursor: "pointer",
|
|
3472
|
+
lineHeight: "16px",
|
|
3473
|
+
border: `1px solid color-mix(in srgb, ${T.brand} 38%, transparent)`,
|
|
3474
|
+
background: `color-mix(in srgb, ${T.brand} 10%, transparent)`,
|
|
3475
|
+
color: T.brand,
|
|
3476
|
+
whiteSpace: "nowrap"
|
|
3477
|
+
}
|
|
3478
|
+
}, `📄 ${String(a.name).replace(/\.md$/, "")}`))) : null) : null,
|
|
1954
3479
|
kind === "task" && d.usage ? h("div", { style: {
|
|
1955
3480
|
display: "flex",
|
|
1956
3481
|
flexDirection: "column",
|
|
@@ -2336,7 +3861,10 @@ window.__ModuleLoader__.load({
|
|
|
2336
3861
|
display: "flex",
|
|
2337
3862
|
flexDirection: "column",
|
|
2338
3863
|
gap: 10,
|
|
2339
|
-
padding: "4px 16px"
|
|
3864
|
+
padding: "4px 16px 10px",
|
|
3865
|
+
height: "100%",
|
|
3866
|
+
minHeight: 0,
|
|
3867
|
+
overflow: "hidden"
|
|
2340
3868
|
} }, h("div", { style: {
|
|
2341
3869
|
display: "flex",
|
|
2342
3870
|
alignItems: "center",
|
|
@@ -2507,7 +4035,18 @@ window.__ModuleLoader__.load({
|
|
|
2507
4035
|
}) : h("span", { style: {
|
|
2508
4036
|
color: T.text2,
|
|
2509
4037
|
fontSize: 11.5
|
|
2510
|
-
} }, "(暂无)")) : null
|
|
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, {
|
|
2511
4050
|
active,
|
|
2512
4051
|
api,
|
|
2513
4052
|
runId: activeRun ? activeRun.id : null,
|
|
@@ -2518,11 +4057,12 @@ window.__ModuleLoader__.load({
|
|
|
2518
4057
|
api,
|
|
2519
4058
|
onRefresh: refresh,
|
|
2520
4059
|
sessionId: props.sessionId,
|
|
4060
|
+
openArtifact: props.openArtifact,
|
|
2521
4061
|
onShowRun: (rid) => {
|
|
2522
4062
|
setTab("pipeline");
|
|
2523
4063
|
setRunId(rid);
|
|
2524
4064
|
}
|
|
2525
|
-
}));
|
|
4065
|
+
})));
|
|
2526
4066
|
}
|
|
2527
4067
|
if (typeof document !== "undefined") {
|
|
2528
4068
|
const styleEl = document.createElement("style");
|
|
@@ -2532,10 +4072,65 @@ window.__ModuleLoader__.load({
|
|
|
2532
4072
|
@keyframes tf-flow { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -28 } }`;
|
|
2533
4073
|
document.head.appendChild(styleEl);
|
|
2534
4074
|
}
|
|
2535
|
-
/**
|
|
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
|
+
*/
|
|
2536
4082
|
async function apply(ctx) {
|
|
2537
4083
|
await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
|
|
2538
4084
|
const teamflow = ctx.get("remote.teamflow");
|
|
4085
|
+
const openResourceSafe = (address, label, quiet) => {
|
|
4086
|
+
try {
|
|
4087
|
+
const sidebarRight = ctx.get("sidebarRight");
|
|
4088
|
+
if (!sidebarRight || typeof sidebarRight.openResource !== "function") {
|
|
4089
|
+
if (!quiet) console.warn("[teamflow] 右侧栏服务不可用", label);
|
|
4090
|
+
return false;
|
|
4091
|
+
}
|
|
4092
|
+
sidebarRight.openResource(address);
|
|
4093
|
+
return true;
|
|
4094
|
+
} catch (e) {
|
|
4095
|
+
if (!quiet) console.warn("[teamflow] 打开右侧栏失败", label, e && e.message);
|
|
4096
|
+
return false;
|
|
4097
|
+
}
|
|
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));
|
|
2539
4134
|
ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
2540
4135
|
name: "conversation.view",
|
|
2541
4136
|
id: "teamflow",
|
|
@@ -2544,7 +4139,9 @@ window.__ModuleLoader__.load({
|
|
|
2544
4139
|
inject: (sessionId) => ({
|
|
2545
4140
|
sessionId,
|
|
2546
4141
|
remote: teamflow,
|
|
2547
|
-
sessions: ctx.get("sessions")
|
|
4142
|
+
sessions: ctx.get("sessions"),
|
|
4143
|
+
openArtifact,
|
|
4144
|
+
openResource: openResourceSafe
|
|
2548
4145
|
})
|
|
2549
4146
|
}, TeamFlowView));
|
|
2550
4147
|
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|