dsh-plugin-teamflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,2412 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-teamflow",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
29
+ let react = require("react");
30
+ react = __toESM(react, 1);
31
+ //#region descriptors.ts
32
+ /** 统一 strict codec(本插件所有参数/结果均为自由 JSON)。 */
33
+ const strict = {
34
+ mode: "strict",
35
+ typeSymbol: "dsh-plugin-teamflow/types#Json",
36
+ schema: { parse: (value) => value }
37
+ };
38
+ const p = (name) => ({
39
+ name,
40
+ wire: name,
41
+ source: "json",
42
+ codec: strict
43
+ });
44
+ const TEAMFLOW_DESCRIPTORS = Object.freeze([
45
+ {
46
+ id: "dsh-plugin-teamflow#teamflow/ping",
47
+ service: "teamflow",
48
+ namespace: "teamflow",
49
+ method: "ping",
50
+ invocation: { kind: "direct" },
51
+ parameters: [],
52
+ result: strict
53
+ },
54
+ {
55
+ id: "dsh-plugin-teamflow#teamflow/list",
56
+ service: "teamflow",
57
+ namespace: "teamflow",
58
+ method: "list",
59
+ invocation: { kind: "direct" },
60
+ parameters: [p("sessionId")],
61
+ result: strict
62
+ },
63
+ {
64
+ id: "dsh-plugin-teamflow#teamflow/snapshot",
65
+ service: "teamflow",
66
+ namespace: "teamflow",
67
+ method: "snapshot",
68
+ invocation: { kind: "direct" },
69
+ parameters: [p("runId"), p("sessionId")],
70
+ result: strict
71
+ },
72
+ {
73
+ id: "dsh-plugin-teamflow#teamflow/start",
74
+ service: "teamflow",
75
+ namespace: "teamflow",
76
+ method: "start",
77
+ invocation: { kind: "direct" },
78
+ parameters: [
79
+ p("sessionId"),
80
+ p("requirement"),
81
+ p("options")
82
+ ],
83
+ result: strict
84
+ },
85
+ {
86
+ id: "dsh-plugin-teamflow#teamflow/cancel",
87
+ service: "teamflow",
88
+ namespace: "teamflow",
89
+ method: "cancel",
90
+ invocation: { kind: "direct" },
91
+ parameters: [p("runId")],
92
+ result: strict
93
+ },
94
+ {
95
+ id: "dsh-plugin-teamflow#teamflow/backlog",
96
+ service: "teamflow",
97
+ namespace: "teamflow",
98
+ method: "backlog",
99
+ invocation: { kind: "direct" },
100
+ parameters: [p("sessionId")],
101
+ result: strict
102
+ },
103
+ {
104
+ id: "dsh-plugin-teamflow#teamflow/backlogUpdate",
105
+ service: "teamflow",
106
+ namespace: "teamflow",
107
+ method: "backlogUpdate",
108
+ invocation: { kind: "direct" },
109
+ parameters: [
110
+ p("kind"),
111
+ p("id"),
112
+ p("to"),
113
+ p("sessionId"),
114
+ p("reason")
115
+ ],
116
+ result: strict
117
+ },
118
+ {
119
+ id: "dsh-plugin-teamflow#teamflow/assign",
120
+ service: "teamflow",
121
+ namespace: "teamflow",
122
+ method: "assign",
123
+ invocation: { kind: "direct" },
124
+ parameters: [
125
+ p("kind"),
126
+ p("id"),
127
+ p("role"),
128
+ p("assignee"),
129
+ p("sessionId")
130
+ ],
131
+ result: strict
132
+ },
133
+ {
134
+ id: "dsh-plugin-teamflow#teamflow/pause",
135
+ service: "teamflow",
136
+ namespace: "teamflow",
137
+ method: "pause",
138
+ invocation: { kind: "direct" },
139
+ parameters: [p("sessionId")],
140
+ result: strict
141
+ },
142
+ {
143
+ id: "dsh-plugin-teamflow#teamflow/resumeSession",
144
+ service: "teamflow",
145
+ namespace: "teamflow",
146
+ method: "resumeSession",
147
+ invocation: { kind: "direct" },
148
+ parameters: [p("sessionId")],
149
+ result: strict
150
+ },
151
+ {
152
+ id: "dsh-plugin-teamflow#teamflow/listTeams",
153
+ service: "teamflow",
154
+ namespace: "teamflow",
155
+ method: "listTeams",
156
+ invocation: { kind: "direct" },
157
+ parameters: [p("sessionId")],
158
+ result: strict
159
+ },
160
+ {
161
+ id: "dsh-plugin-teamflow#teamflow/selectTeam",
162
+ service: "teamflow",
163
+ namespace: "teamflow",
164
+ method: "selectTeam",
165
+ invocation: { kind: "direct" },
166
+ parameters: [p("sessionId"), p("teamId")],
167
+ result: strict
168
+ },
169
+ {
170
+ id: "dsh-plugin-teamflow#teamflow/getActiveTeam",
171
+ service: "teamflow",
172
+ namespace: "teamflow",
173
+ method: "getActiveTeam",
174
+ invocation: { kind: "direct" },
175
+ parameters: [p("sessionId")],
176
+ result: strict
177
+ },
178
+ {
179
+ id: "dsh-plugin-teamflow#teamflow/clearTeam",
180
+ service: "teamflow",
181
+ namespace: "teamflow",
182
+ method: "clearTeam",
183
+ invocation: { kind: "direct" },
184
+ parameters: [p("sessionId")],
185
+ result: strict
186
+ },
187
+ {
188
+ id: "dsh-plugin-teamflow#teamflow/resume",
189
+ service: "teamflow",
190
+ namespace: "teamflow",
191
+ method: "resume",
192
+ invocation: { kind: "direct" },
193
+ parameters: [p("runId"), p("sessionId")],
194
+ result: strict
195
+ },
196
+ {
197
+ id: "dsh-plugin-teamflow#teamflow/stageDetail",
198
+ service: "teamflow",
199
+ namespace: "teamflow",
200
+ method: "stageDetail",
201
+ invocation: { kind: "direct" },
202
+ parameters: [
203
+ p("runId"),
204
+ p("seq"),
205
+ p("sessionId")
206
+ ],
207
+ result: strict
208
+ },
209
+ {
210
+ id: "dsh-plugin-teamflow#teamflow/itemDetail",
211
+ service: "teamflow",
212
+ namespace: "teamflow",
213
+ method: "itemDetail",
214
+ invocation: { kind: "direct" },
215
+ parameters: [
216
+ p("kind"),
217
+ p("id"),
218
+ p("sessionId")
219
+ ],
220
+ result: strict
221
+ }
222
+ ]);
223
+ /** client 端 $mount 使用的贡献对象。 */
224
+ const TEAMFLOW_REMOTE_CONTRIBUTION = Object.freeze({
225
+ package: "dsh-plugin-teamflow",
226
+ descriptors: TEAMFLOW_DESCRIPTORS
227
+ });
228
+ //#endregion
229
+ //#region client/index.tsx
230
+ /**
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 切换
239
+ *
240
+ * 视觉:全部使用 DSH 主题 token(--dsw-alias-*,自动适配深浅色)。
241
+ * Remote 接入:$mount 后 ctx.get('remote.teamflow') 取全局实例传给组件
242
+ * (不能 inject 'remote.teamflow'——自挂载自注入会让 fiber 死锁)。
243
+ */
244
+ const inject = [
245
+ "remote",
246
+ "slots",
247
+ "sessions",
248
+ "locale"
249
+ ];
250
+ const T = {
251
+ bg: "var(--dsw-alias-bg-base)",
252
+ layer1: "var(--dsw-alias-bg-layer-1)",
253
+ layer2: "var(--dsw-alias-bg-layer-2)",
254
+ border: "var(--dsw-alias-border-l1)",
255
+ border2: "var(--dsw-alias-border-l2)",
256
+ brand: "var(--dsw-alias-brand-primary)",
257
+ text: "var(--dsw-alias-label-primary)",
258
+ text2: "var(--dsw-alias-label-secondary)",
259
+ error: "var(--dsw-alias-state-error-primary)",
260
+ success: "var(--dsw-alias-state-success-primary)",
261
+ warn: "var(--dsw-alias-state-warn-primary)"
262
+ };
263
+ const STATUS_TEXT = {
264
+ created: "立项",
265
+ "in-progress": "进行中",
266
+ "pending-acceptance": "待验收",
267
+ accepted: "已验收",
268
+ closed: "已关闭",
269
+ pending: "待办",
270
+ running: "开发中",
271
+ testable: "待测试",
272
+ testing: "测试中",
273
+ rework: "打回",
274
+ "needs-human": "需人工",
275
+ cancelled: "已关闭",
276
+ open: "待认领",
277
+ claimed: "处理中",
278
+ fixed: "已修复待验",
279
+ verified: "已关闭",
280
+ reopened: "重开",
281
+ done: "已完成",
282
+ completed: "已完成",
283
+ failed: "失败",
284
+ interrupted: "已中断",
285
+ superseded: "已取代"
286
+ };
287
+ const STATUS_COLOR = {
288
+ created: T.text2,
289
+ pending: T.text2,
290
+ open: T.text2,
291
+ "in-progress": T.brand,
292
+ running: T.brand,
293
+ claimed: T.brand,
294
+ testing: T.brand,
295
+ fixed: T.brand,
296
+ reopened: "#8250df",
297
+ "pending-acceptance": T.warn,
298
+ testable: T.warn,
299
+ accepted: T.success,
300
+ verified: T.success,
301
+ completed: T.success,
302
+ done: T.success,
303
+ rework: T.error,
304
+ failed: T.error,
305
+ "needs-human": T.error,
306
+ cancelled: T.text2,
307
+ closed: T.text2,
308
+ interrupted: T.warn,
309
+ superseded: T.text2
310
+ };
311
+ const PHASE_ICON = {
312
+ "PRD 产品需求": "📋",
313
+ "UI/UX 设计": "🎨",
314
+ "架构规划": "🏗️",
315
+ "技术方案": "📐",
316
+ 开发: "💻",
317
+ "QA 测试": "🧪",
318
+ "产品验收": "✅"
319
+ };
320
+ const RUN_STATUS_TEXT = {
321
+ pending: "等待中",
322
+ running: "进行中",
323
+ completed: "已完成",
324
+ failed: "失败",
325
+ cancelled: "已取消",
326
+ interrupted: "已中断",
327
+ superseded: "已取代"
328
+ };
329
+ const COLUMNS = {
330
+ req: [
331
+ "created",
332
+ "in-progress",
333
+ "pending-acceptance",
334
+ "accepted",
335
+ "closed",
336
+ "needs-human"
337
+ ],
338
+ task: [
339
+ "pending",
340
+ "running",
341
+ "testable",
342
+ "testing",
343
+ "pending-acceptance",
344
+ "accepted",
345
+ "rework",
346
+ "needs-human",
347
+ "cancelled"
348
+ ],
349
+ bug: [
350
+ "open",
351
+ "claimed",
352
+ "fixed",
353
+ "verified",
354
+ "reopened",
355
+ "needs-human"
356
+ ]
357
+ };
358
+ const KIND_TITLE = {
359
+ req: "需求",
360
+ task: "任务",
361
+ bug: "缺陷"
362
+ };
363
+ const h = react.default.createElement;
364
+ const MONO = "ui-monospace, SFMono-Regular, Consolas, \"Cascadia Mono\", monospace";
365
+ const SANS = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif";
366
+ const flexRow = {
367
+ display: "flex",
368
+ alignItems: "center",
369
+ gap: 6,
370
+ flexWrap: "wrap"
371
+ };
372
+ /** 状态徽章:半透明底 + 状态色文字 + 圆角 pill。 */
373
+ const chip = (text, color, opts = {}) => h("span", { style: {
374
+ display: "inline-flex",
375
+ alignItems: "center",
376
+ gap: 4,
377
+ padding: "1px 8px",
378
+ borderRadius: 999,
379
+ fontSize: 11,
380
+ fontWeight: 500,
381
+ lineHeight: "16px",
382
+ background: `color-mix(in srgb, ${color} 14%, transparent)`,
383
+ color,
384
+ whiteSpace: "nowrap",
385
+ ...opts.style || {}
386
+ } }, opts.dot ? h("span", { style: {
387
+ width: 5,
388
+ height: 5,
389
+ borderRadius: 999,
390
+ background: color,
391
+ display: "inline-block"
392
+ } }) : null, text);
393
+ /** 可折叠长文本:默认只显示前几行预览,「展开全文」/「收起」双向切换(数据不动,纯展示层——summary/需求原文等富文本不再铺满抽屉)。 */
394
+ function FoldableText({ text, charLimit = 280, lineLimit = 5, style }) {
395
+ const [open, setOpen] = react.default.useState(false);
396
+ if (!text) return null;
397
+ const t = String(text);
398
+ const lines = t.split("\n");
399
+ const compact = lines.length <= lineLimit && t.length <= charLimit;
400
+ const body = (txt) => h("div", { style: {
401
+ fontSize: 11.5,
402
+ color: T.text,
403
+ lineHeight: 1.6,
404
+ whiteSpace: "pre-wrap",
405
+ wordBreak: "break-word",
406
+ ...style || {}
407
+ } }, txt);
408
+ if (compact) return body(t);
409
+ if (open) return h("div", null, body(t), h("button", {
410
+ onClick: () => setOpen(false),
411
+ title: "收起全文",
412
+ style: {
413
+ marginTop: 3,
414
+ font: "inherit",
415
+ fontSize: 10.5,
416
+ fontWeight: 600,
417
+ color: T.text2,
418
+ background: "transparent",
419
+ border: "none",
420
+ padding: 0,
421
+ cursor: "pointer"
422
+ }
423
+ }, "收起"));
424
+ const pre = lines.length > lineLimit ? lines.slice(0, lineLimit).join("\n") : t.slice(0, charLimit);
425
+ const more = lines.length > lineLimit ? `… +${lines.length - lineLimit} 行` : "…";
426
+ return h("div", null, body(pre), h("button", {
427
+ onClick: () => setOpen(true),
428
+ title: "点击查看全文",
429
+ style: {
430
+ marginTop: 3,
431
+ font: "inherit",
432
+ fontSize: 10.5,
433
+ fontWeight: 600,
434
+ color: T.brand,
435
+ background: "transparent",
436
+ border: "none",
437
+ padding: 0,
438
+ cursor: "pointer"
439
+ }
440
+ }, `展开全文${more}`));
441
+ }
442
+ function fmtDur(a, b) {
443
+ if (!a) return "";
444
+ const ms = (b || Date.now()) - a;
445
+ if (ms < 0) return "";
446
+ const s = Math.floor(ms / 1e3);
447
+ if (s < 60) return `${s}s`;
448
+ return `${Math.floor(s / 60)}m${s % 60}s`;
449
+ }
450
+ function fmtTokens(n) {
451
+ if (n === null || n === void 0) return "";
452
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
453
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
454
+ return String(n);
455
+ }
456
+ /** 官方口径计费合计(billed input + output)。 */
457
+ function totalTokens(u) {
458
+ return (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0);
459
+ }
460
+ /** 官方口径工具函数(与 DeepSeek usage 账单对齐,零额外概念):
461
+ * - u.input : 输入(缓存未命中)
462
+ * - u.cacheRead : 输入(缓存命中)
463
+ * - u.cacheWrite : 输入写入缓存
464
+ * - u.output : 输出
465
+ * billed input = input + cacheRead + cacheWrite;命中率 = cacheRead / (input + cacheRead)。
466
+ */
467
+ function hitRate(u) {
468
+ const total = (u.input || 0) + (u.cacheRead || 0);
469
+ return total > 0 ? Math.round((u.cacheRead || 0) / total * 100) : null;
470
+ }
471
+ function usageDetail(s) {
472
+ const k = (n) => n === null || n === void 0 ? "—" : fmtTokens(n);
473
+ if (s.usage) {
474
+ const u = s.usage;
475
+ const hit = hitRate(u);
476
+ return `输入(未命中) ${k(u.input)} / 输入(命中) ${k(u.cacheRead)} / 写缓存 ${k(u.cacheWrite)} / 输出 ${k(u.output)} · ${u.calls} 次调用${hit !== null ? ` · 缓存命中 ${hit}%` : ""}`;
477
+ }
478
+ return "无 usage 明细";
479
+ }
480
+ /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
481
+ function stageUsageLine(s) {
482
+ const u = s && s.usage;
483
+ if (u && (u.input || u.cacheRead || u.cacheWrite || u.output)) {
484
+ const hit = hitRate(u);
485
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""}`;
486
+ }
487
+ return null;
488
+ }
489
+ const ROLE_NAME = {
490
+ pm: "产品",
491
+ design: "设计",
492
+ arch: "架构",
493
+ tech: "方案",
494
+ dev: "开发",
495
+ qa: "测试",
496
+ acceptance: "验收",
497
+ other: "其他"
498
+ };
499
+ const roleUsage = (u) => {
500
+ if (!u) return "";
501
+ const hit = hitRate(u);
502
+ return `⇅${fmtTokens(u.input || 0)}/${fmtTokens(u.cacheRead || 0)}·⬆${fmtTokens(u.output || 0)}${hit !== null ? `·${hit}%` : ""}`;
503
+ };
504
+ /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
505
+ function byRoleLine(task) {
506
+ const roles = task && task.byRole || {};
507
+ 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(" · ");
508
+ }
509
+ /** 多阶段 usage 汇总(官方口径)。 */
510
+ function totalUsage(stages) {
511
+ const t = {
512
+ input: 0,
513
+ cacheRead: 0,
514
+ cacheWrite: 0,
515
+ output: 0,
516
+ calls: 0
517
+ };
518
+ for (const s of stages || []) {
519
+ const u = s && s.usage;
520
+ if (!u) continue;
521
+ t.input += u.input || 0;
522
+ t.cacheRead += u.cacheRead || 0;
523
+ t.cacheWrite += u.cacheWrite || 0;
524
+ t.output += u.output || 0;
525
+ t.calls += u.calls || 0;
526
+ }
527
+ return t;
528
+ }
529
+ const stText = (s) => STATUS_TEXT[s] || s;
530
+ const stColor = (s) => STATUS_COLOR[s] || T.text2;
531
+ const NODE_W = 300;
532
+ const PHASE_GAP = 88;
533
+ const WAVE = 56;
534
+ const CARD_H = 54;
535
+ const CARD_H_RUN = 64;
536
+ const HEAD_H = 38;
537
+ const PAD_L = 46;
538
+ const PAD_R = 72;
539
+ const PAD_B = 46;
540
+ const cardH = (s) => s.status === "running" ? CARD_H_RUN : CARD_H;
541
+ /** 横向布局:相位从左至右一排,上下轻微波浪错位(弧线自然成形)+ 绝对定位节点 + 连接锚点。
542
+ * 返回 nodes/conns/worldW/worldH。 */
543
+ function layoutFlow(groups, viewW) {
544
+ const nodes = [];
545
+ const conns = [];
546
+ let maxH = 0;
547
+ groups.forEach((g, i) => {
548
+ const anyRun = g.stages.some((s) => s.status === "running");
549
+ const anyFail = g.stages.some((s) => s.status === "failed" || s.status === "needs-human" || s.status === "cancelled");
550
+ const allDone = g.stages.length > 0 && g.stages.every((s) => s.status === "done");
551
+ const headColor = anyRun ? T.brand : anyFail ? T.error : allDone ? T.success : T.text2;
552
+ const h = 48 + g.stages.reduce((a, s) => a + cardH(s), 0) + Math.max(0, g.stages.length - 1) * 7;
553
+ maxH = Math.max(maxH, h);
554
+ nodes.push({
555
+ i,
556
+ phase: g.phase,
557
+ stages: g.stages,
558
+ left: PAD_L + i * 388,
559
+ h,
560
+ headColor
561
+ });
562
+ });
563
+ const axisY = 70 + maxH / 2;
564
+ nodes.forEach((n) => {
565
+ n.top = axisY + (n.i % 2 === 0 ? -1 : 1) * (WAVE / 2) - n.h / 2;
566
+ n.centerY = n.top + n.h / 2;
567
+ });
568
+ for (let i = 1; i < nodes.length; i++) {
569
+ const p = nodes[i - 1], n = nodes[i];
570
+ conns.push({
571
+ x1: p.left + NODE_W,
572
+ y1: p.centerY,
573
+ x2: n.left,
574
+ y2: n.centerY,
575
+ color: n.headColor
576
+ });
577
+ }
578
+ return {
579
+ nodes,
580
+ conns,
581
+ worldW: PAD_L + nodes.length * 388 - PHASE_GAP + PAD_R,
582
+ worldH: 98 + maxH + PAD_B
583
+ };
584
+ }
585
+ /** S 形弧线路径(从左至右,前后锚点沿水平方向缓进出、垂直方向呈现波浪弧)。 */
586
+ function connPath(c) {
587
+ const dx = Math.max(48, (c.x2 - c.x1) / 2);
588
+ return `M ${c.x1} ${c.y1} C ${c.x1 + dx} ${c.y1}, ${c.x2 - dx} ${c.y2}, ${c.x2} ${c.y2}`;
589
+ }
590
+ function FlowStageCard(s, key, onOpen) {
591
+ const color = stColor(s.status);
592
+ const running = s.status === "running";
593
+ const usage = stageUsageLine(s);
594
+ return h("div", {
595
+ key,
596
+ title: `${s.label} —— 点击查看阶段详情`,
597
+ onMouseDown: (e) => e.stopPropagation(),
598
+ onClick: () => onOpen && onOpen(s),
599
+ style: {
600
+ boxSizing: "border-box",
601
+ height: cardH(s),
602
+ borderRadius: 10,
603
+ position: "relative",
604
+ overflow: "hidden",
605
+ display: "flex",
606
+ flexDirection: "column",
607
+ justifyContent: "center",
608
+ gap: 3,
609
+ padding: "6px 11px 6px 13px",
610
+ background: `linear-gradient(135deg, color-mix(in srgb, ${color} 8%, ${T.layer1}), ${T.layer1} 64%)`,
611
+ border: `1px solid ${T.border}`,
612
+ borderLeft: `3px solid ${color}`,
613
+ opacity: s.status === "pending" ? .56 : 1,
614
+ cursor: "pointer",
615
+ transition: "transform .12s ease, box-shadow .12s ease, border-color .12s ease",
616
+ boxShadow: running ? `0 0 0 1px color-mix(in srgb, ${color} 32%, transparent), 0 6px 18px color-mix(in srgb, ${color} 15%, transparent)` : "0 1px 2px rgba(0,0,0,.05)"
617
+ }
618
+ }, h("div", { style: {
619
+ display: "flex",
620
+ alignItems: "center",
621
+ gap: 6
622
+ } }, running ? h("span", { style: {
623
+ width: 7,
624
+ height: 7,
625
+ borderRadius: 999,
626
+ background: color,
627
+ animation: "tf-pulse 1.15s ease-in-out infinite"
628
+ } }) : h("span", { style: {
629
+ width: 6,
630
+ height: 6,
631
+ borderRadius: 2,
632
+ background: color
633
+ } }), h("span", {
634
+ title: s.label,
635
+ style: {
636
+ flex: 1,
637
+ minWidth: 0,
638
+ fontSize: 12,
639
+ fontWeight: 600,
640
+ color: T.text,
641
+ overflow: "hidden",
642
+ textOverflow: "ellipsis",
643
+ whiteSpace: "nowrap"
644
+ }
645
+ }, s.label), chip(stText(s.status), color, { dot: true }), h("span", { style: {
646
+ color: T.text2,
647
+ fontSize: 11,
648
+ opacity: .5
649
+ } }, "↗")), h("div", { style: {
650
+ display: "flex",
651
+ alignItems: "center",
652
+ gap: 8,
653
+ paddingLeft: 13,
654
+ fontSize: 10.5,
655
+ color: T.text2,
656
+ fontFamily: MONO,
657
+ fontVariantNumeric: "tabular-nums"
658
+ } }, s.startedAt ? h("span", {}, fmtDur(s.startedAt, s.endedAt)) : h("span", {}, "—"), usage ? h("span", {
659
+ title: usageDetail(s),
660
+ style: {
661
+ marginLeft: "auto",
662
+ color
663
+ }
664
+ }, usage) : null), running ? h("div", { style: {
665
+ position: "absolute",
666
+ left: 5,
667
+ right: 5,
668
+ bottom: 3,
669
+ height: 2,
670
+ borderRadius: 2,
671
+ overflow: "hidden",
672
+ background: `color-mix(in srgb, ${color} 20%, transparent)`
673
+ } }, h("div", { style: {
674
+ height: "100%",
675
+ width: "42%",
676
+ borderRadius: 2,
677
+ background: color,
678
+ animation: "tf-shimmer 1.1s linear infinite"
679
+ } })) : null);
680
+ }
681
+ function FlowNode(node, onOpen) {
682
+ const g = node;
683
+ const running = g.stages.some((s) => s.status === "running");
684
+ return h("div", {
685
+ key: "n" + g.i,
686
+ style: {
687
+ position: "absolute",
688
+ left: g.left,
689
+ top: g.top,
690
+ width: NODE_W,
691
+ height: g.h,
692
+ boxSizing: "border-box",
693
+ display: "flex",
694
+ flexDirection: "column"
695
+ }
696
+ }, h("div", { style: {
697
+ position: "absolute",
698
+ top: -8,
699
+ left: 12,
700
+ width: 22,
701
+ height: 22,
702
+ borderRadius: 7,
703
+ zIndex: 2,
704
+ display: "flex",
705
+ alignItems: "center",
706
+ justifyContent: "center",
707
+ fontFamily: MONO,
708
+ fontSize: 10.5,
709
+ fontWeight: 800,
710
+ color: g.headColor,
711
+ background: `color-mix(in srgb, ${g.headColor} 16%, ${T.layer1})`,
712
+ border: `1px solid color-mix(in srgb, ${g.headColor} 36%, transparent)`,
713
+ boxShadow: "0 2px 8px rgba(0,0,0,.14)"
714
+ } }, String(g.i + 1).padStart(2, "0")), h("div", { style: {
715
+ height: HEAD_H,
716
+ boxSizing: "border-box",
717
+ borderRadius: 11,
718
+ padding: "0 11px",
719
+ display: "flex",
720
+ alignItems: "center",
721
+ gap: 7,
722
+ color: g.headColor,
723
+ fontSize: 12.5,
724
+ fontWeight: 700,
725
+ background: `linear-gradient(90deg, color-mix(in srgb, ${g.headColor} 13%, transparent), color-mix(in srgb, ${g.headColor} 5%, transparent))`,
726
+ border: `1px solid color-mix(in srgb, ${g.headColor} 30%, transparent)`
727
+ } }, h("span", { style: { fontSize: 13.5 } }, PHASE_ICON[g.phase] || "⚙️"), h("span", { style: {
728
+ flex: 1,
729
+ overflow: "hidden",
730
+ textOverflow: "ellipsis",
731
+ whiteSpace: "nowrap"
732
+ } }, g.phase), g.stages.length > 1 ? h("span", { style: {
733
+ fontFamily: MONO,
734
+ fontSize: 10,
735
+ fontWeight: 800,
736
+ background: `color-mix(in srgb, ${g.headColor} 16%, transparent)`,
737
+ borderRadius: 999,
738
+ padding: "0 7px",
739
+ lineHeight: "16px"
740
+ } }, `×${g.stages.length}`) : null, running ? h("span", { style: {
741
+ width: 8,
742
+ height: 8,
743
+ borderRadius: 999,
744
+ background: g.headColor,
745
+ animation: "tf-pulse 1.4s ease-in-out infinite"
746
+ } }) : null), h("div", { style: {
747
+ flex: 1,
748
+ minHeight: 0,
749
+ display: "flex",
750
+ flexDirection: "column",
751
+ gap: 7,
752
+ paddingTop: 10
753
+ } }, g.stages.map((s) => FlowStageCard(s, s.seq, onOpen))));
754
+ }
755
+ /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。 */
756
+ function StageDetailDrawer({ det, onClose, sessionId, sessions }) {
757
+ const st = det.stage;
758
+ const d = det.data;
759
+ const color = st && st.status ? stColor(st.status) : T.text2;
760
+ const ownerSession = d && d.ownerSession ? String(d.ownerSession) : null;
761
+ const mySession = sessionId ? String(sessionId) : null;
762
+ const crossSession = !!ownerSession && !!mySession && ownerSession !== mySession;
763
+ const hasChild = !!(!crossSession && d && d.childId && sessions && typeof sessions.openSubagent === "function");
764
+ const openChild = () => {
765
+ if (crossSession || !hasChild) return;
766
+ try {
767
+ sessions.openSubagent({
768
+ parentSessionId: ownerSession || sessionId,
769
+ childSessionId: d.childId,
770
+ mode: "one-shot"
771
+ });
772
+ } catch (e) {}
773
+ };
774
+ const outText = d && d.output ? d.output : d && d.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${d.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
775
+ const closeBtn = {
776
+ font: "inherit",
777
+ width: 26,
778
+ height: 26,
779
+ borderRadius: 8,
780
+ cursor: "pointer",
781
+ border: `1px solid ${T.border}`,
782
+ background: "transparent",
783
+ color: T.text2,
784
+ display: "flex",
785
+ alignItems: "center",
786
+ justifyContent: "center",
787
+ fontSize: 13,
788
+ lineHeight: 1
789
+ };
790
+ const pRef = react.default.useRef(null);
791
+ react.default.useEffect(() => {
792
+ const el = pRef.current;
793
+ if (!el) return;
794
+ const stop = (e) => e.stopPropagation();
795
+ el.addEventListener("wheel", stop, { passive: true });
796
+ return () => el.removeEventListener("wheel", stop);
797
+ }, []);
798
+ return h("div", {
799
+ ref: pRef,
800
+ style: {
801
+ position: "absolute",
802
+ top: 10,
803
+ right: 12,
804
+ bottom: 10,
805
+ width: 384,
806
+ zIndex: 8,
807
+ borderRadius: 14,
808
+ overflow: "hidden",
809
+ display: "flex",
810
+ flexDirection: "column",
811
+ border: `1px solid ${T.border}`,
812
+ background: `color-mix(in srgb, ${T.layer1} 90%, transparent)`,
813
+ backdropFilter: "blur(14px)",
814
+ boxShadow: "0 14px 48px rgba(0,0,0,.30)"
815
+ }
816
+ }, h("div", { style: {
817
+ display: "flex",
818
+ alignItems: "center",
819
+ gap: 9,
820
+ padding: "12px 14px",
821
+ borderBottom: `1px solid ${T.border}`,
822
+ background: `linear-gradient(135deg, color-mix(in srgb, ${color} 14%, transparent), transparent 62%)`
823
+ } }, h("span", { style: { fontSize: 17 } }, PHASE_ICON[st && st.phase] || "⚙️"), h("div", { style: {
824
+ flex: 1,
825
+ minWidth: 0
826
+ } }, h("div", {
827
+ title: st ? st.label : void 0,
828
+ style: {
829
+ fontSize: 12.5,
830
+ fontWeight: 700,
831
+ color: T.text,
832
+ overflow: "hidden",
833
+ textOverflow: "ellipsis",
834
+ whiteSpace: "nowrap"
835
+ }
836
+ }, st ? st.label : "阶段详情"), h("div", { style: {
837
+ fontSize: 10.5,
838
+ color: T.text2,
839
+ marginTop: 1,
840
+ fontFamily: MONO,
841
+ fontVariantNumeric: "tabular-nums"
842
+ } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stText(st.status), color, { dot: true }) : null, h("button", {
843
+ onClick: onClose,
844
+ style: closeBtn,
845
+ title: "关闭"
846
+ }, "✕")), h("div", { style: {
847
+ flex: 1,
848
+ overflowY: "auto",
849
+ padding: "12px 14px",
850
+ display: "flex",
851
+ flexDirection: "column",
852
+ gap: 11
853
+ } }, h("div", { style: {
854
+ display: "flex",
855
+ flexDirection: "column",
856
+ gap: 4
857
+ } }, h("span", { style: {
858
+ fontSize: 10.5,
859
+ fontWeight: 700,
860
+ color: T.text2,
861
+ letterSpacing: .3
862
+ } }, "TOKEN · 官方口径"), h("span", { style: {
863
+ fontSize: 11.5,
864
+ fontFamily: MONO,
865
+ color: T.text,
866
+ lineHeight: 1.65
867
+ } }, usageDetail(d || st || {}))), h("div", { style: {
868
+ display: "flex",
869
+ flexDirection: "column",
870
+ gap: 5
871
+ } }, h("button", {
872
+ onClick: openChild,
873
+ disabled: !hasChild,
874
+ title: crossSession ? `该子代理由会话 ${ownerSession.slice(-6)} 发起,跨会话跳转暂不支持——请打开其发起会话的团队工作台查看` : hasChild ? "跳转到该阶段子代理会话(完整推理与工具调用轨迹);跳转后请切换「对话」tab 查看" : "该阶段无可用子代理会话",
875
+ style: {
876
+ font: "inherit",
877
+ fontSize: 12,
878
+ fontWeight: 600,
879
+ padding: "8px 12px",
880
+ borderRadius: 9,
881
+ cursor: "pointer",
882
+ display: "flex",
883
+ alignItems: "center",
884
+ gap: 6,
885
+ justifyContent: "center",
886
+ border: `1px solid color-mix(in srgb, ${T.brand} 40%, transparent)`,
887
+ background: `color-mix(in srgb, ${T.brand} 12%, transparent)`,
888
+ color: T.brand,
889
+ opacity: hasChild ? 1 : .45
890
+ }
891
+ }, "🎬 跳转子代理会话"), crossSession ? h("div", { style: {
892
+ fontSize: 10.5,
893
+ color: T.text2,
894
+ textAlign: "center",
895
+ lineHeight: 1.55
896
+ } }, `跨会话暂不支持:该子代理由会话 ${ownerSession ? ownerSession.slice(-6) : ""} 发起。如需查看轨迹,请打开其发起会话的团队工作台。`) : hasChild ? h("div", { style: {
897
+ fontSize: 10.5,
898
+ color: T.text2,
899
+ textAlign: "center",
900
+ lineHeight: 1.55
901
+ } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), h("div", { style: {
902
+ display: "flex",
903
+ flexDirection: "column",
904
+ gap: 5
905
+ } }, h("span", { style: {
906
+ fontSize: 10.5,
907
+ fontWeight: 700,
908
+ color: T.text2,
909
+ letterSpacing: .3
910
+ } }, "📄 阶段性产物"), h("div", { style: {
911
+ whiteSpace: "pre-wrap",
912
+ wordBreak: "break-word",
913
+ fontSize: 12,
914
+ lineHeight: 1.62,
915
+ color: T.text,
916
+ background: `color-mix(in srgb, ${T.layer2} 55%, transparent)`,
917
+ border: `1px solid ${T.border}`,
918
+ borderRadius: 10,
919
+ padding: "10px 12px",
920
+ maxHeight: 240,
921
+ overflowY: "auto"
922
+ } }, outText))));
923
+ }
924
+ function PipelinePanel({ active, api, runId, sessionId, sessions }) {
925
+ if (!active) return h("div", { style: {
926
+ color: T.text2,
927
+ fontSize: 13,
928
+ padding: "28px 20px",
929
+ textAlign: "center"
930
+ } }, h("div", { style: {
931
+ fontSize: 28,
932
+ marginBottom: 8
933
+ } }, "🏭"), "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求");
934
+ const groups = [];
935
+ for (const st of active.stages || []) {
936
+ let g = groups.length ? groups[groups.length - 1] : null;
937
+ if (!g || g.phase !== st.phase) {
938
+ g = {
939
+ phase: st.phase,
940
+ stages: []
941
+ };
942
+ groups.push(g);
943
+ }
944
+ g.stages.push(st);
945
+ }
946
+ const wrapRef = react.default.useRef(null);
947
+ const [vw, setVw] = react.default.useState(900);
948
+ const [view, setView] = react.default.useState({
949
+ x: 0,
950
+ y: 44,
951
+ s: 1
952
+ });
953
+ const [grabbing, setGrabbing] = react.default.useState(false);
954
+ const dragRef = react.default.useRef(null);
955
+ const fittedRef = react.default.useRef(false);
956
+ const [det, setDet] = react.default.useState(null);
957
+ react.default.useEffect(() => {
958
+ const el = wrapRef.current;
959
+ if (!el) return;
960
+ const doFit = () => {
961
+ const W = el.clientWidth, H = el.clientHeight;
962
+ if (W <= 0) return;
963
+ if (W !== vw) setVw(W);
964
+ if (!fittedRef.current) {
965
+ const lay = layoutFlow(groups, W);
966
+ const raw = Math.min((H - 46) / lay.worldH, (W - 40) / lay.worldW, 1);
967
+ const s = Math.max(.5, raw);
968
+ setView({
969
+ x: (W - lay.worldW * s) / 2,
970
+ y: (H - lay.worldH * s) / 2,
971
+ s
972
+ });
973
+ fittedRef.current = true;
974
+ }
975
+ };
976
+ doFit();
977
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(doFit) : null;
978
+ if (ro) ro.observe(el);
979
+ return () => {
980
+ if (ro) ro.disconnect();
981
+ };
982
+ }, [active]);
983
+ react.default.useEffect(() => {
984
+ const el = wrapRef.current;
985
+ if (!el) return;
986
+ const onWheel = (e) => {
987
+ e.preventDefault();
988
+ const rect = el.getBoundingClientRect();
989
+ const px = e.clientX - rect.left, py = e.clientY - rect.top;
990
+ setView((v) => {
991
+ const ns = Math.min(1.65, Math.max(.5, v.s * (e.deltaY < 0 ? 1.12 : .89)));
992
+ const k = ns / v.s;
993
+ return {
994
+ s: ns,
995
+ x: px - (px - v.x) * k,
996
+ y: py - (py - v.y) * k
997
+ };
998
+ });
999
+ };
1000
+ el.addEventListener("wheel", onWheel, { passive: false });
1001
+ return () => el.removeEventListener("wheel", onWheel);
1002
+ }, []);
1003
+ react.default.useEffect(() => {
1004
+ setDet(null);
1005
+ }, [active && active.id, runId]);
1006
+ const openDetail = async (s) => {
1007
+ if (!api || !runId) return;
1008
+ setDet({
1009
+ seq: s.seq,
1010
+ stage: s,
1011
+ loading: true,
1012
+ data: null,
1013
+ err: null
1014
+ });
1015
+ try {
1016
+ const d = await api.stageDetail(runId, s.seq, sessionId);
1017
+ setDet({
1018
+ seq: s.seq,
1019
+ stage: s,
1020
+ loading: false,
1021
+ data: unwrap(d, "stageDetail") || null,
1022
+ err: null
1023
+ });
1024
+ } catch (e) {
1025
+ setDet({
1026
+ seq: s.seq,
1027
+ stage: s,
1028
+ loading: false,
1029
+ data: null,
1030
+ err: String(e && e.message || e)
1031
+ });
1032
+ }
1033
+ };
1034
+ const closeDet = () => setDet(null);
1035
+ const layout = layoutFlow(groups, vw);
1036
+ const fitNow = () => {
1037
+ const el = wrapRef.current;
1038
+ if (!el) return;
1039
+ const W = el.clientWidth, H = el.clientHeight;
1040
+ const raw = Math.min((H - 46) / layout.worldH, (W - 40) / layout.worldW, 1);
1041
+ const s = Math.max(.5, raw);
1042
+ setView({
1043
+ x: (W - layout.worldW * s) / 2,
1044
+ y: (H - layout.worldH * s) / 2,
1045
+ s
1046
+ });
1047
+ };
1048
+ const zoomBy = (f) => {
1049
+ setView((v) => {
1050
+ const ns = Math.min(1.65, Math.max(.5, v.s * f));
1051
+ const k = ns / v.s;
1052
+ const el = wrapRef.current;
1053
+ const px = el ? el.clientWidth / 2 : layout.worldW / 2;
1054
+ const py = el ? el.clientHeight / 2 : 64;
1055
+ return {
1056
+ s: ns,
1057
+ x: px - (px - v.x) * k,
1058
+ y: py - (py - v.y) * k
1059
+ };
1060
+ });
1061
+ };
1062
+ const zoomStyle = {
1063
+ font: "inherit",
1064
+ fontSize: 13,
1065
+ width: 26,
1066
+ height: 24,
1067
+ borderRadius: 7,
1068
+ cursor: "pointer",
1069
+ border: `1px solid ${T.border}`,
1070
+ background: T.layer1,
1071
+ color: T.text,
1072
+ display: "flex",
1073
+ alignItems: "center",
1074
+ justifyContent: "center",
1075
+ lineHeight: 1
1076
+ };
1077
+ const onDown = (e) => {
1078
+ if (e.button !== 0) return;
1079
+ dragRef.current = {
1080
+ sx: e.clientX,
1081
+ sy: e.clientY,
1082
+ ox: view.x,
1083
+ oy: view.y
1084
+ };
1085
+ setGrabbing(true);
1086
+ e.preventDefault();
1087
+ };
1088
+ const onMove = (e) => {
1089
+ const d = dragRef.current;
1090
+ if (!d) return;
1091
+ setView((v) => ({
1092
+ ...v,
1093
+ x: d.ox + (e.clientX - d.sx),
1094
+ y: d.oy + (e.clientY - d.sy)
1095
+ }));
1096
+ };
1097
+ const onUp = () => {
1098
+ dragRef.current = null;
1099
+ setGrabbing(false);
1100
+ };
1101
+ return h("div", {
1102
+ ref: wrapRef,
1103
+ onMouseDown: onDown,
1104
+ onMouseMove: onMove,
1105
+ onMouseUp: onUp,
1106
+ onMouseLeave: onUp,
1107
+ style: {
1108
+ position: "relative",
1109
+ height: 560,
1110
+ borderRadius: 12,
1111
+ overflow: "hidden",
1112
+ touchAction: "none",
1113
+ border: `1px solid ${T.border}`,
1114
+ userSelect: "none",
1115
+ cursor: grabbing ? "grabbing" : "grab",
1116
+ background: `radial-gradient(circle, ${T.border2} 1px, transparent 1px) 0 0 / 24px 24px, ${T.layer1}`,
1117
+ backgroundBlendMode: "overlay"
1118
+ }
1119
+ }, h("div", { style: {
1120
+ position: "absolute",
1121
+ left: 0,
1122
+ top: 0,
1123
+ width: layout.worldW,
1124
+ height: layout.worldH,
1125
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.s})`,
1126
+ transformOrigin: "0 0"
1127
+ } }, h("svg", {
1128
+ width: layout.worldW,
1129
+ height: layout.worldH,
1130
+ style: {
1131
+ position: "absolute",
1132
+ left: 0,
1133
+ top: 0,
1134
+ overflow: "visible",
1135
+ zIndex: 0
1136
+ }
1137
+ }, h("defs", {}, layout.conns.map((c) => h("marker", {
1138
+ key: "m" + c.x1 + "-" + c.y1,
1139
+ id: "tfm-" + c.x1 + "-" + c.y1,
1140
+ viewBox: "0 0 10 10",
1141
+ refX: 8,
1142
+ refY: 5,
1143
+ markerWidth: 7,
1144
+ markerHeight: 7,
1145
+ orient: "auto-start-reverse"
1146
+ }, h("path", {
1147
+ d: "M 0 0 L 10 5 L 0 10 z",
1148
+ fill: c.color
1149
+ })))), layout.conns.map((c) => h("g", { key: "c" + c.x1 + "-" + c.y1 }, h("path", {
1150
+ d: connPath(c),
1151
+ fill: "none",
1152
+ stroke: `color-mix(in srgb, ${c.color} 12%, transparent)`,
1153
+ strokeWidth: 7,
1154
+ strokeLinecap: "round"
1155
+ }), h("path", {
1156
+ d: connPath(c),
1157
+ fill: "none",
1158
+ stroke: `color-mix(in srgb, ${c.color} 45%, transparent)`,
1159
+ strokeWidth: 2.5,
1160
+ markerEnd: `url(#tfm-${c.x1}-${c.y1})`
1161
+ }), h("path", {
1162
+ d: connPath(c),
1163
+ fill: "none",
1164
+ stroke: c.color,
1165
+ strokeWidth: 2,
1166
+ strokeLinecap: "round",
1167
+ strokeDasharray: "5 9",
1168
+ animation: "tf-flow .75s linear infinite"
1169
+ })))), h("div", { style: {
1170
+ position: "absolute",
1171
+ left: 0,
1172
+ top: 0,
1173
+ zIndex: 1
1174
+ } }, layout.nodes.map((n) => FlowNode(n, openDetail)))), layout.nodes.length === 0 ? h("div", { style: {
1175
+ position: "absolute",
1176
+ inset: 0,
1177
+ display: "flex",
1178
+ alignItems: "center",
1179
+ justifyContent: "center",
1180
+ color: T.text2,
1181
+ fontSize: 13
1182
+ } }, "流水线还没有开始执行节点") : null, h("div", {
1183
+ onMouseDown: (e) => e.stopPropagation(),
1184
+ style: {
1185
+ position: "absolute",
1186
+ top: 10,
1187
+ right: 12,
1188
+ zIndex: 6,
1189
+ display: "flex",
1190
+ alignItems: "center",
1191
+ gap: 6,
1192
+ padding: 5,
1193
+ borderRadius: 11,
1194
+ border: `1px solid ${T.border}`,
1195
+ background: `color-mix(in srgb, ${T.layer1} 82%, transparent)`,
1196
+ backdropFilter: "blur(8px)",
1197
+ boxShadow: "0 6px 20px rgba(0,0,0,.16)"
1198
+ }
1199
+ }, h("button", {
1200
+ title: "缩小",
1201
+ onClick: () => zoomBy(.86),
1202
+ style: zoomStyle
1203
+ }, "−"), h("span", { style: {
1204
+ fontFamily: MONO,
1205
+ fontSize: 10.5,
1206
+ color: T.text2,
1207
+ minWidth: 34,
1208
+ textAlign: "center"
1209
+ } }, `${Math.round(view.s * 100)}%`), h("button", {
1210
+ title: "放大",
1211
+ onClick: () => zoomBy(1.16),
1212
+ style: zoomStyle
1213
+ }, "+"), h("span", { style: {
1214
+ width: 1,
1215
+ height: 14,
1216
+ background: T.border
1217
+ } }), h("button", {
1218
+ title: "适应画布",
1219
+ onClick: fitNow,
1220
+ style: {
1221
+ ...zoomStyle,
1222
+ fontSize: 13
1223
+ }
1224
+ }, "⤢"), h("span", { style: {
1225
+ width: 1,
1226
+ height: 14,
1227
+ background: T.border
1228
+ } }), h("span", { style: {
1229
+ fontSize: 10.5,
1230
+ color: T.text2,
1231
+ paddingRight: 4,
1232
+ opacity: .85
1233
+ } }, "✥ 拖动画布 · 滚轮缩放")), det ? h(StageDetailDrawer, {
1234
+ det,
1235
+ onClose: closeDet,
1236
+ sessionId,
1237
+ sessions
1238
+ }) : null);
1239
+ }
1240
+ function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun }) {
1241
+ const [drag, setDrag] = react.default.useState(null);
1242
+ const [over, setOver] = react.default.useState(null);
1243
+ const [det, setDet] = react.default.useState(null);
1244
+ const openItem = async (kind, id) => {
1245
+ setDet({
1246
+ kind,
1247
+ id,
1248
+ loading: true,
1249
+ data: null,
1250
+ err: null
1251
+ });
1252
+ try {
1253
+ const d = unwrap(await api.itemDetail(kind, id, sessionId), "itemDetail");
1254
+ setDet({
1255
+ kind,
1256
+ id,
1257
+ loading: false,
1258
+ data: d,
1259
+ err: null
1260
+ });
1261
+ } catch (e) {
1262
+ setDet({
1263
+ kind,
1264
+ id,
1265
+ loading: false,
1266
+ data: null,
1267
+ err: String(e && e.message || e)
1268
+ });
1269
+ }
1270
+ };
1271
+ if (!backlog) return h("div", { style: {
1272
+ color: T.text2,
1273
+ fontSize: 13,
1274
+ padding: "28px 20px",
1275
+ textAlign: "center"
1276
+ } }, h("div", { style: {
1277
+ fontSize: 28,
1278
+ marginBottom: 8
1279
+ } }, "📋"), "backlog 为空(还没有流水线运行过)");
1280
+ const subtaskMap = {};
1281
+ for (const t of backlog.tasks || []) if (t.type === "subtask" && t.id) subtaskMap[t.id] = t;
1282
+ const move = async (kind, id, to) => {
1283
+ try {
1284
+ await api.backlogUpdate(kind, id, to, sessionId, "看板拖拽流转");
1285
+ } catch (e) {}
1286
+ onRefresh();
1287
+ };
1288
+ const card = (item, kind) => h("div", {
1289
+ key: item.id,
1290
+ draggable: true,
1291
+ onClick: () => openItem(kind, item.id),
1292
+ onDragStart: () => setDrag({
1293
+ kind,
1294
+ id: item.id,
1295
+ from: item.status
1296
+ }),
1297
+ onDragEnd: () => setDrag(null),
1298
+ style: {
1299
+ background: item.humanIntervention || item.status === "needs-human" ? `color-mix(in srgb, ${T.error} 7%, ${T.layer1})` : T.layer1,
1300
+ border: `1px solid ${T.border}`,
1301
+ borderLeft: `3px solid ${stColor(item.status)}`,
1302
+ borderRadius: 8,
1303
+ padding: "7px 9px",
1304
+ cursor: "grab",
1305
+ fontSize: 12,
1306
+ opacity: drag && drag.id === item.id ? .35 : 1,
1307
+ transition: "opacity .1s ease, transform .12s ease",
1308
+ boxShadow: "0 1px 2px rgba(0,0,0,.05)"
1309
+ },
1310
+ title: `${item.id} · ${item.status}${item.summary ? "\n" + item.summary : ""}(点击查看详情)`
1311
+ }, h("div", { style: {
1312
+ display: "flex",
1313
+ alignItems: "center",
1314
+ gap: 4
1315
+ } }, h("span", { style: {
1316
+ fontFamily: MONO,
1317
+ color: T.text2,
1318
+ fontSize: 10.5
1319
+ } }, 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: {
1320
+ marginLeft: "auto",
1321
+ fontSize: 10.5,
1322
+ color: T.text2
1323
+ } }, `👤 ${item.owner}`) : null), h("div", {
1324
+ title: item.title || void 0,
1325
+ style: {
1326
+ fontWeight: 500,
1327
+ margin: "3px 0 4px",
1328
+ lineHeight: 1.4
1329
+ }
1330
+ }, (item.title || "").slice(0, 30)), h("div", { style: {
1331
+ ...flexRow,
1332
+ marginTop: 2
1333
+ } }, chip(stText(item.status), stColor(item.status)), typeof item.retries === "number" && item.retries > 0 ? h("span", { style: {
1334
+ fontSize: 10.5,
1335
+ color: T.warn,
1336
+ fontFamily: MONO
1337
+ } }, `↻${item.retries}`) : null, item.humanIntervention || item.status === "needs-human" ? h("span", { style: {
1338
+ fontSize: 10.5,
1339
+ color: T.error,
1340
+ fontWeight: 700
1341
+ } }, "⚠") : null), kind === "task" && (item.devAssign || item.qaAssign || item.acceptBy) ? h("div", { style: {
1342
+ ...flexRow,
1343
+ marginTop: 3,
1344
+ fontSize: 10.5,
1345
+ color: T.text2,
1346
+ fontFamily: MONO
1347
+ } }, item.devAssign ? h("span", {
1348
+ title: `dev 分配\n${item.devAssign}`,
1349
+ style: {
1350
+ display: "inline-flex",
1351
+ alignItems: "center",
1352
+ gap: 3,
1353
+ overflow: "hidden",
1354
+ textOverflow: "ellipsis",
1355
+ maxWidth: 130,
1356
+ whiteSpace: "nowrap"
1357
+ }
1358
+ }, `👨‍💻${item.devAssign}`) : null, item.qaAssign ? h("span", {
1359
+ title: `qa 分配\n${item.qaAssign}`,
1360
+ style: {
1361
+ display: "inline-flex",
1362
+ alignItems: "center",
1363
+ gap: 3,
1364
+ overflow: "hidden",
1365
+ textOverflow: "ellipsis",
1366
+ maxWidth: 130,
1367
+ whiteSpace: "nowrap"
1368
+ }
1369
+ }, `🧪${item.qaAssign}`) : null, item.acceptBy ? h("span", {
1370
+ title: `验收/汇报人\n${item.acceptBy}`,
1371
+ style: {
1372
+ display: "inline-flex",
1373
+ alignItems: "center",
1374
+ gap: 3,
1375
+ overflow: "hidden",
1376
+ textOverflow: "ellipsis",
1377
+ maxWidth: 130,
1378
+ whiteSpace: "nowrap"
1379
+ }
1380
+ }, `✅${item.acceptBy}`) : null) : null, kind === "task" && byRoleLine(item) ? h("div", { style: {
1381
+ ...flexRow,
1382
+ marginTop: 3,
1383
+ fontSize: 10.5,
1384
+ color: T.warn,
1385
+ fontFamily: MONO
1386
+ } }, h("span", { style: { color: T.text2 } }, "⛽"), byRoleLine(item)) : null, kind === "task" && (item.subtaskIds || []).length > 0 ? (() => {
1387
+ const subs = item.subtaskIds.map((id) => subtaskMap[id]).filter(Boolean);
1388
+ if (subs.length === 0) return null;
1389
+ const done = subs.filter((s) => s.status === "done").length;
1390
+ const failed = subs.filter((s) => s.status === "failed").length;
1391
+ const running = subs.filter((s) => s.status === "running").length;
1392
+ return h("div", { style: { marginTop: 5 } }, h("div", { style: {
1393
+ ...flexRow,
1394
+ fontSize: 10.5,
1395
+ color: T.text2,
1396
+ fontFamily: MONO,
1397
+ marginBottom: 3
1398
+ } }, h("span", null, `📦 ${subs.length} 子卡`), done > 0 ? h("span", { style: { color: T.success } }, `${done}✓`) : null, running > 0 ? h("span", { style: { color: T.brand } }, `${running}⟳`) : null, failed > 0 ? h("span", { style: { color: T.error } }, `${failed}✗`) : null), subs.map((sub) => h("div", {
1399
+ key: sub.id,
1400
+ style: {
1401
+ display: "flex",
1402
+ alignItems: "center",
1403
+ gap: 4,
1404
+ fontSize: 10.5,
1405
+ fontFamily: MONO,
1406
+ padding: "2px 6px",
1407
+ borderRadius: 4,
1408
+ marginBottom: 2,
1409
+ background: sub.status === "failed" ? `color-mix(in srgb, ${T.error} 7%, transparent)` : T.layer2,
1410
+ border: `1px solid ${stColor(sub.status)}30`
1411
+ }
1412
+ }, h("span", { style: {
1413
+ color: stColor(sub.status),
1414
+ fontWeight: 600,
1415
+ minWidth: 12
1416
+ } }, sub.status === "done" ? "✓" : sub.status === "failed" ? "✗" : sub.status === "running" ? "⟳" : "…"), h("span", {
1417
+ title: sub.title || void 0,
1418
+ style: {
1419
+ overflow: "hidden",
1420
+ textOverflow: "ellipsis",
1421
+ whiteSpace: "nowrap",
1422
+ maxWidth: 130
1423
+ }
1424
+ }, (sub.title || "").replace(/^开发 · /, "")), sub.devAssign ? h("span", {
1425
+ title: `dev 分配\n${sub.devAssign}`,
1426
+ style: {
1427
+ marginLeft: "auto",
1428
+ color: T.text2,
1429
+ overflow: "hidden",
1430
+ textOverflow: "ellipsis",
1431
+ maxWidth: 70,
1432
+ whiteSpace: "nowrap"
1433
+ }
1434
+ }, sub.devAssign) : null)));
1435
+ })() : null);
1436
+ const groups = [
1437
+ {
1438
+ kind: "req",
1439
+ list: backlog.requirements || []
1440
+ },
1441
+ {
1442
+ kind: "task",
1443
+ list: (backlog.tasks || []).filter((t) => t.type !== "subtask")
1444
+ },
1445
+ {
1446
+ kind: "bug",
1447
+ list: backlog.bugs || []
1448
+ }
1449
+ ];
1450
+ return h("div", { style: {
1451
+ display: "flex",
1452
+ flexDirection: "column",
1453
+ gap: 14,
1454
+ maxHeight: "72vh",
1455
+ overflowY: "auto",
1456
+ paddingRight: 4
1457
+ } }, groups.map(({ kind, list }) => {
1458
+ const counts = {};
1459
+ for (const s of COLUMNS[kind]) counts[s] = 0;
1460
+ for (const item of list) counts[item.status] = (counts[item.status] || 0) + 1;
1461
+ return h("div", { key: kind }, h("div", { style: {
1462
+ display: "flex",
1463
+ alignItems: "center",
1464
+ gap: 8,
1465
+ fontWeight: 700,
1466
+ fontSize: 13,
1467
+ marginBottom: 8
1468
+ } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), KIND_TITLE[kind], h("span", { style: {
1469
+ fontSize: 11,
1470
+ fontWeight: 600,
1471
+ color: T.text2,
1472
+ background: T.layer2,
1473
+ borderRadius: 999,
1474
+ padding: "0 8px",
1475
+ lineHeight: "18px"
1476
+ } }, String(list.length))), h("div", { style: {
1477
+ display: "flex",
1478
+ gap: 8,
1479
+ overflowX: "auto",
1480
+ paddingBottom: 6
1481
+ } }, COLUMNS[kind].map((s) => {
1482
+ const isOver = over === `${kind}:${s}`;
1483
+ const color = stColor(s);
1484
+ return h("div", {
1485
+ key: s,
1486
+ onDragOver: (e) => {
1487
+ e.preventDefault();
1488
+ setOver(`${kind}:${s}`);
1489
+ },
1490
+ onDragLeave: () => setOver((o) => o === `${kind}:${s}` ? null : o),
1491
+ onDrop: async () => {
1492
+ if (drag && drag.kind === kind && drag.from !== s) await move(kind, drag.id, s);
1493
+ setDrag(null);
1494
+ setOver(null);
1495
+ },
1496
+ style: {
1497
+ minWidth: 140,
1498
+ maxWidth: 172,
1499
+ flex: "0 0 auto",
1500
+ borderRadius: 10,
1501
+ padding: 7,
1502
+ minHeight: 84,
1503
+ maxHeight: 340,
1504
+ overflowY: "auto",
1505
+ background: isOver ? `color-mix(in srgb, ${color} 8%, ${T.layer1})` : T.layer2,
1506
+ border: `1px dashed ${isOver ? color : T.border}`,
1507
+ transition: "background .12s ease, border-color .12s ease"
1508
+ }
1509
+ }, h("div", { style: {
1510
+ ...flexRow,
1511
+ fontSize: 11,
1512
+ fontWeight: 600,
1513
+ color,
1514
+ marginBottom: 6,
1515
+ padding: "0 2px"
1516
+ } }, h("span", null, stText(s)), h("span", { style: {
1517
+ marginLeft: "auto",
1518
+ fontFamily: MONO,
1519
+ fontSize: 10,
1520
+ opacity: .75
1521
+ } }, counts[s] || 0)), h("div", { style: {
1522
+ display: "flex",
1523
+ flexDirection: "column",
1524
+ gap: 6
1525
+ } }, list.filter((item) => item.status === s).map((item) => card(item, kind))));
1526
+ })));
1527
+ }), det ? h(ItemDetailDrawer, {
1528
+ det,
1529
+ onClose: () => setDet(null),
1530
+ onShowRun
1531
+ }) : null);
1532
+ }
1533
+ function fmtAt(ts) {
1534
+ return ts ? new Date(ts).toLocaleTimeString("zh-CN", {
1535
+ hour: "2-digit",
1536
+ minute: "2-digit"
1537
+ }) : "—";
1538
+ }
1539
+ function ItemDetailDrawer({ det, onClose, onShowRun }) {
1540
+ const d = det && det.data;
1541
+ const loading = det && det.loading;
1542
+ const err = det && det.err;
1543
+ const kind = det && det.kind;
1544
+ const icon = kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞";
1545
+ const color = d ? stColor(d.status) : T.text2;
1546
+ const closeBtn = {
1547
+ font: "inherit",
1548
+ width: 26,
1549
+ height: 26,
1550
+ borderRadius: 8,
1551
+ cursor: "pointer",
1552
+ border: `1px solid ${T.border}`,
1553
+ background: "transparent",
1554
+ color: T.text2,
1555
+ display: "flex",
1556
+ alignItems: "center",
1557
+ justifyContent: "center",
1558
+ fontSize: 13,
1559
+ lineHeight: 1
1560
+ };
1561
+ const secTitle = (label) => h("span", { style: {
1562
+ fontSize: 10.5,
1563
+ fontWeight: 700,
1564
+ color: T.text2,
1565
+ letterSpacing: .3
1566
+ } }, label);
1567
+ const kv = (k, v, mono) => h("div", { style: {
1568
+ display: "flex",
1569
+ gap: 8,
1570
+ fontSize: 11.5,
1571
+ lineHeight: 1.6
1572
+ } }, h("span", { style: {
1573
+ flex: "0 0 72px",
1574
+ color: T.text2
1575
+ } }, k), h("span", {
1576
+ title: v === null || v === void 0 ? void 0 : String(v),
1577
+ style: {
1578
+ flex: 1,
1579
+ minWidth: 0,
1580
+ fontFamily: mono ? MONO : void 0,
1581
+ color: T.text,
1582
+ wordBreak: "break-all",
1583
+ textOverflow: "ellipsis"
1584
+ }
1585
+ }, v === null || v === void 0 ? "—" : String(v)));
1586
+ const linkRow = (it, extra) => h("div", {
1587
+ key: it.id,
1588
+ style: {
1589
+ display: "flex",
1590
+ flexDirection: "column",
1591
+ gap: 2,
1592
+ padding: "5px 8px",
1593
+ borderRadius: 7,
1594
+ background: T.layer2,
1595
+ fontSize: 11.5
1596
+ }
1597
+ }, h("div", { style: {
1598
+ display: "flex",
1599
+ gap: 7,
1600
+ alignItems: "center"
1601
+ } }, h("span", { style: {
1602
+ fontFamily: MONO,
1603
+ fontSize: 10.5,
1604
+ color: T.text2,
1605
+ flex: "0 0 auto"
1606
+ } }, it.id), it.severity ? chip(it.severity, it.severity === "P0" ? T.error : it.severity === "P1" ? T.warn : T.text2) : null, chip(stText(it.status), stColor(it.status)), extra && extra.usage ? h("span", { style: {
1607
+ fontSize: 10,
1608
+ fontFamily: MONO,
1609
+ color: T.warn,
1610
+ flex: "0 0 auto"
1611
+ } }, `⛽${fmtTokens((extra.usage.input || 0) + (extra.usage.cacheRead || 0) + (extra.usage.cacheWrite || 0) + (extra.usage.output || 0))}`) : null, extra && extra.assignee ? h("span", {
1612
+ title: `dev 分配\n${extra.assignee}`,
1613
+ style: {
1614
+ display: "inline-flex",
1615
+ alignItems: "center",
1616
+ gap: 2,
1617
+ fontSize: 10,
1618
+ fontFamily: MONO,
1619
+ color: T.text2,
1620
+ flex: "0 0 auto",
1621
+ overflow: "hidden",
1622
+ textOverflow: "ellipsis",
1623
+ maxWidth: 90,
1624
+ whiteSpace: "nowrap"
1625
+ }
1626
+ }, `👨‍💻${String(extra.assignee).slice(0, 14)}`) : null, h("span", {
1627
+ title: it.title || void 0,
1628
+ style: {
1629
+ flex: 1,
1630
+ minWidth: 0,
1631
+ overflow: "hidden",
1632
+ textOverflow: "ellipsis",
1633
+ whiteSpace: "nowrap",
1634
+ color: T.text
1635
+ }
1636
+ }, (it.title || "").slice(0, 44))), it.summary ? h("div", {
1637
+ title: it.summary,
1638
+ style: {
1639
+ fontSize: 10.5,
1640
+ color: T.text2,
1641
+ overflow: "hidden",
1642
+ textOverflow: "ellipsis",
1643
+ whiteSpace: "nowrap",
1644
+ maxWidth: "100%"
1645
+ }
1646
+ }, it.summary) : null);
1647
+ return h("div", { style: {
1648
+ position: "absolute",
1649
+ top: 10,
1650
+ right: 12,
1651
+ bottom: 10,
1652
+ width: 400,
1653
+ zIndex: 9,
1654
+ borderRadius: 14,
1655
+ overflow: "hidden",
1656
+ display: "flex",
1657
+ flexDirection: "column",
1658
+ border: `1px solid ${T.border}`,
1659
+ background: `color-mix(in srgb, ${T.layer1} 92%, transparent)`,
1660
+ backdropFilter: "blur(14px)",
1661
+ boxShadow: "0 14px 48px rgba(0,0,0,.30)"
1662
+ } }, h("div", { style: {
1663
+ display: "flex",
1664
+ alignItems: "center",
1665
+ gap: 9,
1666
+ padding: "12px 14px",
1667
+ borderBottom: `1px solid ${T.border}`,
1668
+ background: `linear-gradient(135deg, color-mix(in srgb, ${color} 14%, transparent), transparent 62%)`
1669
+ } }, h("span", { style: { fontSize: 17 } }, icon), h("div", { style: {
1670
+ flex: 1,
1671
+ minWidth: 0
1672
+ } }, h("div", {
1673
+ style: {
1674
+ fontSize: 12.5,
1675
+ fontWeight: 700,
1676
+ color: T.text,
1677
+ overflow: "hidden",
1678
+ textOverflow: "ellipsis",
1679
+ whiteSpace: "nowrap"
1680
+ },
1681
+ title: d ? d.title || d.id : void 0
1682
+ }, d ? d.title || d.id : `${det.kind} · ${det.id}`), d ? h("div", { style: {
1683
+ fontSize: 10.5,
1684
+ color: T.text2,
1685
+ marginTop: 1,
1686
+ fontFamily: MONO,
1687
+ fontVariantNumeric: "tabular-nums"
1688
+ } }, `${d.id} · ${d.kind}${d.severity ? " · " + d.severity : ""}`) : null), d ? chip(stText(d.status), color, { dot: true }) : null, h("button", {
1689
+ onClick: onClose,
1690
+ style: closeBtn,
1691
+ title: "关闭"
1692
+ }, "✕")), h("div", { style: {
1693
+ flex: 1,
1694
+ overflowY: "auto",
1695
+ padding: "12px 14px",
1696
+ display: "flex",
1697
+ flexDirection: "column",
1698
+ gap: 13
1699
+ } }, loading ? h("div", { style: {
1700
+ color: T.text2,
1701
+ fontSize: 12,
1702
+ padding: 12
1703
+ } }, "加载中…") : err ? h("div", { style: {
1704
+ color: T.error,
1705
+ fontSize: 12,
1706
+ padding: 12
1707
+ } }, "⚠ " + err) : !d ? null : [
1708
+ h("div", { style: {
1709
+ display: "flex",
1710
+ flexDirection: "column",
1711
+ gap: 4
1712
+ } }, secTitle("概览"), d.spec ? h(FoldableText, {
1713
+ text: d.spec,
1714
+ charLimit: 300,
1715
+ lineLimit: 4,
1716
+ style: {
1717
+ background: T.layer2,
1718
+ borderRadius: 8,
1719
+ padding: "8px 10px"
1720
+ }
1721
+ }) : null, h("div", { style: {
1722
+ display: "flex",
1723
+ flexDirection: "column",
1724
+ gap: 4
1725
+ } }, d.summary ? h(FoldableText, { text: d.summary }) : null), h("div", { style: {
1726
+ display: "flex",
1727
+ flexDirection: "column",
1728
+ gap: 2,
1729
+ marginTop: 2
1730
+ } }, d.devAssign ? kv("👨‍💻 dev", d.devAssign.slice(0, 26), true) : null, d.qaAssign ? kv("🧪 qa", d.qaAssign.slice(0, 26), true) : null, d.assignBy ? kv("✅ 验收", d.assignBy.slice(0, 26), true) : null, d.owner ? kv("👤 owner", d.owner.slice(0, 26), true) : null, typeof d.retries === "number" && d.retries > 0 ? kv("↻ 重试", String(d.retries)) : null, d.humanIntervention ? kv("⚠ 人工介入", "需人工介入") : null, kv("更新于", fmtAt(d.updatedAt) || "—"))),
1731
+ d.runInfo ? (() => {
1732
+ const ri = d.runInfo;
1733
+ return h("div", { style: {
1734
+ display: "flex",
1735
+ flexDirection: "column",
1736
+ gap: 6
1737
+ } }, h("div", { style: {
1738
+ display: "flex",
1739
+ flexDirection: "column",
1740
+ gap: 3
1741
+ } }, h("div", { style: {
1742
+ display: "flex",
1743
+ alignItems: "center",
1744
+ gap: 8
1745
+ } }, secTitle("运行"), h("span", { style: {
1746
+ fontFamily: MONO,
1747
+ fontSize: 10.5,
1748
+ color: T.text2,
1749
+ whiteSpace: "nowrap"
1750
+ } }, ri.runId), chip(stText(ri.status), stColor(ri.status)), onShowRun ? h("button", {
1751
+ onClick: () => onShowRun(ri.runId),
1752
+ title: `跳转到该需求的流水线视图 #${String(ri.runId).slice(-6)}`,
1753
+ style: {
1754
+ marginLeft: "auto",
1755
+ flex: "0 0 auto",
1756
+ whiteSpace: "nowrap",
1757
+ fontSize: 10.5,
1758
+ fontWeight: 600,
1759
+ padding: "2px 9px",
1760
+ borderRadius: 6,
1761
+ cursor: "pointer",
1762
+ border: `1px solid color-mix(in srgb, ${T.brand} 38%, transparent)`,
1763
+ background: `color-mix(in srgb, ${T.brand} 10%, transparent)`,
1764
+ color: T.brand,
1765
+ lineHeight: "16px"
1766
+ }
1767
+ }, "▶ 流水线") : null), ri.startedAt || ri.endedAt ? h("span", { style: {
1768
+ fontSize: 10.5,
1769
+ color: T.text2,
1770
+ fontFamily: MONO
1771
+ } }, `${fmtAt(ri.startedAt)} → ${fmtAt(ri.endedAt)} · ${fmtDur(ri.startedAt, ri.endedAt)}`) : null), h("div", { style: {
1772
+ display: "flex",
1773
+ flexDirection: "column",
1774
+ gap: 4
1775
+ } }, secTitle("需求原文"), h(FoldableText, {
1776
+ text: ri.requirement || "(无原文)",
1777
+ charLimit: 300,
1778
+ lineLimit: 4,
1779
+ style: {
1780
+ background: T.layer2,
1781
+ borderRadius: 8,
1782
+ padding: "8px 10px"
1783
+ }
1784
+ })));
1785
+ })() : null,
1786
+ d.runDocs ? h("div", { style: {
1787
+ display: "flex",
1788
+ flexDirection: "column",
1789
+ gap: 4
1790
+ } }, secTitle("任务夹"), h("div", { style: {
1791
+ fontSize: 11.5,
1792
+ fontFamily: MONO,
1793
+ color: T.brand,
1794
+ background: T.layer2,
1795
+ borderRadius: 8,
1796
+ padding: "7px 10px",
1797
+ wordBreak: "break-all"
1798
+ } }, d.runDocs + "/")) : null,
1799
+ kind === "task" && d.usage ? h("div", { style: {
1800
+ display: "flex",
1801
+ flexDirection: "column",
1802
+ gap: 4
1803
+ } }, secTitle("TOKEN · 官方口径"), h("span", { style: {
1804
+ fontSize: 11.5,
1805
+ fontFamily: MONO,
1806
+ color: T.text,
1807
+ lineHeight: 1.65
1808
+ } }, usageDetail(d)), d.byRole && Object.keys(d.byRole).length > 0 ? h("div", { style: {
1809
+ display: "flex",
1810
+ flexDirection: "column",
1811
+ gap: 2,
1812
+ marginTop: 2
1813
+ } }, Object.entries(d.byRole).sort((a, b) => totalTokens(b[1]) - totalTokens(a[1])).map(([role, uRaw]) => {
1814
+ const u = uRaw;
1815
+ const label = role === "dev" ? "👨‍💻 开发" : role === "qa" ? "🧪 QA" : role === "acceptance" ? "✅ 验收" : role === "pm" ? "📌 产品" : role === "design" ? "🎨 设计" : role === "arch" ? "🏗 架构" : "⚙️ " + role;
1816
+ return h("div", {
1817
+ key: role,
1818
+ style: {
1819
+ display: "flex",
1820
+ gap: 7,
1821
+ alignItems: "center",
1822
+ fontSize: 10.5,
1823
+ fontFamily: MONO,
1824
+ color: T.text2
1825
+ }
1826
+ }, h("span", { style: { flex: "0 0 64px" } }, label), h("span", { style: { color: T.text } }, `⛽${fmtTokens(totalTokens(u))}`), h("span", null, `${fmtTokens(u.input || 0)}i / ${fmtTokens(u.cacheRead || 0)}c / ${fmtTokens(u.output || 0)}o · ${u.calls || 0} 次`));
1827
+ })) : null) : null,
1828
+ d.subtasks && d.subtasks.length > 0 ? h("div", { style: {
1829
+ display: "flex",
1830
+ flexDirection: "column",
1831
+ gap: 4
1832
+ } }, secTitle(`关联子卡(${d.subtasks.length})`), h("div", { style: {
1833
+ display: "flex",
1834
+ flexDirection: "column",
1835
+ gap: 5
1836
+ } }, d.subtasks.map((s) => linkRow(s, {
1837
+ usage: s.usage,
1838
+ assignee: s.devAssign
1839
+ })))) : null,
1840
+ d.bugs && d.bugs.length > 0 ? h("div", { style: {
1841
+ display: "flex",
1842
+ flexDirection: "column",
1843
+ gap: 4
1844
+ } }, secTitle(`关联缺陷(${d.bugs.length})`), h("div", { style: {
1845
+ display: "flex",
1846
+ flexDirection: "column",
1847
+ gap: 5
1848
+ } }, d.bugs.map((b) => linkRow(b, {})))) : null,
1849
+ d.events && d.events.length > 0 ? h("div", { style: {
1850
+ display: "flex",
1851
+ flexDirection: "column",
1852
+ gap: 4
1853
+ } }, secTitle(`流转时间线(${d.events.length})`), h("div", { style: {
1854
+ display: "flex",
1855
+ flexDirection: "column",
1856
+ gap: 0
1857
+ } }, d.events.slice().reverse().map((ev) => h("div", {
1858
+ key: `${ev.at}-${ev.to}`,
1859
+ style: {
1860
+ display: "flex",
1861
+ gap: 8,
1862
+ alignItems: "baseline",
1863
+ padding: "5px 0",
1864
+ borderBottom: `1px dashed ${T.border}`,
1865
+ fontSize: 11.5
1866
+ }
1867
+ }, h("span", { style: {
1868
+ fontFamily: MONO,
1869
+ color: T.text2,
1870
+ fontSize: 10.5,
1871
+ flex: "0 0 42px"
1872
+ } }, fmtAt(ev.at)), h("span", { style: { color: T.text } }, ev.from || "—"), h("span", { style: { color: T.text2 } }, "→"), h("span", { style: {
1873
+ fontFamily: MONO,
1874
+ color: T.brand
1875
+ } }, ev.to), h("span", {
1876
+ style: {
1877
+ flex: 1,
1878
+ minWidth: 0,
1879
+ color: T.text2,
1880
+ fontSize: 10.5,
1881
+ overflow: "hidden",
1882
+ textOverflow: "ellipsis",
1883
+ whiteSpace: "nowrap"
1884
+ },
1885
+ title: ev.reason || void 0
1886
+ }, ev.reason || ""))))) : null
1887
+ ]));
1888
+ }
1889
+ function TeamSelector({ sessionId, remote }) {
1890
+ const [teams, setTeams] = react.default.useState([]);
1891
+ const [active, setActive] = react.default.useState(null);
1892
+ const [open, setOpen] = react.default.useState(false);
1893
+ const ref = react.default.useRef(null);
1894
+ const load = react.default.useCallback(async () => {
1895
+ if (!remote || !sessionId) return;
1896
+ try {
1897
+ const tw = unwrap(await remote.listTeams(sessionId), "listTeams");
1898
+ setTeams(tw && tw.teams || []);
1899
+ const at = unwrap(await remote.getActiveTeam(sessionId), "getActiveTeam");
1900
+ setActive(at && at.team ? at.team : null);
1901
+ } catch (e) {}
1902
+ }, [remote, sessionId]);
1903
+ react.default.useEffect(() => {
1904
+ load();
1905
+ }, [load]);
1906
+ react.default.useEffect(() => {
1907
+ if (!open) return;
1908
+ const handler = (e) => {
1909
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
1910
+ };
1911
+ document.addEventListener("mousedown", handler);
1912
+ return () => document.removeEventListener("mousedown", handler);
1913
+ }, [open]);
1914
+ const select = async (teamId) => {
1915
+ if (!remote || !sessionId) return;
1916
+ try {
1917
+ if (!teamId) {
1918
+ await remote.clearTeam(sessionId);
1919
+ setActive(null);
1920
+ } else {
1921
+ const r = unwrap(await remote.selectTeam(sessionId, teamId), "selectTeam");
1922
+ setActive(r && r.team ? r.team : null);
1923
+ }
1924
+ } catch (e) {}
1925
+ setOpen(false);
1926
+ };
1927
+ if (teams.length === 0) return null;
1928
+ return h("div", {
1929
+ ref,
1930
+ style: { position: "relative" }
1931
+ }, h("button", {
1932
+ onClick: () => setOpen(!open),
1933
+ title: active ? `当前团队:${active.name}(点击切换)` : "选择团队",
1934
+ style: {
1935
+ display: "inline-flex",
1936
+ alignItems: "center",
1937
+ gap: 4,
1938
+ padding: "3px 8px",
1939
+ borderRadius: 6,
1940
+ cursor: "pointer",
1941
+ fontSize: 11,
1942
+ fontFamily: MONO,
1943
+ fontWeight: 500,
1944
+ border: `1px solid ${active ? T.brand : T.border}`,
1945
+ background: active ? `color-mix(in srgb, ${T.brand} 10%, transparent)` : "transparent",
1946
+ color: active ? T.brand : T.text2,
1947
+ transition: "all .12s ease"
1948
+ }
1949
+ }, h("span", { style: { fontSize: 12 } }, active ? active.icon : "🏭"), h("span", {
1950
+ title: active && active.name ? String(active.name) : "团队",
1951
+ style: {
1952
+ maxWidth: 80,
1953
+ overflow: "hidden",
1954
+ textOverflow: "ellipsis",
1955
+ whiteSpace: "nowrap"
1956
+ }
1957
+ }, active ? active.name : "团队"), h("span", { style: {
1958
+ fontSize: 8,
1959
+ opacity: .6
1960
+ } }, open ? "▲" : "▼")), open ? h("div", { style: {
1961
+ position: "absolute",
1962
+ bottom: "100%",
1963
+ right: 0,
1964
+ marginBottom: 4,
1965
+ minWidth: 228,
1966
+ background: T.layer1,
1967
+ border: `1px solid ${T.border}`,
1968
+ borderRadius: 8,
1969
+ boxShadow: "0 4px 12px rgba(0,0,0,.15)",
1970
+ zIndex: 100,
1971
+ overflow: "hidden"
1972
+ } }, h("div", { style: {
1973
+ padding: "7px 12px",
1974
+ fontSize: 10,
1975
+ color: T.text2,
1976
+ borderBottom: `1px solid ${T.border}`
1977
+ } }, "选择团队"), h("button", {
1978
+ onClick: () => select(null),
1979
+ style: {
1980
+ display: "flex",
1981
+ alignItems: "flex-start",
1982
+ gap: 10,
1983
+ width: "100%",
1984
+ padding: "9px 12px",
1985
+ border: "none",
1986
+ cursor: "pointer",
1987
+ textAlign: "left",
1988
+ background: !active ? `color-mix(in srgb, ${T.text2} 10%, transparent)` : "transparent",
1989
+ color: T.text,
1990
+ fontSize: 12,
1991
+ transition: "background .1s",
1992
+ borderBottom: `1px solid ${T.border}`
1993
+ }
1994
+ }, h("span", { style: {
1995
+ fontSize: 14,
1996
+ opacity: .5,
1997
+ width: 18,
1998
+ flexShrink: 0,
1999
+ lineHeight: 1.4
2000
+ } }, "💬"), h("div", { style: {
2001
+ minWidth: 0,
2002
+ flex: 1
2003
+ } }, h("div", { style: {
2004
+ fontWeight: 600,
2005
+ lineHeight: 1.35
2006
+ } }, "无团队(直接对话)"), h("div", { style: {
2007
+ fontSize: 10.5,
2008
+ color: T.text2,
2009
+ marginTop: 3,
2010
+ lineHeight: 1.45,
2011
+ whiteSpace: "normal",
2012
+ wordBreak: "break-word"
2013
+ } }, "不走 teamflow,模型直接工作")), !active ? h("span", { style: {
2014
+ marginLeft: "auto",
2015
+ color: T.text2,
2016
+ fontSize: 12,
2017
+ flexShrink: 0
2018
+ } }, "✓") : null), teams.map((team) => h("button", {
2019
+ key: team.id,
2020
+ onClick: () => select(team.id),
2021
+ style: {
2022
+ display: "flex",
2023
+ alignItems: "flex-start",
2024
+ gap: 10,
2025
+ width: "100%",
2026
+ padding: "9px 12px",
2027
+ border: "none",
2028
+ cursor: "pointer",
2029
+ textAlign: "left",
2030
+ background: active && active.id === team.id ? `color-mix(in srgb, ${T.brand} 10%, transparent)` : "transparent",
2031
+ color: T.text,
2032
+ fontSize: 12,
2033
+ transition: "background .1s"
2034
+ }
2035
+ }, h("span", { style: {
2036
+ fontSize: 14,
2037
+ width: 18,
2038
+ flexShrink: 0,
2039
+ lineHeight: 1.4
2040
+ } }, team.icon), h("div", { style: {
2041
+ minWidth: 0,
2042
+ flex: 1
2043
+ } }, h("div", { style: {
2044
+ fontWeight: 600,
2045
+ lineHeight: 1.35
2046
+ } }, team.name), h("div", { style: {
2047
+ fontSize: 10.5,
2048
+ color: T.text2,
2049
+ marginTop: 3,
2050
+ lineHeight: 1.45,
2051
+ whiteSpace: "normal",
2052
+ wordBreak: "break-word"
2053
+ } }, team.description)), active && active.id === team.id ? h("span", { style: {
2054
+ marginLeft: "auto",
2055
+ color: T.brand,
2056
+ fontSize: 12,
2057
+ flexShrink: 0
2058
+ } }, "✓") : null))) : null);
2059
+ }
2060
+ /** 解包 remote 信封:失败抛错;成功返回 value。 */
2061
+ function unwrap(res, what) {
2062
+ if (!res || !res.ok) throw new Error(`${what || "remote"} 调用失败:${res && res.error && (res.error.message || res.error.code) || "未知错误"}`);
2063
+ return res.value;
2064
+ }
2065
+ function TeamFlowView(props) {
2066
+ const api = props.remote;
2067
+ const [state, setState] = react.default.useState({
2068
+ runs: [],
2069
+ active: null,
2070
+ backlog: null,
2071
+ err: null,
2072
+ workspace: null
2073
+ });
2074
+ const [runId, setRunId] = react.default.useState(null);
2075
+ const [tab, setTab] = react.default.useState("pipeline");
2076
+ const [busy, setBusy] = react.default.useState(false);
2077
+ const refresh = react.default.useCallback(async () => {
2078
+ if (!api) {
2079
+ setState((s) => ({
2080
+ ...s,
2081
+ err: "remote 未就绪"
2082
+ }));
2083
+ return;
2084
+ }
2085
+ try {
2086
+ const lr = unwrap(await api.list(props.sessionId), "list");
2087
+ const runsList = lr && lr.runs || [];
2088
+ const workspace = lr && lr.workspace || null;
2089
+ const id = runId || runsList[0] && runsList[0].id;
2090
+ const snapWrap = id ? await api.snapshot(id, props.sessionId) : null;
2091
+ const snap = snapWrap ? unwrap(snapWrap, "snapshot") : null;
2092
+ const bo = unwrap(await api.backlog(props.sessionId), "backlog");
2093
+ setState({
2094
+ runs: (runsList || []).slice(0, 12),
2095
+ active: snap,
2096
+ backlog: bo,
2097
+ err: null,
2098
+ workspace
2099
+ });
2100
+ } catch (e) {
2101
+ setState((s) => ({
2102
+ ...s,
2103
+ err: String(e && e.message || e)
2104
+ }));
2105
+ }
2106
+ }, [
2107
+ runId,
2108
+ api,
2109
+ props.sessionId
2110
+ ]);
2111
+ react.default.useEffect(() => {
2112
+ refresh();
2113
+ const t = setInterval(refresh, 2e3);
2114
+ return () => clearInterval(t);
2115
+ }, [refresh]);
2116
+ const { runs, active, backlog, err, workspace } = state;
2117
+ const total = totalUsage(active && active.stages);
2118
+ const needHuman = [];
2119
+ if (backlog) {
2120
+ for (const item of [
2121
+ ...backlog.requirements || [],
2122
+ ...backlog.tasks || [],
2123
+ ...backlog.bugs || []
2124
+ ]) if (item.humanIntervention || item.status === "needs-human") needHuman.push(item);
2125
+ }
2126
+ const activeRun = runs.find((r) => r.id === runId) || runs[0];
2127
+ const canResume = activeRun && activeRun.incompleteStages && (activeRun.status === "interrupted" || activeRun.status === "failed" || activeRun.status === "cancelled");
2128
+ const onResume = async () => {
2129
+ if (!api || !activeRun || busy) return;
2130
+ setBusy(true);
2131
+ try {
2132
+ await api.resume(activeRun.id, props.sessionId);
2133
+ refresh();
2134
+ } catch (e) {
2135
+ setState((s) => ({
2136
+ ...s,
2137
+ err: String(e && e.message || e)
2138
+ }));
2139
+ }
2140
+ setBusy(false);
2141
+ };
2142
+ const anyRunning = runs.some((r) => r.status === "running" || r.status === "pending");
2143
+ const btn = {
2144
+ font: "inherit",
2145
+ fontSize: 12,
2146
+ padding: "4px 12px",
2147
+ borderRadius: 8,
2148
+ cursor: "pointer",
2149
+ border: `1px solid ${T.border2}`,
2150
+ background: T.layer1,
2151
+ color: T.text,
2152
+ transition: "border-color .12s ease, background .12s ease"
2153
+ };
2154
+ const tabBtn = (on) => ({
2155
+ font: "inherit",
2156
+ fontSize: 12,
2157
+ fontWeight: on ? 600 : 500,
2158
+ padding: "5px 14px",
2159
+ borderRadius: 8,
2160
+ cursor: "pointer",
2161
+ border: "none",
2162
+ background: on ? `color-mix(in srgb, ${T.brand} 14%, transparent)` : "transparent",
2163
+ color: on ? T.brand : T.text2,
2164
+ transition: "background .12s ease, color .12s ease"
2165
+ });
2166
+ const chipBtn = (sel) => ({
2167
+ font: "inherit",
2168
+ fontSize: 11,
2169
+ padding: "2px 10px",
2170
+ borderRadius: 999,
2171
+ cursor: "pointer",
2172
+ border: `1px solid ${sel ? T.brand : T.border}`,
2173
+ background: sel ? `color-mix(in srgb, ${T.brand} 12%, transparent)` : "transparent",
2174
+ color: sel ? T.brand : T.text2,
2175
+ fontFamily: MONO
2176
+ });
2177
+ return h("div", { style: {
2178
+ fontFamily: SANS,
2179
+ fontSize: 13,
2180
+ color: T.text,
2181
+ display: "flex",
2182
+ flexDirection: "column",
2183
+ gap: 10,
2184
+ padding: "4px 16px"
2185
+ } }, h("div", { style: {
2186
+ display: "flex",
2187
+ alignItems: "center",
2188
+ gap: 10
2189
+ } }, h("div", { style: {
2190
+ width: 28,
2191
+ height: 28,
2192
+ borderRadius: 8,
2193
+ display: "flex",
2194
+ alignItems: "center",
2195
+ justifyContent: "center",
2196
+ fontSize: 15,
2197
+ background: `color-mix(in srgb, ${T.brand} 12%, transparent)`,
2198
+ border: `1px solid color-mix(in srgb, ${T.brand} 28%, transparent)`
2199
+ } }, "🏭"), h("div", { style: {
2200
+ display: "flex",
2201
+ flexDirection: "column",
2202
+ gap: 1
2203
+ } }, h("span", { style: {
2204
+ fontWeight: 700,
2205
+ fontSize: 14,
2206
+ lineHeight: "18px"
2207
+ } }, "团队工作台"), h("span", { style: {
2208
+ fontSize: 11,
2209
+ color: T.text2,
2210
+ display: "flex",
2211
+ alignItems: "center",
2212
+ gap: 5
2213
+ } }, h("span", { style: {
2214
+ width: 7,
2215
+ height: 7,
2216
+ borderRadius: 999,
2217
+ display: "inline-block",
2218
+ background: anyRunning ? T.success : T.text2,
2219
+ animation: anyRunning ? "tf-pulse 1.6s ease-in-out infinite" : "none"
2220
+ } }), anyRunning ? "流水线运行中" : "空闲")), h("button", {
2221
+ onClick: refresh,
2222
+ style: {
2223
+ ...btn,
2224
+ marginLeft: "auto",
2225
+ display: "flex",
2226
+ alignItems: "center",
2227
+ gap: 5
2228
+ }
2229
+ }, "🔄 刷新"), canResume ? h("button", {
2230
+ onClick: onResume,
2231
+ disabled: busy,
2232
+ title: `断点续跑 ${activeRun.id}\n当前状态:${RUN_STATUS_TEXT[activeRun.status] || activeRun.status};跳过已完成阶段,从第一个未完成阶段重跑`,
2233
+ style: {
2234
+ ...btn,
2235
+ background: T.error,
2236
+ color: "#fff",
2237
+ border: "none",
2238
+ fontWeight: 600
2239
+ }
2240
+ }, busy ? "续跑中…" : `↻ 从断点重跑 #${String(activeRun.id).slice(-6)}`) : null), err ? h("div", { style: {
2241
+ color: T.error,
2242
+ fontSize: 12,
2243
+ background: `color-mix(in srgb, ${T.error} 8%, transparent)`,
2244
+ border: `1px solid color-mix(in srgb, ${T.error} 30%, transparent)`,
2245
+ borderRadius: 8,
2246
+ padding: "7px 11px"
2247
+ } }, `⚠ ${err}(确认已安装 dsh-plugin-teamflow 且 web 已重启)`) : null, needHuman.length > 0 ? h("div", { style: {
2248
+ display: "flex",
2249
+ alignItems: "center",
2250
+ gap: 10,
2251
+ flexWrap: "wrap",
2252
+ background: `color-mix(in srgb, ${T.warn} 9%, ${T.layer1})`,
2253
+ border: `1px solid color-mix(in srgb, ${T.warn} 35%, transparent)`,
2254
+ borderRadius: 10,
2255
+ padding: "9px 12px"
2256
+ } }, h("span", { style: {
2257
+ fontWeight: 700,
2258
+ color: T.warn,
2259
+ fontSize: 12.5
2260
+ } }, `⚠ ${needHuman.length} 项需人工介入`), needHuman.slice(0, 5).map((item) => {
2261
+ const kind = (backlog.requirements || []).some((r) => r.id === item.id) ? "req" : (backlog.tasks || []).some((t) => t.id === item.id) ? "task" : "bug";
2262
+ const fin = kind === "bug" ? "verified" : "accepted";
2263
+ return h("button", {
2264
+ key: item.id,
2265
+ onClick: async () => {
2266
+ await api.backlogUpdate(kind, item.id, fin, props.sessionId, "人工处理");
2267
+ refresh();
2268
+ },
2269
+ style: {
2270
+ ...btn,
2271
+ background: T.success,
2272
+ color: "#fff",
2273
+ border: "none",
2274
+ fontWeight: 600
2275
+ }
2276
+ }, `处理 ${item.id}`);
2277
+ })) : null, h("div", { style: {
2278
+ display: "flex",
2279
+ alignItems: "center",
2280
+ gap: 2,
2281
+ borderBottom: `1px solid ${T.border}`
2282
+ } }, h("button", {
2283
+ onClick: () => setTab("pipeline"),
2284
+ style: tabBtn(tab === "pipeline")
2285
+ }, "🔄 流水线"), h("button", {
2286
+ onClick: () => setTab("board"),
2287
+ style: tabBtn(tab === "board")
2288
+ }, "📋 Backlog 看板"), h("div", { style: {
2289
+ marginLeft: "auto",
2290
+ display: "flex",
2291
+ alignItems: "center",
2292
+ gap: 10
2293
+ } }, activeRun ? h("span", {
2294
+ title: `${activeRun.id}\n${activeRun.requirement || ""}`,
2295
+ style: {
2296
+ fontSize: 11.5,
2297
+ fontFamily: MONO,
2298
+ padding: "2px 10px",
2299
+ borderRadius: 999,
2300
+ background: T.layer2,
2301
+ color: activeRun.status === "interrupted" ? T.warn : T.text2,
2302
+ border: `1px solid ${T.border}`
2303
+ }
2304
+ }, `#${String(activeRun.id).slice(-8)} · ${RUN_STATUS_TEXT[activeRun.status] || activeRun.status}`) : null, total && total.input + total.cacheRead + total.cacheWrite + total.output > 0 ? h("span", {
2305
+ style: {
2306
+ fontSize: 11.5,
2307
+ fontFamily: MONO,
2308
+ color: T.text2,
2309
+ cursor: "help"
2310
+ },
2311
+ title: "输入(未命中)/输入(命中)/输出 全部阶段合计"
2312
+ }, `∑ ⇅${fmtTokens(total.input)}/⇅${fmtTokens(total.cacheRead)}·⬆${fmtTokens(total.output)}`) : null)), h("div", { style: {
2313
+ display: "flex",
2314
+ alignItems: "center",
2315
+ gap: 8,
2316
+ fontSize: 12,
2317
+ flexWrap: "wrap"
2318
+ } }, h("span", {
2319
+ title: `当前工作区(workspace 级隔离):${workspace && workspace.path || "未连接工作区"}`,
2320
+ style: {
2321
+ display: "inline-flex",
2322
+ alignItems: "center",
2323
+ gap: 6,
2324
+ fontFamily: MONO,
2325
+ fontSize: 11.5,
2326
+ padding: "2px 10px",
2327
+ borderRadius: 999,
2328
+ background: `color-mix(in srgb, ${T.brand} 10%, transparent)`,
2329
+ border: `1px solid color-mix(in srgb, ${T.brand} 30%, transparent)`,
2330
+ color: T.brand
2331
+ }
2332
+ }, h("span", { style: { fontSize: 13 } }, "🗂"), h("span", {
2333
+ title: workspace && workspace.path ? String(workspace.path) : void 0,
2334
+ style: {
2335
+ overflow: "hidden",
2336
+ textOverflow: "ellipsis",
2337
+ whiteSpace: "nowrap",
2338
+ maxWidth: 220
2339
+ }
2340
+ }, workspace && workspace.path ? String(workspace.path).split(/[\\/]/).filter(Boolean).pop() : "ungrouped")), tab === "pipeline" ? h("div", { style: {
2341
+ display: "flex",
2342
+ alignItems: "center",
2343
+ gap: 5
2344
+ } }, h("span", { style: { color: T.text2 } }, "历史"), runs.length ? runs.map((r) => {
2345
+ const sel = r.id === (runId || runs[0] && runs[0].id);
2346
+ return h("button", {
2347
+ key: r.id,
2348
+ onClick: () => setRunId(r.id),
2349
+ style: chipBtn(sel),
2350
+ title: `${r.id}\n${r.requirement || ""}`
2351
+ }, `#${String(r.id).slice(-6)}`);
2352
+ }) : h("span", { style: {
2353
+ color: T.text2,
2354
+ fontSize: 11.5
2355
+ } }, "(暂无)")) : null), tab === "pipeline" ? h(PipelinePanel, {
2356
+ active,
2357
+ api,
2358
+ runId: activeRun ? activeRun.id : null,
2359
+ sessionId: props.sessionId,
2360
+ sessions: props.sessions
2361
+ }) : h(BoardPanel, {
2362
+ backlog,
2363
+ api,
2364
+ onRefresh: refresh,
2365
+ sessionId: props.sessionId,
2366
+ onShowRun: (rid) => {
2367
+ setTab("pipeline");
2368
+ setRunId(rid);
2369
+ }
2370
+ }));
2371
+ }
2372
+ if (typeof document !== "undefined") {
2373
+ const styleEl = document.createElement("style");
2374
+ styleEl.textContent = `
2375
+ @keyframes tf-pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .45; transform: scale(.8); } }
2376
+ @keyframes tf-shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(350%); } }
2377
+ @keyframes tf-flow { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -28 } }`;
2378
+ document.head.appendChild(styleEl);
2379
+ }
2380
+ /** 注册 conversation.view tab「团队工作台」+ input.right 团队选择按钮。 */
2381
+ async function apply(ctx) {
2382
+ await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
2383
+ const teamflow = ctx.get("remote.teamflow");
2384
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
2385
+ name: "conversation.view",
2386
+ id: "teamflow",
2387
+ order: 20,
2388
+ label: "🏭 团队工作台",
2389
+ inject: (sessionId) => ({
2390
+ sessionId,
2391
+ remote: teamflow,
2392
+ sessions: ctx.get("sessions")
2393
+ })
2394
+ }, TeamFlowView));
2395
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2396
+ name: "conversation.input.right",
2397
+ id: "teamflow-team-select",
2398
+ order: 5,
2399
+ inject: (sessionId) => ({
2400
+ sessionId,
2401
+ remote: teamflow
2402
+ })
2403
+ }, TeamSelector));
2404
+ }
2405
+ //#endregion
2406
+ exports.apply = apply;
2407
+ exports.inject = inject;
2408
+ return module.exports;
2409
+ }
2410
+ });
2411
+
2412
+ //# sourceMappingURL=client.js.map