dsh-plugin-teamflow 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -51,6 +51,15 @@ window.__ModuleLoader__.load({
51
51
  parameters: [],
52
52
  result: strict
53
53
  },
54
+ {
55
+ id: "dsh-plugin-teamflow#teamflow/setLocale",
56
+ service: "teamflow",
57
+ namespace: "teamflow",
58
+ method: "setLocale",
59
+ invocation: { kind: "direct" },
60
+ parameters: [p("locale")],
61
+ result: strict
62
+ },
54
63
  {
55
64
  id: "dsh-plugin-teamflow#teamflow/list",
56
65
  service: "teamflow",
@@ -218,6 +227,60 @@ window.__ModuleLoader__.load({
218
227
  p("sessionId")
219
228
  ],
220
229
  result: strict
230
+ },
231
+ {
232
+ id: "dsh-plugin-teamflow#teamflow/products",
233
+ service: "teamflow",
234
+ namespace: "teamflow",
235
+ method: "products",
236
+ invocation: { kind: "direct" },
237
+ parameters: [p("sessionId")],
238
+ result: strict
239
+ },
240
+ {
241
+ id: "dsh-plugin-teamflow#teamflow/productView",
242
+ service: "teamflow",
243
+ namespace: "teamflow",
244
+ method: "productView",
245
+ invocation: { kind: "direct" },
246
+ parameters: [p("product")],
247
+ result: strict
248
+ },
249
+ {
250
+ id: "dsh-plugin-teamflow#teamflow/productRunDetail",
251
+ service: "teamflow",
252
+ namespace: "teamflow",
253
+ method: "productRunDetail",
254
+ invocation: { kind: "direct" },
255
+ parameters: [p("product"), p("runId")],
256
+ result: strict
257
+ },
258
+ {
259
+ id: "dsh-plugin-teamflow#teamflow/productStageDetail",
260
+ service: "teamflow",
261
+ namespace: "teamflow",
262
+ method: "productStageDetail",
263
+ invocation: { kind: "direct" },
264
+ parameters: [
265
+ p("product"),
266
+ p("runId"),
267
+ p("seq")
268
+ ],
269
+ result: strict
270
+ },
271
+ {
272
+ id: "dsh-plugin-teamflow#teamflow/productItemDetail",
273
+ service: "teamflow",
274
+ namespace: "teamflow",
275
+ method: "productItemDetail",
276
+ invocation: { kind: "direct" },
277
+ parameters: [
278
+ p("product"),
279
+ p("kind"),
280
+ p("id"),
281
+ p("sessionId")
282
+ ],
283
+ result: strict
221
284
  }
222
285
  ]);
223
286
  /** client 端 $mount 使用的贡献对象。 */
@@ -226,27 +289,59 @@ window.__ModuleLoader__.load({
226
289
  descriptors: TEAMFLOW_DESCRIPTORS
227
290
  });
228
291
  //#endregion
229
- //#region client/index.tsx
292
+ //#region client/shared.tsx
230
293
  /**
231
- * dsh-plugin-teamflow — browser half(v0.7 视觉重构:「精密工程控制台」)。
294
+ * dsh-plugin-teamflow — browser half 共享展示层(主题 token / 状态词表 / 格式化)。
232
295
  *
233
- * 注册 conversation.view tab「🏭 团队工作台」(与 chat / 轨迹并列):
234
- * - 流水线图形工作流:阶段泳道 + 节点卡片(状态/耗时/token/子代理会话)+ 实时轮询
235
- * - Backlog 拖拽看板:需求/任务/缺陷三组状态泳道,卡片拖拽流转(原生 HTML5 DnD,零依赖)
236
- * - 成本中心:每阶段 token + 总计 + 运行时长
237
- * - 人工介入中心:needs-human 项聚合 + 一键终态
238
- * - 历史 run 切换
296
+ * 会话内工作台(index.tsx)与全局面板(panel.tsx)共用:**只放无状态纯展示件**,
297
+ * 不放任何 Remote 调用或会话上下文逻辑——两边数据来源不同(sessionId / productKey),
298
+ * 展示语言必须一致。
239
299
  *
240
- * 视觉:全部使用 DSH 主题 token(--dsw-alias-*,自动适配深浅色)。
241
- * Remote 接入:$mount 后 ctx.get('remote.teamflow') 取全局实例传给组件
242
- * (不能 inject 'remote.teamflow'——自挂载自注入会让 fiber 死锁)。
300
+ * 双语(v0.1.9):文案统一走宿主 locale 服务(机制说明见 client/locales.ts)。
301
+ * 组件里能拿到注入的 `t`,但**词表/格式化/折叠件是纯函数**,拿不到 prop,
302
+ * 故由 `apply()` `setTranslator()` 注入模块级翻译函数:`bind()` 每次调用都读当前语言,
303
+ * 且宿主在切语言时会重渲染每个 slot outlet(ui-renderer `useLocaleRevision`),
304
+ * 因此模块级函数不会持有过期语言。
243
305
  */
244
- const inject = [
245
- "remote",
246
- "slots",
247
- "sessions",
248
- "locale"
249
- ];
306
+ /** 翻译函数(默认恒等:未注入时显示 key 本身,便于发现漏注册)。 */
307
+ let translate = (key) => key;
308
+ /** 当前语言 id 的读取器(默认 en)。 */
309
+ let localeIdOf = () => "en";
310
+ /**
311
+ * 注入翻译函数与语言读取器(apply 时调用一次)。
312
+ * @param fn - `ctx.locale.bind(NS)` 的返回值(调用时读当前语言)。
313
+ * @param idOf - 返回当前语言 id 的函数(如 `() => ctx.locale.getSnapshot().active`)。
314
+ */
315
+ function setTranslator(fn, idOf) {
316
+ if (typeof fn === "function") translate = fn;
317
+ if (typeof idOf === "function") localeIdOf = idOf;
318
+ }
319
+ /** 翻译(`{name}` 占位符由宿主替换)。 */
320
+ const t = (key, params) => translate(key, params);
321
+ /** 当前语言的 BCP 47 标签(时间格式化用;未知语言回退 en)。 */
322
+ function localeTag() {
323
+ let id = "en";
324
+ try {
325
+ id = String(localeIdOf() || "en");
326
+ } catch (e) {}
327
+ return id === "zh" ? "zh-CN" : id;
328
+ }
329
+ /** 词表查表:命中返回译文,未命中回退原始值(未知状态/角色等)。 */
330
+ function vocab(prefix, raw) {
331
+ const key = `${prefix}.${raw}`;
332
+ const hit = t(key);
333
+ return hit === key ? String(raw === null || raw === void 0 ? "" : raw) : hit;
334
+ }
335
+ const stText = (s) => vocab("status", s);
336
+ const runStatusText = (s) => vocab("runStatus", s);
337
+ const kindTitle = (k) => vocab("kind", k);
338
+ const roleName = (r) => vocab("role", r);
339
+ /** 角色 chip(带图标;未知角色回退「⚙️ <raw>」)。 */
340
+ function roleChip(r) {
341
+ const key = `roleChip.${r}`;
342
+ const hit = t(key);
343
+ return hit === key ? `⚙️ ${String(r)}` : hit;
344
+ }
250
345
  const T = {
251
346
  bg: "var(--dsw-alias-bg-base)",
252
347
  layer1: "var(--dsw-alias-bg-layer-1)",
@@ -260,30 +355,7 @@ window.__ModuleLoader__.load({
260
355
  success: "var(--dsw-alias-state-success-primary)",
261
356
  warn: "var(--dsw-alias-state-warn-primary)"
262
357
  };
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
- };
358
+ /** 状态色表(与语言无关的纯视觉映射)。 */
287
359
  const STATUS_COLOR = {
288
360
  created: T.text2,
289
361
  pending: T.text2,
@@ -308,7 +380,7 @@ window.__ModuleLoader__.load({
308
380
  interrupted: T.warn,
309
381
  superseded: T.text2
310
382
  };
311
- /** 阶段英文键 → 图标/中文展示名(2026-09-06 英文化:journal.phase 为英文键,展示名统一走映射——未来 i18n 换表即换语言)。 */
383
+ /** 阶段英文键 → 图标(2026-09-06 英文化:journal.phase 为英文键,展示名统一走词表——换语言即换表)。 */
312
384
  const PHASE_ICON = {
313
385
  prd: "📋",
314
386
  design: "🎨",
@@ -318,17 +390,6 @@ window.__ModuleLoader__.load({
318
390
  qa: "🧪",
319
391
  acceptance: "✅"
320
392
  };
321
- const PHASE_NAME = {
322
- prd: "PRD 产品需求",
323
- design: "UI/UX 设计",
324
- scaffold: "架构规划",
325
- tech: "技术方案",
326
- dev: "开发",
327
- qa: "QA 测试",
328
- acceptance: "产品验收"
329
- };
330
- const phaseNameOf = (p) => PHASE_NAME[p] || p || "—";
331
- const phaseIconOf = (p) => PHASE_ICON[p] || "⚙️";
332
393
  /** phase 归一:英文键直通;存量中文映射(防御性——新数据全英文)。 */
333
394
  const phaseKeyOf = (p) => ({
334
395
  "PRD 产品需求": "prd",
@@ -339,15 +400,21 @@ window.__ModuleLoader__.load({
339
400
  "QA 测试": "qa",
340
401
  "产品验收": "acceptance"
341
402
  })[p] || String(p || "");
342
- const RUN_STATUS_TEXT = {
343
- pending: "等待中",
344
- running: "进行中",
345
- completed: "已完成",
346
- failed: "失败",
347
- cancelled: "已取消",
348
- interrupted: "已中断",
349
- superseded: "已取代"
350
- };
403
+ const phaseNameOf = (p) => vocab("phase", phaseKeyOf(p));
404
+ const phaseIconOf = (p) => PHASE_ICON[phaseKeyOf(p)] || "⚙️";
405
+ /**
406
+ * 阶段的展示名(双语安全的取法)。
407
+ *
408
+ * journal 里 `stage.label` 是**持久化中文**(teams.json 配置 + 历史数据),而每个阶段都带
409
+ * 英文 `phase` 键——所以展示时:任务级阶段(dev 子卡,`taskKey` 有值)保留任务名(LLM 数据,
410
+ * 不该翻译),其余阶段用 `phase` 键查当前语言词表。**只影响展示,不动数据**:
411
+ * `__taskKey`/`taskKeyOf` 的任务聚合身份仍走 label 清理(聚合语义不能随语言变)。
412
+ */
413
+ function stageLabelOf(s) {
414
+ const raw = String(s && s.label || "");
415
+ if (s && s.taskKey) return raw || String(s.taskKey);
416
+ return (s && s.phase ? phaseNameOf(s.phase) : "") || raw;
417
+ }
351
418
  const COLUMNS = {
352
419
  req: [
353
420
  "created",
@@ -377,11 +444,6 @@ window.__ModuleLoader__.load({
377
444
  "needs-human"
378
445
  ]
379
446
  };
380
- const KIND_TITLE = {
381
- req: "需求",
382
- task: "任务",
383
- bug: "缺陷"
384
- };
385
447
  const h = react.default.createElement;
386
448
  const MONO = "ui-monospace, SFMono-Regular, Consolas, \"Cascadia Mono\", monospace";
387
449
  const SANS = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif";
@@ -391,7 +453,7 @@ window.__ModuleLoader__.load({
391
453
  gap: 6,
392
454
  flexWrap: "wrap"
393
455
  };
394
- /** 状态徽章:半透明底 + 状态色文字 + 圆角 pill */
456
+ /** 状态徽章:半透明底 + 状态色文字 + 圆角 pill(超宽可省略:窄列/窄卡里不顶破外层)。 */
395
457
  const chip = (text, color, opts = {}) => h("span", { style: {
396
458
  display: "inline-flex",
397
459
  alignItems: "center",
@@ -404,21 +466,25 @@ window.__ModuleLoader__.load({
404
466
  background: `color-mix(in srgb, ${color} 14%, transparent)`,
405
467
  color,
406
468
  whiteSpace: "nowrap",
469
+ maxWidth: "100%",
470
+ overflow: "hidden",
471
+ textOverflow: "ellipsis",
407
472
  ...opts.style || {}
408
473
  } }, opts.dot ? h("span", { style: {
409
474
  width: 5,
410
475
  height: 5,
411
476
  borderRadius: 999,
412
477
  background: color,
413
- display: "inline-block"
478
+ display: "inline-block",
479
+ flex: "0 0 auto"
414
480
  } }) : null, text);
415
481
  /** 可折叠长文本:默认只显示前几行预览,「展开全文」/「收起」双向切换(数据不动,纯展示层——summary/需求原文等富文本不再铺满抽屉)。 */
416
482
  function FoldableText({ text, charLimit = 280, lineLimit = 5, style }) {
417
483
  const [open, setOpen] = react.default.useState(false);
418
484
  if (!text) return null;
419
- const t = String(text);
420
- const lines = t.split("\n");
421
- const compact = lines.length <= lineLimit && t.length <= charLimit;
485
+ const s = String(text);
486
+ const lines = s.split("\n");
487
+ const compact = lines.length <= lineLimit && s.length <= charLimit;
422
488
  const body = (txt) => h("div", { style: {
423
489
  fontSize: 11.5,
424
490
  color: T.text,
@@ -427,10 +493,10 @@ window.__ModuleLoader__.load({
427
493
  wordBreak: "break-word",
428
494
  ...style || {}
429
495
  } }, txt);
430
- if (compact) return body(t);
431
- if (open) return h("div", null, body(t), h("button", {
496
+ if (compact) return body(s);
497
+ if (open) return h("div", null, body(s), h("button", {
432
498
  onClick: () => setOpen(false),
433
- title: "收起全文",
499
+ title: t("common.collapseFull"),
434
500
  style: {
435
501
  marginTop: 3,
436
502
  font: "inherit",
@@ -442,12 +508,12 @@ window.__ModuleLoader__.load({
442
508
  padding: 0,
443
509
  cursor: "pointer"
444
510
  }
445
- }, "收起"));
446
- const pre = lines.length > lineLimit ? lines.slice(0, lineLimit).join("\n") : t.slice(0, charLimit);
447
- const more = lines.length > lineLimit ? `… +${lines.length - lineLimit} 行` : "…";
511
+ }, t("common.collapse")));
512
+ const pre = lines.length > lineLimit ? lines.slice(0, lineLimit).join("\n") : s.slice(0, charLimit);
513
+ const more = lines.length > lineLimit ? t("common.moreLines", { n: lines.length - lineLimit }) : "…";
448
514
  return h("div", null, body(pre), h("button", {
449
515
  onClick: () => setOpen(true),
450
- title: "点击查看全文",
516
+ title: t("common.clickToExpand"),
451
517
  style: {
452
518
  marginTop: 3,
453
519
  font: "inherit",
@@ -459,97 +525,2076 @@ window.__ModuleLoader__.load({
459
525
  padding: 0,
460
526
  cursor: "pointer"
461
527
  }
462
- }, `展开全文${more}`));
463
- }
464
- function fmtDur(a, b) {
465
- if (!a) return "";
466
- const ms = (b || Date.now()) - a;
467
- if (ms < 0) return "";
468
- const s = Math.floor(ms / 1e3);
469
- if (s < 60) return `${s}s`;
470
- return `${Math.floor(s / 60)}m${s % 60}s`;
471
- }
472
- function fmtTokens(n) {
473
- if (n === null || n === void 0) return "";
474
- if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
475
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
476
- return String(n);
477
- }
478
- /** 官方口径计费合计(billed input + output)。 */
479
- function totalTokens(u) {
480
- return (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0);
528
+ }, `${t("common.expandFull")}${more}`));
529
+ }
530
+ function fmtTime(tm) {
531
+ if (!tm) return "";
532
+ const d = new Date(tm);
533
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
534
+ }
535
+ function fmtDur(a, b) {
536
+ if (!a) return "";
537
+ const ms = (b || Date.now()) - a;
538
+ if (ms < 0) return "";
539
+ const s = Math.floor(ms / 1e3);
540
+ if (s < 60) return `${s}s`;
541
+ return `${Math.floor(s / 60)}m${s % 60}s`;
542
+ }
543
+ function fmtTokens(n) {
544
+ if (n === null || n === void 0) return "";
545
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
546
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
547
+ return String(n);
548
+ }
549
+ /** 官方口径计费合计(billed input + output)。 */
550
+ function totalTokens(u) {
551
+ return (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0) + (u.output || 0);
552
+ }
553
+ /** 官方口径工具函数(与 DeepSeek usage 账单对齐,零额外概念):
554
+ * - u.input : 输入(缓存未命中)
555
+ * - u.cacheRead : 输入(缓存命中)
556
+ * - u.cacheWrite : 输入写入缓存
557
+ * - u.output : 输出
558
+ * billed input = input + cacheRead + cacheWrite;命中率 = cacheRead / (input + cacheRead)。
559
+ */
560
+ function hitRate(u) {
561
+ const total = (u.input || 0) + (u.cacheRead || 0);
562
+ return total > 0 ? Math.round((u.cacheRead || 0) / total * 100) : null;
563
+ }
564
+ function usageDetail(s) {
565
+ const k = (n) => n === null || n === void 0 ? "—" : fmtTokens(n);
566
+ if (s.usage) {
567
+ const u = s.usage;
568
+ const hit = hitRate(u);
569
+ return t("token.usageLine", {
570
+ input: k(u.input),
571
+ cacheRead: k(u.cacheRead),
572
+ cacheWrite: k(u.cacheWrite),
573
+ output: k(u.output),
574
+ calls: u.calls
575
+ }) + (hit !== null ? t("token.usageHit", { hit }) : "");
576
+ }
577
+ return t("token.usageMissing");
578
+ }
579
+ /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
580
+ function stageUsageLine(s) {
581
+ const u = s && s.usage;
582
+ if (u && (u.input || u.cacheRead || u.cacheWrite || u.output)) {
583
+ const hit = hitRate(u);
584
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""}`;
585
+ }
586
+ return null;
587
+ }
588
+ const roleUsage = (u) => {
589
+ if (!u) return "";
590
+ const hit = hitRate(u);
591
+ return `⇅${fmtTokens(u.input || 0)}/${fmtTokens(u.cacheRead || 0)}·⬆${fmtTokens(u.output || 0)}${hit !== null ? `·${hit}%` : ""}`;
592
+ };
593
+ /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
594
+ function byRoleLine(task) {
595
+ const roles = task && task.byRole || {};
596
+ return Object.keys(roles).filter((k) => roles[k] && roles[k].input + roles[k].output + roles[k].cacheRead + roles[k].cacheWrite > 0).map((k) => `${roleName(k)} ${roleUsage(roles[k])}`).join(" · ");
597
+ }
598
+ /** 多阶段 usage 汇总(官方口径)。 */
599
+ function totalUsage(stages) {
600
+ const sum = {
601
+ input: 0,
602
+ cacheRead: 0,
603
+ cacheWrite: 0,
604
+ output: 0,
605
+ calls: 0
606
+ };
607
+ for (const s of stages || []) {
608
+ const u = s && s.usage;
609
+ if (!u) continue;
610
+ sum.input += u.input || 0;
611
+ sum.cacheRead += u.cacheRead || 0;
612
+ sum.cacheWrite += u.cacheWrite || 0;
613
+ sum.output += u.output || 0;
614
+ sum.calls += u.calls || 0;
615
+ }
616
+ return sum;
617
+ }
618
+ const stColor = (s) => STATUS_COLOR[s] || T.text2;
619
+ //#endregion
620
+ //#region client/locales.ts
621
+ /**
622
+ * dsh-plugin-teamflow — 客户端文案词典(zh / en)。
623
+ *
624
+ * 机制:走宿主 `ctx.locale`(@deepseek-ai/dsh-client-locale),不自建 i18n:
625
+ * - `apply()` 里 `ctx.locale.register(NS, { zh, en })` 注册两个词典;
626
+ * - `ctx.locale.bind(NS)` 取翻译函数(调用时读当前语言,故可常驻模块变量);
627
+ * - slot 注册项声明 `locale: NS`,切语言时宿主重渲染每个 outlet
628
+ * (ui-renderer `useLocaleRevision`),面板/tab 名称用 thunk `() => t(...)`。
629
+ *
630
+ * 约定:
631
+ * - key 扁平点号命名,按区域前缀分组(common/status/phase/kind/role/stage/…);
632
+ * - 占位符用 `{name}`(宿主 `translate` 的替换语法);
633
+ * - **zh 与 en 的 key 集合必须完全一致**(en 是兜底语言,缺 key 会直接显示 key 本身;
634
+ * 由 test/smoke.js 断言守门);
635
+ * - 数据面文案(LLM 产出的标题/摘要、teams.json 的团队名、journal 里既有中文 label)不在此表,
636
+ * 属 host/产物语言层,见 AGENTS §5「客户端面」锚点。
637
+ */
638
+ /** 词典命名空间(宿主 LocaleRuntime 的单占位命名空间)。 */
639
+ const NS = "teamflow";
640
+ /** 中文词典(key 为单一事实来源,en 必须与之同形)。 */
641
+ const zh = {
642
+ "common.close": "关闭",
643
+ "common.loading": "加载中…",
644
+ "common.retry": "重试",
645
+ "common.collapse": "收起",
646
+ "common.collapseFull": "收起全文",
647
+ "common.clickToExpand": "点击查看全文",
648
+ "common.expandFull": "展开全文",
649
+ "common.moreLines": "… +{n} 行",
650
+ "common.none": "无",
651
+ "common.noneDash": "(暂无)",
652
+ "common.noSummary": "(无摘要)",
653
+ "common.failed": "失败",
654
+ "common.running": "进行中",
655
+ "common.needsHuman": "需人工介入",
656
+ "common.unknownError": "未知错误",
657
+ "common.gotIt": "知道了",
658
+ "common.remoteNotReady": "remote 未就绪",
659
+ "common.remoteCallFailed": "{what} 调用失败:{detail}",
660
+ "common.remoteEmptyResult": "{what} 返回空结果(ok=true 但无 value;原始信封={raw})",
661
+ "common.calls": "{n} 次",
662
+ "common.callsCount": "{n} 次调用",
663
+ "common.noUsage": "无 token 数据",
664
+ "status.created": "立项",
665
+ "status.in-progress": "进行中",
666
+ "status.pending-acceptance": "待验收",
667
+ "status.accepted": "已验收",
668
+ "status.closed": "已关闭",
669
+ "status.pending": "待办",
670
+ "status.running": "开发中",
671
+ "status.testable": "待测试",
672
+ "status.testing": "测试中",
673
+ "status.rework": "打回",
674
+ "status.needs-human": "需人工",
675
+ "status.cancelled": "已关闭",
676
+ "status.open": "待认领",
677
+ "status.claimed": "处理中",
678
+ "status.fixed": "已修复待验",
679
+ "status.verified": "已关闭",
680
+ "status.reopened": "重开",
681
+ "status.done": "已完成",
682
+ "status.completed": "已完成",
683
+ "status.failed": "失败",
684
+ "status.interrupted": "已中断",
685
+ "status.superseded": "已取代",
686
+ "runStatus.pending": "等待中",
687
+ "runStatus.running": "进行中",
688
+ "runStatus.completed": "已完成",
689
+ "runStatus.failed": "失败",
690
+ "runStatus.cancelled": "已取消",
691
+ "runStatus.interrupted": "已中断",
692
+ "runStatus.superseded": "已取代",
693
+ "phase.prd": "PRD 产品需求",
694
+ "phase.design": "UI/UX 设计",
695
+ "phase.scaffold": "架构规划",
696
+ "phase.tech": "技术方案",
697
+ "phase.dev": "开发",
698
+ "phase.qa": "QA 测试",
699
+ "phase.acceptance": "产品验收",
700
+ "kind.req": "需求",
701
+ "kind.task": "任务",
702
+ "kind.bug": "缺陷",
703
+ "role.pm": "产品",
704
+ "role.design": "设计",
705
+ "role.arch": "架构",
706
+ "role.tech": "方案",
707
+ "role.dev": "开发",
708
+ "role.qa": "测试",
709
+ "role.acceptance": "验收",
710
+ "role.other": "其他",
711
+ "roleChip.pm": "📌 产品",
712
+ "roleChip.design": "🎨 设计",
713
+ "roleChip.arch": "🏗 架构",
714
+ "roleChip.tech": "📐 方案",
715
+ "roleChip.dev": "👨‍💻 开发",
716
+ "roleChip.qa": "🧪 QA",
717
+ "roleChip.acceptance": "✅ 验收",
718
+ "token.officialTitle": "TOKEN · 官方口径",
719
+ "token.usageLine": "输入(未命中) {input} / 输入(命中) {cacheRead} / 写缓存 {cacheWrite} / 输出 {output} · {calls} 次调用",
720
+ "token.usageHit": " · 缓存命中 {hit}%",
721
+ "token.usageMissing": "无 usage 明细",
722
+ "token.inputMiss": "输入(未命中) {v}",
723
+ "token.inputHit": "输入(命中) {v}",
724
+ "token.cacheWrite": "写缓存 {v}",
725
+ "token.output": "输出 {v}",
726
+ "token.total": "合计 {v}",
727
+ "token.allStagesTip": "输入(未命中)/输入(命中)/输出 全部阶段合计",
728
+ "token.stageLine": "阶段 token:{v}",
729
+ "stage.cardTip": "{label} —— 点击查看阶段详情",
730
+ "stage.retryTip": "重试 {n} 次(共 {m} 次尝试)",
731
+ "stage.detailTitle": "阶段详情",
732
+ "stage.summaryFallback": "(该 run 未保存完整正文,展示摘要)\n\n{summary}",
733
+ "stage.loadFailed": "⚠ 加载失败:{err}",
734
+ "stage.noOutput": "(无产物正文)",
735
+ "stage.attemptHistory": "↻ 尝试历史({n} 次 · 点击查看该次详情)",
736
+ "stage.attemptDone": "✅ 成功",
737
+ "stage.attemptFailed": "❌ {outcome}",
738
+ "stage.attemptRunning": "⏳ 进行中",
739
+ "stage.childJumpBtn": "🎬 跳转子代理会话",
740
+ "stage.childJumpTip": "跳转到该阶段子代理会话(完整推理与工具调用轨迹);跳转后请切换「对话」tab 查看",
741
+ "stage.childNoneTip": "该阶段无可用子代理会话",
742
+ "stage.childCrossSessionTip": "该子代理由会话 {sid} 发起,跨会话跳转暂不支持——请打开其发起会话的团队工作台查看",
743
+ "stage.childCrossSessionNote": "跨会话暂不支持:该子代理由会话 {sid} 发起。如需查看轨迹,请打开其发起会话的团队工作台。",
744
+ "stage.childJumpNote": "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹",
745
+ "stage.evidenceTitle": "🔬 验证证据",
746
+ "stage.evidenceMissing": "(缺失——契约未兑现,host 已记警告;可与该 run 的命令日志对照:run 期间在 logs/teamflow/<runId>/,结束后由 host 归档到 $DSH_HOME/teamflow/<workspace>/logs/<runId>/)",
747
+ "stage.evidenceMissingShort": "(缺失——契约未兑现,host 已记警告)",
748
+ "stage.artifactsTitle": "📄 阶段性产物",
749
+ "pipeline.empty": "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求",
750
+ "pipeline.noNodes": "流水线还没有开始执行节点",
751
+ "pipeline.zoomOut": "缩小",
752
+ "pipeline.zoomIn": "放大",
753
+ "pipeline.fitCanvas": "适应画布",
754
+ "pipeline.canvasHint": "✥ 拖动画布 · 滚轮缩放",
755
+ "workbench.title": "团队工作台",
756
+ "workbench.running": "流水线运行中",
757
+ "workbench.idle": "空闲",
758
+ "workbench.refresh": "🔄 刷新",
759
+ "workbench.resumeTip": "断点续跑 {id}\n当前状态:{status};跳过已完成阶段,从第一个未完成阶段重跑",
760
+ "workbench.resuming": "续跑中…",
761
+ "workbench.resumeBtn": "↻ 从断点重跑 #{id}",
762
+ "workbench.loadFailed": "⚠ {err}(确认已安装 dsh-plugin-teamflow 且 web 已重启)",
763
+ "workbench.needsHumanBanner": "⚠ {n} 项需人工介入",
764
+ "workbench.handle": "处理 {id}",
765
+ "workbench.manualReason": "人工处理",
766
+ "workbench.tabPipeline": "🔄 流水线",
767
+ "workbench.tabBoard": "📋 Backlog 看板",
768
+ "workbench.workspaceTip": "当前工作区(workspace 级隔离):{path}",
769
+ "workbench.noWorkspace": "未连接工作区",
770
+ "workbench.history": "历史",
771
+ "workbench.openRightBarTip": "在右侧栏打开该 run 详情(与任务夹产物并排看)",
772
+ "workbench.openRightBarBtn": "⇥ 右栏打开",
773
+ "board.empty": "backlog 为空(还没有流水线运行过)",
774
+ "board.dragReason": "看板拖拽流转",
775
+ "board.cardTip": "{id} · {status}{summary}(点击查看详情)",
776
+ "board.assignDevTip": "dev 分配\n{who}",
777
+ "board.assignQaTip": "qa 分配\n{who}",
778
+ "board.acceptTip": "验收/汇报人\n{who}",
779
+ "board.subtaskCount": "📦 {n} 子卡",
780
+ "board.retries": "重试 {n}",
781
+ "item.overview": "概览",
782
+ "item.acceptRow": "✅ 验收",
783
+ "item.retryRow": "↻ 重试",
784
+ "item.humanRow": "⚠ 人工介入",
785
+ "item.updatedAt": "更新于",
786
+ "item.runSection": "运行",
787
+ "item.runBtn": "▶ 流水线",
788
+ "item.jumpRunTip": "跳转到该需求的流水线视图 #{id}",
789
+ "item.requirement": "需求原文",
790
+ "item.noRequirement": "(无原文)",
791
+ "item.runDocs": "任务夹",
792
+ "item.previewTip": "在右侧栏预览 {path}",
793
+ "item.subtasks": "关联子卡({n})",
794
+ "item.bugs": "关联缺陷({n})",
795
+ "item.timeline": "流转时间线({n})",
796
+ "team.currentTip": "当前团队:{name}(点击切换)",
797
+ "team.pick": "选择团队",
798
+ "team.label": "团队",
799
+ "team.none": "无团队(直接对话)",
800
+ "team.noneNote": "不走 teamflow,模型直接工作",
801
+ "panel.title": "🏭 团队工作台",
802
+ "panel.subtitle": "全局面板 · 按产品线",
803
+ "panel.currentSession": "当前会话 {sid}",
804
+ "panel.noSession": "无当前会话",
805
+ "panel.reload": "刷新数据",
806
+ "panel.backToChat": "回到对话",
807
+ "panel.backToChatTip": "回到对话(再点侧边栏图标即可切回本面板)",
808
+ "panel.railTitle": "产品线 · {n}",
809
+ "panel.rescanTip": "重新扫描 $DSH_HOME/teamflow",
810
+ "panel.refreshing": "刷新中",
811
+ "panel.refresh": "刷新",
812
+ "panel.noProducts": "还没有产品线。在某个工作区跑过一次流水线后,这里会出现对应产品线($DSH_HOME/teamflow/<key>)。",
813
+ "panel.activeRuns": "运行 {n}",
814
+ "panel.updated": "更新 {time}",
815
+ "panel.reading": "读取中…",
816
+ "panel.verdict": "验收 {v}",
817
+ "panel.pickProduct": "选择左侧产品线查看 backlog 与 run(首次进入默认选最近更新的产品线)。",
818
+ "panel.loadingView": "读取产品线数据中…",
819
+ "panel.runChip": "run {n}",
820
+ "panel.activeChip": "活跃 {n}",
821
+ "panel.verdictChip": "验收 {v}",
822
+ "panel.tabRuns": "🚀 流水线 run · {n}",
823
+ "panel.tabBacklog": "📋 Backlog · {n}",
824
+ "panel.pinnedActive": "已置顶进行中 {n}",
825
+ "panel.showRecent": "只看最近 {n} 条",
826
+ "panel.showAll": "展开全部 {n} 条",
827
+ "panel.clearRunFilterTip": "清除 run 状态筛选",
828
+ "panel.filtered": "筛选中 {sel} 项 · 显示 {shown}/{total} × 清除",
829
+ "panel.emptyRunFilter": "该筛选下没有 run。",
830
+ "panel.runHint": "点一行看详情浮层;「去会话右栏」= 跳到该 run 的发起会话并在其右侧栏打开(与任务夹产物并排)",
831
+ "panel.boardHint": "点状态徽章可筛选(多选);终态卡片默认收起,筛选时自动显示",
832
+ "panel.runDetail": "run 详情",
833
+ "panel.goOwnerSession": "去发起会话",
834
+ "panel.goOwnerSessionTip": "跳到该 run 的发起会话,并在那个会话的右侧栏打开(与任务夹产物并排看)",
835
+ "panel.remoteProductsUnavailable": "remote.products 不可用(插件未挂载或版本过旧)",
836
+ "panel.hintNoAddress": "{label}:host 未生成可打开的地址(可能缺少会话上下文)",
837
+ "panel.hintRightbarFailed": "右侧栏打开失败(宿主只在对话视图挂载它):{label}——已在本面板内联显示",
838
+ "panel.hintSessionGone": "发起会话 {sid}… 不在会话列表里(可能已被清理)——已在本面板展示详情",
839
+ "panel.hintSwitchedNoRightbar": "已切到发起会话,但右栏没打开({label})——可在该会话里用「⇥ 右栏打开」重试",
840
+ "panel.hintInlineSuffix": "——已在本面板内联显示",
841
+ "runList.empty": "该产品线还没有 run 记录。",
842
+ "runList.running": "进行中",
843
+ "runList.noRequirement": "(无需求描述)",
844
+ "runList.stageProgress": "阶段 {done}/{total}",
845
+ "runList.openRightBar": "去会话右栏",
846
+ "runList.openRightBarTip": "跳到该 run 的发起会话,并在那个会话的右侧栏打开详情(右侧栏是会话级的:挂到无关会话上没有意义)",
847
+ "runList.callsSuffix": " · {n} 次",
848
+ "panelBoard.empty": "backlog 为空(该产品线还没有立项卡片)。",
849
+ "panelBoard.groupCount": "{kind} · {n}",
850
+ "panelBoard.activeCount": "活动 {n}",
851
+ "panelBoard.clearFilterTip": "清除本组筛选",
852
+ "panelBoard.collapseDone": "收起已完成 {n}",
853
+ "panelBoard.expandDone": "已完成 {n} ▸",
854
+ "panelBoard.collapseDoneTip": "收起已完成/已关闭卡片",
855
+ "panelBoard.expandDoneTip": "展开已完成/已关闭卡片",
856
+ "panelBoard.emptyFiltered": "该筛选下没有卡片。",
857
+ "panelBoard.filterChipOn": "点击取消该状态筛选",
858
+ "panelBoard.filterChipOff": "点击只看该状态(可多选)",
859
+ "panelItem.row.req": "需求",
860
+ "panelItem.row.owner": "负责人",
861
+ "panelItem.row.dev": "开发",
862
+ "panelItem.row.qa": "测试",
863
+ "panelItem.row.accept": "验收",
864
+ "panelItem.row.retries": "重试",
865
+ "panelItem.row.runDocs": "任务夹",
866
+ "panelItem.defectSection": "缺陷详情(QA 报告导入)",
867
+ "panelItem.row.defectId": "缺陷编号",
868
+ "panelItem.row.severity": "严重级",
869
+ "panelItem.row.module": "功能模块",
870
+ "panelItem.row.reproduce": "复现步骤",
871
+ "panelItem.row.expected": "期望行为",
872
+ "panelItem.row.actual": "实际行为",
873
+ "panelItem.row.defectAc": "关联验收项",
874
+ "panelItem.row.defectCheck": "检测命令",
875
+ "panelItem.row.defectCriterion": "通过判据",
876
+ "panelItem.noDefectDetail": "(该卡无缺陷描述:登记时 QA 报告只给了三要素,或为旧版本登记的卡;细节见关联 run 的 QA-REPORT.md)",
877
+ "panelItem.spec": "规格",
878
+ "panelItem.summary": "结论摘要",
879
+ "panelItem.artifacts": "任务夹产物 · {n}",
880
+ "panelItem.artifactTip": "{address}\n(跳到产物所属会话后在该会话右侧栏打开)",
881
+ "panelItem.noArtifacts": "该条目没有可预览的任务夹产物(或缺少会话上下文,无法生成文件地址)。",
882
+ "panelItem.subtasks": "子卡 · {n}",
883
+ "panelItem.bugs": "关联缺陷 · {n}",
884
+ "panelItem.events": "流转时间线 · 最近 {n} 条",
885
+ "detail.runMissing": "未找到该 run(可能已被清理,或地址已过期)。",
886
+ "detail.subagents": "子代理 {n}",
887
+ "detail.stages": "阶段 · {n}",
888
+ "detail.stageFailed": "阶段详情读取失败:{err}",
889
+ "detail.stageTitle": "阶段 #{seq} 详情",
890
+ "detail.output": "阶段产出",
891
+ "detail.attempts": "同任务尝试 · {n}",
892
+ "detail.logs": "日志 · 最近 {n} 条",
893
+ "detail.endedRunning": "进行中",
894
+ "detail.subagentChip": "子代理 {id}",
895
+ "tab.resolveFailed": "无法解析 run 地址:{address}",
896
+ "tab.readingAddress": "读取 tab 地址中…",
897
+ "tab.noHook": "宿主 tab 信息钩子不可用(useTabInfo 缺失)",
898
+ "tab.remoteUnavailable": "remote 不可用(插件未挂载或版本过旧)",
899
+ "tab.readingRun": "读取 run 详情中…"
900
+ };
901
+ /** 英文词典(key 集合必须与 zh 完全一致)。 */
902
+ const en = {
903
+ "common.close": "Close",
904
+ "common.loading": "Loading…",
905
+ "common.retry": "Retry",
906
+ "common.collapse": "Collapse",
907
+ "common.collapseFull": "Collapse",
908
+ "common.clickToExpand": "Click to expand",
909
+ "common.expandFull": "Show more",
910
+ "common.moreLines": "… +{n} lines",
911
+ "common.none": "none",
912
+ "common.noneDash": "(none)",
913
+ "common.noSummary": "(no summary)",
914
+ "common.failed": "Failed",
915
+ "common.running": "Running",
916
+ "common.needsHuman": "needs human intervention",
917
+ "common.unknownError": "unknown error",
918
+ "common.gotIt": "Got it",
919
+ "common.remoteNotReady": "remote not ready",
920
+ "common.remoteCallFailed": "{what} call failed: {detail}",
921
+ "common.remoteEmptyResult": "{what} returned an empty result (ok=true but no value; raw envelope={raw})",
922
+ "common.calls": "{n} calls",
923
+ "common.callsCount": "{n} calls",
924
+ "common.noUsage": "no token data",
925
+ "status.created": "Created",
926
+ "status.in-progress": "In progress",
927
+ "status.pending-acceptance": "Pending acceptance",
928
+ "status.accepted": "Accepted",
929
+ "status.closed": "Closed",
930
+ "status.pending": "Todo",
931
+ "status.running": "Developing",
932
+ "status.testable": "Ready for test",
933
+ "status.testing": "Testing",
934
+ "status.rework": "Rework",
935
+ "status.needs-human": "Needs human",
936
+ "status.cancelled": "Closed",
937
+ "status.open": "Unclaimed",
938
+ "status.claimed": "In progress",
939
+ "status.fixed": "Fixed (pending verify)",
940
+ "status.verified": "Closed",
941
+ "status.reopened": "Reopened",
942
+ "status.done": "Done",
943
+ "status.completed": "Completed",
944
+ "status.failed": "Failed",
945
+ "status.interrupted": "Interrupted",
946
+ "status.superseded": "Superseded",
947
+ "runStatus.pending": "Queued",
948
+ "runStatus.running": "Running",
949
+ "runStatus.completed": "Completed",
950
+ "runStatus.failed": "Failed",
951
+ "runStatus.cancelled": "Cancelled",
952
+ "runStatus.interrupted": "Interrupted",
953
+ "runStatus.superseded": "Superseded",
954
+ "phase.prd": "PRD",
955
+ "phase.design": "UI/UX design",
956
+ "phase.scaffold": "Architecture",
957
+ "phase.tech": "Technical design",
958
+ "phase.dev": "Development",
959
+ "phase.qa": "QA testing",
960
+ "phase.acceptance": "Acceptance",
961
+ "kind.req": "Requirements",
962
+ "kind.task": "Tasks",
963
+ "kind.bug": "Defects",
964
+ "role.pm": "Product",
965
+ "role.design": "Design",
966
+ "role.arch": "Architecture",
967
+ "role.tech": "Tech design",
968
+ "role.dev": "Dev",
969
+ "role.qa": "QA",
970
+ "role.acceptance": "Acceptance",
971
+ "role.other": "Other",
972
+ "roleChip.pm": "📌 Product",
973
+ "roleChip.design": "🎨 Design",
974
+ "roleChip.arch": "🏗 Architecture",
975
+ "roleChip.tech": "📐 Tech design",
976
+ "roleChip.dev": "👨‍💻 Dev",
977
+ "roleChip.qa": "🧪 QA",
978
+ "roleChip.acceptance": "✅ Acceptance",
979
+ "token.officialTitle": "TOKEN · official metering",
980
+ "token.usageLine": "Input (miss) {input} / Input (hit) {cacheRead} / Cache write {cacheWrite} / Output {output} · {calls} calls",
981
+ "token.usageHit": " · cache hit {hit}%",
982
+ "token.usageMissing": "no usage detail",
983
+ "token.inputMiss": "Input (miss) {v}",
984
+ "token.inputHit": "Input (hit) {v}",
985
+ "token.cacheWrite": "Cache write {v}",
986
+ "token.output": "Output {v}",
987
+ "token.total": "Total {v}",
988
+ "token.allStagesTip": "Input (miss) / Input (hit) / Output totals across all stages",
989
+ "token.stageLine": "Stage tokens: {v}",
990
+ "stage.cardTip": "{label} — click for stage detail",
991
+ "stage.retryTip": "Retried {n}× ({m} attempts in total)",
992
+ "stage.detailTitle": "Stage detail",
993
+ "stage.summaryFallback": "(Full text was not saved for this run — showing the summary)\n\n{summary}",
994
+ "stage.loadFailed": "⚠ Load failed: {err}",
995
+ "stage.noOutput": "(no output text)",
996
+ "stage.attemptHistory": "↻ Attempt history ({n} · click to inspect one)",
997
+ "stage.attemptDone": "✅ OK",
998
+ "stage.attemptFailed": "❌ {outcome}",
999
+ "stage.attemptRunning": "⏳ Running",
1000
+ "stage.childJumpBtn": "🎬 Open subagent session",
1001
+ "stage.childJumpTip": "Open this stage's subagent session (full reasoning and tool-call trail); then switch to the \"Chat\" tab",
1002
+ "stage.childNoneTip": "No subagent session is available for this stage",
1003
+ "stage.childCrossSessionTip": "This subagent was started by session {sid}; cross-session navigation is not supported yet — open the Team Workbench in its originating session",
1004
+ "stage.childCrossSessionNote": "Cross-session navigation is not supported yet: this subagent was started by session {sid}. Open the Team Workbench in its originating session to see the trail.",
1005
+ "stage.childJumpNote": "After jumping, switch to the \"Chat\" tab to see this subagent's full session trail",
1006
+ "stage.evidenceTitle": "🔬 Verification evidence",
1007
+ "stage.evidenceMissing": "(missing — the contract was not honored and the host logged a warning; cross-check with this run's command logs: logs/teamflow/<runId>/ while the run is live, archived by the host to $DSH_HOME/teamflow/<workspace>/logs/<runId>/ afterwards)",
1008
+ "stage.evidenceMissingShort": "(missing — the contract was not honored and the host logged a warning)",
1009
+ "stage.artifactsTitle": "📄 Stage artifacts",
1010
+ "pipeline.empty": "No active pipeline — ask the model to call teamflow_start, or type a requirement above",
1011
+ "pipeline.noNodes": "The pipeline has not started any stage node yet",
1012
+ "pipeline.zoomOut": "Zoom out",
1013
+ "pipeline.zoomIn": "Zoom in",
1014
+ "pipeline.fitCanvas": "Fit canvas",
1015
+ "pipeline.canvasHint": "✥ Drag to pan · scroll to zoom",
1016
+ "workbench.title": "Team Workbench",
1017
+ "workbench.running": "Pipeline running",
1018
+ "workbench.idle": "Idle",
1019
+ "workbench.refresh": "🔄 Refresh",
1020
+ "workbench.resumeTip": "Resume {id}\nCurrent status: {status}; completed stages are skipped and the run restarts from the first unfinished stage",
1021
+ "workbench.resuming": "Resuming…",
1022
+ "workbench.resumeBtn": "↻ Resume from #{id}",
1023
+ "workbench.loadFailed": "⚠ {err} (make sure dsh-plugin-teamflow is installed and the web host was restarted)",
1024
+ "workbench.needsHumanBanner": "⚠ {n} item(s) need human intervention",
1025
+ "workbench.handle": "Handle {id}",
1026
+ "workbench.manualReason": "manual handling",
1027
+ "workbench.tabPipeline": "🔄 Pipeline",
1028
+ "workbench.tabBoard": "📋 Backlog board",
1029
+ "workbench.workspaceTip": "Current workspace (workspace-level isolation): {path}",
1030
+ "workbench.noWorkspace": "no workspace connected",
1031
+ "workbench.history": "History",
1032
+ "workbench.openRightBarTip": "Open this run's detail in the right sidebar (side by side with task-folder artifacts)",
1033
+ "workbench.openRightBarBtn": "⇥ Open in right bar",
1034
+ "board.empty": "Backlog is empty (no pipeline has run yet)",
1035
+ "board.dragReason": "kanban drag transition",
1036
+ "board.cardTip": "{id} · {status}{summary} (click for details)",
1037
+ "board.assignDevTip": "dev assignee\n{who}",
1038
+ "board.assignQaTip": "qa assignee\n{who}",
1039
+ "board.acceptTip": "acceptance/report owner\n{who}",
1040
+ "board.subtaskCount": "📦 {n} subtasks",
1041
+ "board.retries": "retries {n}",
1042
+ "item.overview": "Overview",
1043
+ "item.acceptRow": "✅ Acceptance",
1044
+ "item.retryRow": "↻ Retries",
1045
+ "item.humanRow": "⚠ Human intervention",
1046
+ "item.updatedAt": "Updated",
1047
+ "item.runSection": "Run",
1048
+ "item.runBtn": "▶ Pipeline",
1049
+ "item.jumpRunTip": "Open this requirement's pipeline view #{id}",
1050
+ "item.requirement": "Raw requirement",
1051
+ "item.noRequirement": "(no raw text)",
1052
+ "item.runDocs": "Task folder",
1053
+ "item.previewTip": "Preview {path} in the right sidebar",
1054
+ "item.subtasks": "Linked subtasks ({n})",
1055
+ "item.bugs": "Linked defects ({n})",
1056
+ "item.timeline": "Transition timeline ({n})",
1057
+ "team.currentTip": "Current team: {name} (click to switch)",
1058
+ "team.pick": "Select team",
1059
+ "team.label": "Team",
1060
+ "team.none": "No team (chat directly)",
1061
+ "team.noneNote": "teamflow is not used; the model works directly",
1062
+ "panel.title": "🏭 Team Workbench",
1063
+ "panel.subtitle": "Global panel · by product line",
1064
+ "panel.currentSession": "Session {sid}",
1065
+ "panel.noSession": "No active session",
1066
+ "panel.reload": "Reload data",
1067
+ "panel.backToChat": "Back to chat",
1068
+ "panel.backToChatTip": "Back to chat (click the sidebar icon again to return to this panel)",
1069
+ "panel.railTitle": "Product lines · {n}",
1070
+ "panel.rescanTip": "Rescan $DSH_HOME/teamflow",
1071
+ "panel.refreshing": "Refreshing",
1072
+ "panel.refresh": "Refresh",
1073
+ "panel.noProducts": "No product lines yet. Once a pipeline has run in a workspace, its product line shows up here ($DSH_HOME/teamflow/<key>).",
1074
+ "panel.activeRuns": "{n} running",
1075
+ "panel.updated": "updated {time}",
1076
+ "panel.reading": "Loading…",
1077
+ "panel.verdict": "acceptance {v}",
1078
+ "panel.pickProduct": "Pick a product line on the left to see its backlog and runs (the most recently updated one is selected on first entry).",
1079
+ "panel.loadingView": "Loading product-line data…",
1080
+ "panel.runChip": "runs {n}",
1081
+ "panel.activeChip": "active {n}",
1082
+ "panel.verdictChip": "acceptance {v}",
1083
+ "panel.tabRuns": "🚀 Pipeline runs · {n}",
1084
+ "panel.tabBacklog": "📋 Backlog · {n}",
1085
+ "panel.pinnedActive": "{n} running pinned",
1086
+ "panel.showRecent": "Show latest {n}",
1087
+ "panel.showAll": "Show all {n}",
1088
+ "panel.clearRunFilterTip": "Clear the run status filter",
1089
+ "panel.filtered": "Filtered {sel} · showing {shown}/{total} × clear",
1090
+ "panel.emptyRunFilter": "No runs match this filter.",
1091
+ "panel.runHint": "Click a row for the detail overlay; \"Open in session right bar\" jumps to the run's originating session and opens the right bar there (side by side with task-folder artifacts)",
1092
+ "panel.boardHint": "Click a status badge to filter (multi-select); terminal cards are collapsed by default and appear automatically while filtering",
1093
+ "panel.runDetail": "Run detail",
1094
+ "panel.goOwnerSession": "Go to originating session",
1095
+ "panel.goOwnerSessionTip": "Jump to the run's originating session and open its right bar there (side by side with task-folder artifacts)",
1096
+ "panel.remoteProductsUnavailable": "remote.products unavailable (plugin not mounted or outdated)",
1097
+ "panel.hintNoAddress": "{label}: the host produced no openable address (session context may be missing)",
1098
+ "panel.hintRightbarFailed": "Failed to open the right sidebar (the host mounts it only in the chat view): {label} — shown inline in this panel instead",
1099
+ "panel.hintSessionGone": "Originating session {sid}… is not in the session list (it may have been cleaned up) — showing the detail in this panel",
1100
+ "panel.hintSwitchedNoRightbar": "Switched to the originating session but the right bar did not open ({label}) — retry with \"⇥ Open in right bar\" in that session",
1101
+ "panel.hintInlineSuffix": " — shown inline in this panel instead",
1102
+ "runList.empty": "No runs recorded for this product line yet.",
1103
+ "runList.running": "Running",
1104
+ "runList.noRequirement": "(no requirement text)",
1105
+ "runList.stageProgress": "Stages {done}/{total}",
1106
+ "runList.openRightBar": "Open in session right bar",
1107
+ "runList.openRightBarTip": "Jump to the run's originating session and open the detail in that session's right bar (the right bar is session-scoped: attaching it to an unrelated session is meaningless)",
1108
+ "runList.callsSuffix": " · {n} calls",
1109
+ "panelBoard.empty": "Backlog is empty (this product line has no cards yet).",
1110
+ "panelBoard.groupCount": "{kind} · {n}",
1111
+ "panelBoard.activeCount": "Active {n}",
1112
+ "panelBoard.clearFilterTip": "Clear this group's filter",
1113
+ "panelBoard.collapseDone": "Hide {n} done",
1114
+ "panelBoard.expandDone": "{n} done ▸",
1115
+ "panelBoard.collapseDoneTip": "Hide done/closed cards",
1116
+ "panelBoard.expandDoneTip": "Show done/closed cards",
1117
+ "panelBoard.emptyFiltered": "No cards match this filter.",
1118
+ "panelBoard.filterChipOn": "Click to clear this status filter",
1119
+ "panelBoard.filterChipOff": "Click to filter by this status only (multi-select)",
1120
+ "panelItem.row.req": "Requirement",
1121
+ "panelItem.row.owner": "Owner",
1122
+ "panelItem.row.dev": "Dev",
1123
+ "panelItem.row.qa": "QA",
1124
+ "panelItem.row.accept": "Acceptance",
1125
+ "panelItem.row.retries": "Retries",
1126
+ "panelItem.row.runDocs": "Task folder",
1127
+ "panelItem.defectSection": "Defect detail (imported from the QA report)",
1128
+ "panelItem.row.defectId": "Defect id",
1129
+ "panelItem.row.severity": "Severity",
1130
+ "panelItem.row.module": "Module",
1131
+ "panelItem.row.reproduce": "Steps to reproduce",
1132
+ "panelItem.row.expected": "Expected",
1133
+ "panelItem.row.actual": "Actual",
1134
+ "panelItem.row.defectAc": "Related AC",
1135
+ "panelItem.row.defectCheck": "Check command",
1136
+ "panelItem.row.defectCriterion": "Pass criterion",
1137
+ "panelItem.noDefectDetail": "(No defect description on this card: the QA report only carried the three key fields when it was registered, or the card predates this version; see QA-REPORT.md in the linked run)",
1138
+ "panelItem.spec": "Spec",
1139
+ "panelItem.summary": "Summary",
1140
+ "panelItem.artifacts": "Task-folder artifacts · {n}",
1141
+ "panelItem.artifactTip": "{address}\n(opens in the right sidebar of the session that owns this artifact)",
1142
+ "panelItem.noArtifacts": "This item has no previewable task-folder artifacts (or no session context to build a file address).",
1143
+ "panelItem.subtasks": "Subtasks · {n}",
1144
+ "panelItem.bugs": "Linked defects · {n}",
1145
+ "panelItem.events": "Transition timeline · last {n}",
1146
+ "detail.runMissing": "Run not found (it may have been cleaned up, or the address is stale).",
1147
+ "detail.subagents": "Subagents {n}",
1148
+ "detail.stages": "Stages · {n}",
1149
+ "detail.stageFailed": "Failed to load stage detail: {err}",
1150
+ "detail.stageTitle": "Stage #{seq} detail",
1151
+ "detail.output": "Stage output",
1152
+ "detail.attempts": "Attempts for this task · {n}",
1153
+ "detail.logs": "Logs · last {n}",
1154
+ "detail.endedRunning": "running",
1155
+ "detail.subagentChip": "Subagent {id}",
1156
+ "tab.resolveFailed": "Cannot parse the run address: {address}",
1157
+ "tab.readingAddress": "Reading the tab address…",
1158
+ "tab.noHook": "Host tab-info hook unavailable (useTabInfo missing)",
1159
+ "tab.remoteUnavailable": "remote unavailable (plugin not mounted or outdated)",
1160
+ "tab.readingRun": "Loading run detail…"
1161
+ };
1162
+ //#endregion
1163
+ //#region client/panel.tsx
1164
+ /**
1165
+ * dsh-plugin-teamflow — 全局面板 + 右栏 run 详情 tab(v0.1.8 ①)。
1166
+ *
1167
+ * 与 conversation.view tab(会话内工作台)的分工:
1168
+ * - 全局面板挂在**应用外壳**的 root slot 上:`sidebar.panellist`(侧边栏图标)+
1169
+ * `main`(中央主区,keyed,**必须与 panellist 同 id**——宿主 `layout.selectPanel`
1170
+ * 对未注册的 main key 直接抛错)。它没有会话上下文,因此数据面按**产品线 key**
1171
+ * 寻址(`remote.products/productView/...`),跨会话可用。
1172
+ * - 右栏 run 详情 tab:tab 类型进 `ctx.sidebarRightTabs`,正文进 keyed seat
1173
+ * `sidebar.right.pane.tab`;地址由 host 生成(`dsh-resource://teamflow/run/<产品线>/<runId>`),
1174
+ * 与产物预览同一条原则——client 不拼地址。右侧栏是 AppFrame 级 root slot,
1175
+ * 全局面板占着中央区时它依然挂载(唯一失败态是"没有挂载会话",此时降级为面板内联详情)。
1176
+ */
1177
+ const RUN_TAB_ID = "dsh-plugin-teamflow/run";
1178
+ const RUN_TAB_KIND = "teamflow-run";
1179
+ const RUN_TAB_PATTERN = "dsh-resource://teamflow/run/**";
1180
+ /** 解析 host 生成的 run 地址 → { product, runId }(不合法 → null)。 */
1181
+ function parseRunAddress(address) {
1182
+ const m = /^dsh-resource:\/\/teamflow\/run\/([^/]+)\/([^/?#]+)$/.exec(String(address || ""));
1183
+ if (!m) return null;
1184
+ try {
1185
+ return {
1186
+ product: decodeURIComponent(m[1]),
1187
+ runId: decodeURIComponent(m[2])
1188
+ };
1189
+ } catch (e) {
1190
+ return null;
1191
+ }
1192
+ }
1193
+ /** 右栏 tab 类型定义(类型本身;正文在 sidebar.right.pane.tab 的 keyed seat)。 */
1194
+ function runTabDefinition() {
1195
+ return {
1196
+ id: RUN_TAB_ID,
1197
+ kind: RUN_TAB_KIND,
1198
+ priority: "extension",
1199
+ patterns: [RUN_TAB_PATTERN],
1200
+ title: (address) => {
1201
+ const p = parseRunAddress(address);
1202
+ return p ? `TeamFlow · ${p.runId.slice(0, 14)}` : "TeamFlow Run";
1203
+ }
1204
+ };
1205
+ }
1206
+ /** Typert remote 信封解包(与会话内工作台同一约定)。
1207
+ * 空信封(`{ok:true}` 但没有 `value`)是**真实故障态**:host 方法返回了 `undefined` / 结果被丢弃——
1208
+ * 此时不能静默返回 undefined(调用方会把它再包成「未知错误」,丢掉定位信息),故显式报出原始信封。 */
1209
+ function unwrap$1(res, what) {
1210
+ if (!res || !res.ok) {
1211
+ let detail = t("common.unknownError");
1212
+ try {
1213
+ detail = res && res.error && (res.error.message || res.error.code) || JSON.stringify(res) || t("common.unknownError");
1214
+ } catch (e) {}
1215
+ try {
1216
+ console.warn("[teamflow] remote 调用失败", what, res);
1217
+ } catch (e) {}
1218
+ throw new Error(t("common.remoteCallFailed", {
1219
+ what: what || "remote",
1220
+ detail
1221
+ }));
1222
+ }
1223
+ if (res.value === void 0) {
1224
+ try {
1225
+ console.warn("[teamflow] remote 返回空信封(ok 但无 value)", what, res);
1226
+ } catch (e) {}
1227
+ let raw = "";
1228
+ try {
1229
+ raw = JSON.stringify(res);
1230
+ } catch (e) {
1231
+ raw = String(res);
1232
+ }
1233
+ throw new Error(t("common.remoteEmptyResult", {
1234
+ what: what || "remote",
1235
+ raw
1236
+ }));
1237
+ }
1238
+ return res.value;
1239
+ }
1240
+ /** 全局面板图标:三条泳道 + 节点(与工作台「流水线」语义一致),跟随选中态配色。 */
1241
+ function TeamflowPanelIcon({ size = 16, active = false }) {
1242
+ return h("svg", {
1243
+ width: size,
1244
+ height: size,
1245
+ viewBox: "0 0 24 24",
1246
+ fill: "none",
1247
+ stroke: active ? "currentColor" : "currentColor",
1248
+ strokeWidth: 1.6,
1249
+ strokeLinecap: "round",
1250
+ strokeLinejoin: "round",
1251
+ "aria-hidden": "true",
1252
+ focusable: "false"
1253
+ }, h("rect", {
1254
+ x: 3,
1255
+ y: 4,
1256
+ width: 5.2,
1257
+ height: 4,
1258
+ rx: 1.2,
1259
+ opacity: active ? 1 : .85
1260
+ }), h("rect", {
1261
+ x: 9.4,
1262
+ y: 10,
1263
+ width: 5.2,
1264
+ height: 4,
1265
+ rx: 1.2,
1266
+ opacity: active ? 1 : .85
1267
+ }), h("rect", {
1268
+ x: 3,
1269
+ y: 16,
1270
+ width: 5.2,
1271
+ height: 4,
1272
+ rx: 1.2,
1273
+ opacity: active ? 1 : .85
1274
+ }), 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" }));
1275
+ }
1276
+ const panelBtn = {
1277
+ font: "inherit",
1278
+ fontSize: 11,
1279
+ fontWeight: 600,
1280
+ padding: "3px 9px",
1281
+ borderRadius: 7,
1282
+ cursor: "pointer",
1283
+ border: `1px solid ${T.border}`,
1284
+ background: T.layer1,
1285
+ color: T.text,
1286
+ whiteSpace: "nowrap",
1287
+ lineHeight: "16px"
1288
+ };
1289
+ const brandBtn = {
1290
+ ...panelBtn,
1291
+ border: `1px solid color-mix(in srgb, ${T.brand} 40%, transparent)`,
1292
+ background: `color-mix(in srgb, ${T.brand} 12%, transparent)`,
1293
+ color: T.brand
1294
+ };
1295
+ const sectionTitle = (text, extra) => h("div", { style: {
1296
+ ...flexRow,
1297
+ justifyContent: "space-between",
1298
+ margin: "14px 0 8px"
1299
+ } }, h("div", { style: {
1300
+ fontSize: 12,
1301
+ fontWeight: 700,
1302
+ color: T.text,
1303
+ letterSpacing: ".02em"
1304
+ } }, text), extra || null);
1305
+ const muted = (text, style = {}) => h("div", { style: {
1306
+ fontSize: 11,
1307
+ color: T.text2,
1308
+ lineHeight: 1.6,
1309
+ ...style
1310
+ } }, text);
1311
+ const runUsageText = (u) => {
1312
+ if (!u || !(u.input || u.cacheRead || u.cacheWrite || u.output)) return t("common.noUsage");
1313
+ const hit = hitRate(u);
1314
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""} · ${t("common.calls", { n: u.calls })}`;
1315
+ };
1316
+ const RUN_PREVIEW = 8;
1317
+ const TERMINAL_STATUSES = [
1318
+ "accepted",
1319
+ "closed",
1320
+ "verified",
1321
+ "cancelled"
1322
+ ];
1323
+ function ProductRail({ products, current, loadingKey, onSelect, onRefresh, busy }) {
1324
+ return h("div", { style: {
1325
+ width: 236,
1326
+ flex: "0 0 236px",
1327
+ minHeight: 0,
1328
+ display: "flex",
1329
+ flexDirection: "column",
1330
+ borderRight: `1px solid ${T.border}`,
1331
+ background: `color-mix(in srgb, ${T.layer1} 40%, transparent)`
1332
+ } }, h("div", { style: {
1333
+ ...flexRow,
1334
+ justifyContent: "space-between",
1335
+ padding: "10px 12px 8px"
1336
+ } }, h("span", { style: {
1337
+ fontSize: 11.5,
1338
+ fontWeight: 700,
1339
+ color: T.text2
1340
+ } }, t("panel.railTitle", { n: products.length })), h("button", {
1341
+ style: panelBtn,
1342
+ onClick: onRefresh,
1343
+ disabled: busy,
1344
+ title: t("panel.rescanTip")
1345
+ }, busy ? t("panel.refreshing") : t("panel.refresh"))), h("div", { style: {
1346
+ flex: 1,
1347
+ minHeight: 0,
1348
+ overflowY: "auto",
1349
+ padding: "0 8px 12px"
1350
+ } }, products.length === 0 ? muted(t("panel.noProducts"), { padding: "10px 4px" }) : products.map((p) => {
1351
+ const on = p.key === current;
1352
+ return h("div", {
1353
+ key: p.key,
1354
+ onClick: () => onSelect(p.key),
1355
+ title: p.path || p.key,
1356
+ style: {
1357
+ padding: "8px 10px",
1358
+ marginBottom: 6,
1359
+ borderRadius: 9,
1360
+ cursor: "pointer",
1361
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
1362
+ background: on ? `color-mix(in srgb, ${T.brand} 10%, ${T.layer1})` : "transparent"
1363
+ }
1364
+ }, h("div", { style: {
1365
+ ...flexRow,
1366
+ justifyContent: "space-between"
1367
+ } }, h("span", { style: {
1368
+ fontSize: 12,
1369
+ fontWeight: 600,
1370
+ color: T.text,
1371
+ overflow: "hidden",
1372
+ textOverflow: "ellipsis",
1373
+ whiteSpace: "nowrap"
1374
+ } }, p.title || p.key), p.activeRuns > 0 ? chip(t("panel.activeRuns", { n: p.activeRuns }), T.brand, { dot: true }) : null), h("div", { style: {
1375
+ fontSize: 10,
1376
+ color: T.text2,
1377
+ fontFamily: MONO,
1378
+ marginTop: 2,
1379
+ overflow: "hidden",
1380
+ textOverflow: "ellipsis",
1381
+ whiteSpace: "nowrap"
1382
+ } }, p.key), h("div", { style: {
1383
+ ...flexRow,
1384
+ gap: 8,
1385
+ marginTop: 3,
1386
+ fontSize: 10,
1387
+ color: T.text2
1388
+ } }, h("span", null, `run ${p.totalRuns}`), p.updatedAt ? h("span", null, t("panel.updated", { time: fmtTime(p.updatedAt) })) : null, p.key === loadingKey ? h("span", { style: {
1389
+ color: T.brand,
1390
+ fontWeight: 600
1391
+ } }, t("panel.reading")) : null), p.lastRequirement ? h("div", { style: {
1392
+ fontSize: 10.5,
1393
+ color: T.text2,
1394
+ marginTop: 3,
1395
+ lineHeight: 1.45,
1396
+ display: "-webkit-box",
1397
+ WebkitLineClamp: 2,
1398
+ WebkitBoxOrient: "vertical",
1399
+ overflow: "hidden"
1400
+ } }, p.lastRequirement) : null, p.lastVerdict ? h("div", { style: { marginTop: 4 } }, chip(t("panel.verdict", { v: p.lastVerdict }), stColor(p.lastVerdict === "accepted" ? "accepted" : p.lastVerdict))) : null);
1401
+ })));
1402
+ }
1403
+ function RunList({ runs, activeRunId, onOpenRun, onInlineRun }) {
1404
+ if (!runs.length) return muted(t("runList.empty"), { padding: "2px 2px 8px" });
1405
+ return h("div", { style: {
1406
+ display: "flex",
1407
+ flexDirection: "column",
1408
+ gap: 6
1409
+ } }, runs.map((r) => {
1410
+ const on = r.id === activeRunId;
1411
+ const active = r.status === "running" || r.status === "pending";
1412
+ return h("div", {
1413
+ key: r.id,
1414
+ style: {
1415
+ padding: "8px 11px",
1416
+ borderRadius: 9,
1417
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
1418
+ borderLeft: `3px solid ${stColor(r.status)}`,
1419
+ background: on ? `color-mix(in srgb, ${T.brand} 8%, ${T.layer1})` : T.layer1,
1420
+ display: "flex",
1421
+ alignItems: "center",
1422
+ gap: 10,
1423
+ cursor: "pointer"
1424
+ },
1425
+ onClick: () => onInlineRun(r)
1426
+ }, h("div", { style: {
1427
+ flex: 1,
1428
+ minWidth: 0
1429
+ } }, h("div", { style: {
1430
+ ...flexRow,
1431
+ gap: 6
1432
+ } }, chip(runStatusText(r.status), stColor(r.status), { dot: true }), r.mode ? chip(String(r.mode), T.text2) : null, h("span", { style: {
1433
+ fontFamily: MONO,
1434
+ fontSize: 10,
1435
+ color: T.text2
1436
+ } }, r.id), active ? h("span", { style: {
1437
+ fontSize: 10,
1438
+ color: T.brand
1439
+ } }, t("runList.running")) : null), h("div", { style: {
1440
+ fontSize: 11.5,
1441
+ color: T.text,
1442
+ marginTop: 3,
1443
+ overflow: "hidden",
1444
+ textOverflow: "ellipsis",
1445
+ whiteSpace: "nowrap"
1446
+ } }, r.requirement || t("runList.noRequirement")), h("div", { style: {
1447
+ ...flexRow,
1448
+ gap: 10,
1449
+ marginTop: 3,
1450
+ fontSize: 10,
1451
+ color: T.text2,
1452
+ fontFamily: MONO
1453
+ } }, h("span", null, t("runList.stageProgress", {
1454
+ done: r.doneStages,
1455
+ total: r.stageCount
1456
+ })), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), h("button", {
1457
+ style: brandBtn,
1458
+ title: t("runList.openRightBarTip"),
1459
+ onClick: (e) => {
1460
+ e.stopPropagation();
1461
+ onOpenRun(r);
1462
+ }
1463
+ }, t("runList.openRightBar")));
1464
+ }));
1465
+ }
1466
+ function BacklogCard({ kind, item, onOpen }) {
1467
+ const color = stColor(item.status);
1468
+ return h("div", {
1469
+ onClick: () => onOpen(kind, item.id),
1470
+ style: {
1471
+ padding: "6px 9px",
1472
+ borderRadius: 8,
1473
+ border: `1px solid ${T.border}`,
1474
+ borderLeft: `3px solid ${color}`,
1475
+ background: T.layer1,
1476
+ cursor: "pointer",
1477
+ minWidth: 0,
1478
+ boxSizing: "border-box",
1479
+ maxWidth: "100%",
1480
+ overflow: "hidden"
1481
+ }
1482
+ }, h("div", { style: {
1483
+ ...flexRow,
1484
+ gap: 5,
1485
+ minWidth: 0
1486
+ } }, h("span", { style: {
1487
+ fontFamily: MONO,
1488
+ fontSize: 10,
1489
+ color: T.text2,
1490
+ minWidth: 0,
1491
+ overflow: "hidden",
1492
+ textOverflow: "ellipsis",
1493
+ whiteSpace: "nowrap"
1494
+ } }, item.id), chip(stText(item.status), color, { dot: true }), kind === "bug" && item.severity ? chip(String(item.severity), stColor("rework")) : null), h("div", { style: {
1495
+ fontSize: 11.5,
1496
+ color: T.text,
1497
+ marginTop: 3,
1498
+ overflow: "hidden",
1499
+ textOverflow: "ellipsis",
1500
+ whiteSpace: "nowrap"
1501
+ } }, item.title || item.id), (() => {
1502
+ const roles = kind === "task" ? byRoleLine(item) : "";
1503
+ return roles ? h("div", {
1504
+ title: roles,
1505
+ style: {
1506
+ fontSize: 10,
1507
+ color: T.text2,
1508
+ fontFamily: MONO,
1509
+ marginTop: 3,
1510
+ minWidth: 0,
1511
+ overflowWrap: "anywhere",
1512
+ wordBreak: "break-word"
1513
+ }
1514
+ }, roles) : null;
1515
+ })(), h("div", { style: {
1516
+ ...flexRow,
1517
+ gap: 6,
1518
+ marginTop: 3,
1519
+ fontSize: 10,
1520
+ color: T.text2
1521
+ } }, item.devAssign ? h("span", null, `dev ${item.devAssign}`) : null, item.qaAssign ? h("span", null, `qa ${item.qaAssign}`) : null, item.acceptBy ? h("span", null, t("panel.verdict", { v: item.acceptBy })) : null, item.retries ? h("span", { style: { color: T.warn } }, t("board.retries", { n: item.retries })) : null, item.humanIntervention ? h("span", { style: { color: T.error } }, t("status.needs-human")) : null));
1522
+ }
1523
+ /** 可点筛选徽章(多选):单击切换该状态,选中态用状态色实心;再点取消。 */
1524
+ const filterChip = (text, color, on, onToggle) => h("button", {
1525
+ onClick: onToggle,
1526
+ title: on ? t("panelBoard.filterChipOn") : t("panelBoard.filterChipOff"),
1527
+ style: {
1528
+ font: "inherit",
1529
+ fontSize: 11,
1530
+ fontWeight: 600,
1531
+ padding: "1px 8px",
1532
+ borderRadius: 999,
1533
+ cursor: "pointer",
1534
+ lineHeight: "16px",
1535
+ whiteSpace: "nowrap",
1536
+ border: `1px solid ${on ? `color-mix(in srgb, ${color} 55%, transparent)` : T.border}`,
1537
+ background: on ? `color-mix(in srgb, ${color} 26%, transparent)` : "transparent",
1538
+ color: on ? color : T.text2
1539
+ }
1540
+ }, text);
1541
+ /** run 状态展示顺序(可行动的在前)。 */
1542
+ const RUN_STATUS_ORDER = [
1543
+ "running",
1544
+ "pending",
1545
+ "interrupted",
1546
+ "failed",
1547
+ "cancelled",
1548
+ "completed",
1549
+ "superseded"
1550
+ ];
1551
+ function BacklogGroups({ backlog, onOpen, productKey }) {
1552
+ const [showDone, setShowDone] = react.default.useState({});
1553
+ const [filters, setFilters] = react.default.useState({});
1554
+ react.default.useEffect(() => {
1555
+ setShowDone({});
1556
+ setFilters({});
1557
+ }, [productKey]);
1558
+ if (!backlog) return null;
1559
+ const groups = [
1560
+ ["req", backlog.requirements || []],
1561
+ ["task", (backlog.tasks || []).filter((t) => t.type !== "subtask")],
1562
+ ["bug", backlog.bugs || []]
1563
+ ];
1564
+ if (!groups.reduce((a, [, arr]) => a + arr.length, 0)) return muted(t("panelBoard.empty"), { padding: "2px 2px 8px" });
1565
+ return h("div", null, groups.map(([kind, list]) => {
1566
+ if (!list.length) return null;
1567
+ const done = list.filter((it) => TERMINAL_STATUSES.indexOf(it.status) !== -1 && !it.humanIntervention);
1568
+ const active = list.filter((it) => done.indexOf(it) === -1);
1569
+ const open = showDone[kind] === true;
1570
+ const sel = filters[kind] || [];
1571
+ const selSet = new Set(sel);
1572
+ const filtering = sel.length > 0;
1573
+ const matched = filtering ? list.filter((it) => selSet.has(it.status)) : list;
1574
+ const byStatus = {};
1575
+ for (const it of list) byStatus[it.status] = (byStatus[it.status] || 0) + 1;
1576
+ const toggle = (st) => setFilters((m) => {
1577
+ const cur = new Set(m[kind] || []);
1578
+ if (cur.has(st)) cur.delete(st);
1579
+ else cur.add(st);
1580
+ return {
1581
+ ...m,
1582
+ [kind]: [...cur]
1583
+ };
1584
+ });
1585
+ const visible = filtering ? matched : open ? list : active;
1586
+ return h("div", {
1587
+ key: kind,
1588
+ style: { marginBottom: 12 }
1589
+ }, h("div", { style: {
1590
+ ...flexRow,
1591
+ gap: 6,
1592
+ marginBottom: 6,
1593
+ minWidth: 0
1594
+ } }, h("span", { style: {
1595
+ fontSize: 11.5,
1596
+ fontWeight: 700,
1597
+ color: T.text,
1598
+ flex: "0 0 auto"
1599
+ } }, t("panelBoard.groupCount", {
1600
+ kind: kindTitle(kind),
1601
+ n: list.length
1602
+ })), h("span", { style: {
1603
+ fontSize: 10,
1604
+ color: T.text2,
1605
+ flex: "0 0 auto"
1606
+ } }, t("panelBoard.activeCount", { n: active.length })), ...Object.keys(byStatus).map((s) => filterChip(`${stText(s)} ${byStatus[s]}`, stColor(s), selSet.has(s), () => toggle(s))), filtering ? h("button", {
1607
+ style: {
1608
+ ...panelBtn,
1609
+ marginLeft: "auto",
1610
+ flex: "0 0 auto"
1611
+ },
1612
+ title: t("panelBoard.clearFilterTip"),
1613
+ onClick: () => setFilters((m) => ({
1614
+ ...m,
1615
+ [kind]: []
1616
+ }))
1617
+ }, t("panel.filtered", {
1618
+ sel: sel.length,
1619
+ shown: matched.length,
1620
+ total: list.length
1621
+ })) : done.length ? h("button", {
1622
+ style: {
1623
+ ...panelBtn,
1624
+ marginLeft: "auto",
1625
+ flex: "0 0 auto"
1626
+ },
1627
+ title: open ? t("panelBoard.collapseDoneTip") : t("panelBoard.expandDoneTip"),
1628
+ onClick: () => setShowDone((m) => ({
1629
+ ...m,
1630
+ [kind]: !open
1631
+ }))
1632
+ }, open ? t("panelBoard.collapseDone", { n: done.length }) : t("panelBoard.expandDone", { n: done.length })) : null), visible.length ? h("div", { style: {
1633
+ display: "grid",
1634
+ gridTemplateColumns: "repeat(auto-fill, minmax(228px, 1fr))",
1635
+ gap: 6
1636
+ } }, visible.map((it) => h(BacklogCard, {
1637
+ key: it.id,
1638
+ kind,
1639
+ item: it,
1640
+ onOpen
1641
+ }))) : muted(t("panelBoard.emptyFiltered"), { fontSize: 10.5 }));
1642
+ }));
1643
+ }
1644
+ function ItemDetailPane({ det, openArtifact, onClose }) {
1645
+ if (!det) return null;
1646
+ const row = (k, v) => v === null || v === void 0 || v === "" ? null : h("div", { style: {
1647
+ ...flexRow,
1648
+ gap: 6,
1649
+ fontSize: 11
1650
+ } }, h("span", { style: {
1651
+ color: T.text2,
1652
+ flex: "0 0 auto"
1653
+ } }, k), h("span", { style: {
1654
+ color: T.text,
1655
+ minWidth: 0,
1656
+ wordBreak: "break-word"
1657
+ } }, String(v)));
1658
+ return h("div", { style: {
1659
+ padding: "10px 12px",
1660
+ display: "flex",
1661
+ flexDirection: "column",
1662
+ gap: 8,
1663
+ minHeight: 0
1664
+ } }, h("div", { style: {
1665
+ ...flexRow,
1666
+ justifyContent: "space-between"
1667
+ } }, h("div", { style: {
1668
+ ...flexRow,
1669
+ gap: 6
1670
+ } }, h("span", { style: {
1671
+ fontFamily: MONO,
1672
+ fontSize: 11,
1673
+ color: T.text2
1674
+ } }, det.id), chip(stText(det.status), stColor(det.status), { dot: true }), h("span", { style: {
1675
+ fontSize: 10.5,
1676
+ color: T.text2
1677
+ } }, kindTitle(det.kind))), h("button", {
1678
+ style: panelBtn,
1679
+ onClick: onClose
1680
+ }, t("common.close"))), h("div", { style: {
1681
+ fontSize: 13,
1682
+ fontWeight: 600,
1683
+ color: T.text,
1684
+ lineHeight: 1.45
1685
+ } }, det.title), det.kind === "bug" ? h("div", { style: {
1686
+ display: "flex",
1687
+ flexDirection: "column",
1688
+ gap: 4,
1689
+ padding: "8px 10px",
1690
+ borderRadius: 8,
1691
+ background: T.layer2,
1692
+ border: `1px solid ${T.border}`
1693
+ } }, h("div", { style: {
1694
+ ...flexRow,
1695
+ gap: 6
1696
+ } }, h("span", { style: {
1697
+ fontSize: 11,
1698
+ fontWeight: 700,
1699
+ color: T.text
1700
+ } }, t("panelItem.defectSection")), det.severity ? chip(String(det.severity), String(det.severity) === "P0" ? T.error : String(det.severity) === "P1" ? T.warn : T.text2) : null), row(t("panelItem.row.defectId"), det.defectId), row(t("panelItem.row.module"), det.module), row(t("panelItem.row.reproduce"), det.reproduce), row(t("panelItem.row.expected"), det.expected), row(t("panelItem.row.actual"), det.actual), row(t("panelItem.row.defectCheck"), det.defectCheck), row(t("panelItem.row.defectCriterion"), det.defectCriterion), row(t("panelItem.row.defectAc"), det.defectAc), det.reproduce || det.expected || det.actual ? null : h("div", { style: {
1701
+ fontSize: 10.5,
1702
+ color: T.warn,
1703
+ lineHeight: 1.5
1704
+ } }, t("panelItem.noDefectDetail"))) : null, h("div", { style: {
1705
+ display: "flex",
1706
+ flexDirection: "column",
1707
+ gap: 3
1708
+ } }, row(t("panelItem.row.req"), det.reqId), row(t("panelItem.row.owner"), det.owner), row(t("panelItem.row.dev"), det.devAssign), row(t("panelItem.row.qa"), det.qaAssign), row(t("panelItem.row.accept"), det.assignBy), row(t("panelItem.row.retries"), det.retries), row(t("panelItem.row.runDocs"), det.runDocs)), det.spec ? h("div", null, h("div", { style: {
1709
+ fontSize: 11,
1710
+ color: T.text2,
1711
+ marginBottom: 3
1712
+ } }, t("panelItem.spec")), h(FoldableText, { text: det.spec })) : null, det.summary ? h("div", null, h("div", { style: {
1713
+ fontSize: 11,
1714
+ color: T.text2,
1715
+ marginBottom: 3
1716
+ } }, t("panelItem.summary")), h(FoldableText, { text: det.summary })) : null, det.artifacts && det.artifacts.length ? h("div", null, h("div", { style: {
1717
+ fontSize: 11,
1718
+ color: T.text2,
1719
+ marginBottom: 4
1720
+ } }, t("panelItem.artifacts", { n: det.artifacts.length })), h("div", { style: {
1721
+ ...flexRow,
1722
+ gap: 5
1723
+ } }, det.artifacts.map((a) => h("button", {
1724
+ key: a.name,
1725
+ style: brandBtn,
1726
+ title: t("panelItem.artifactTip", { address: a.address }),
1727
+ onClick: () => openArtifact && openArtifact(a.address, a.name, det.runInfo && det.runInfo.ownerSession || null)
1728
+ }, a.name)))) : muted(t("panelItem.noArtifacts")), det.subtasks && det.subtasks.length ? h("div", null, h("div", { style: {
1729
+ fontSize: 11,
1730
+ color: T.text2,
1731
+ marginBottom: 4
1732
+ } }, t("panelItem.subtasks", { n: det.subtasks.length })), h("div", { style: {
1733
+ display: "flex",
1734
+ flexDirection: "column",
1735
+ gap: 4
1736
+ } }, det.subtasks.map((s) => h("div", {
1737
+ key: s.id,
1738
+ style: {
1739
+ ...flexRow,
1740
+ gap: 6,
1741
+ fontSize: 11,
1742
+ padding: "4px 7px",
1743
+ borderRadius: 7,
1744
+ background: T.layer1,
1745
+ border: `1px solid ${T.border}`
1746
+ }
1747
+ }, h("span", { style: {
1748
+ fontFamily: MONO,
1749
+ fontSize: 10,
1750
+ color: T.text2
1751
+ } }, s.id), chip(stText(s.status), stColor(s.status)), h("span", { style: {
1752
+ color: T.text,
1753
+ minWidth: 0,
1754
+ overflow: "hidden",
1755
+ textOverflow: "ellipsis",
1756
+ whiteSpace: "nowrap"
1757
+ } }, s.title), s.failed ? chip(t("common.failed"), T.error) : null)))) : null, det.bugs && det.bugs.length ? h("div", null, h("div", { style: {
1758
+ fontSize: 11,
1759
+ color: T.text2,
1760
+ marginBottom: 4
1761
+ } }, t("panelItem.bugs", { n: det.bugs.length })), h("div", { style: {
1762
+ display: "flex",
1763
+ flexDirection: "column",
1764
+ gap: 4
1765
+ } }, det.bugs.map((b) => h("div", {
1766
+ key: b.id,
1767
+ style: {
1768
+ ...flexRow,
1769
+ gap: 6,
1770
+ fontSize: 11,
1771
+ padding: "4px 7px",
1772
+ borderRadius: 7,
1773
+ background: T.layer1,
1774
+ border: `1px solid ${T.border}`
1775
+ }
1776
+ }, h("span", { style: {
1777
+ fontFamily: MONO,
1778
+ fontSize: 10,
1779
+ color: T.text2
1780
+ } }, b.id), b.severity ? chip(String(b.severity), T.warn) : null, chip(stText(b.status), stColor(b.status)), h("span", { style: {
1781
+ color: T.text,
1782
+ minWidth: 0,
1783
+ overflow: "hidden",
1784
+ textOverflow: "ellipsis",
1785
+ whiteSpace: "nowrap"
1786
+ } }, b.title))))) : null, det.events && det.events.length ? h("div", null, h("div", { style: {
1787
+ fontSize: 11,
1788
+ color: T.text2,
1789
+ marginBottom: 4
1790
+ } }, t("panelItem.events", { n: Math.min(det.events.length, 30) })), h("div", { style: {
1791
+ display: "flex",
1792
+ flexDirection: "column",
1793
+ gap: 3,
1794
+ fontFamily: MONO,
1795
+ fontSize: 10,
1796
+ color: T.text2
1797
+ } }, det.events.slice(-30).map((e, i) => h("div", { key: i }, `${fmtTime(e.at)} ${e.from || "—"} → ${e.to || "—"}${e.by ? ` · ${e.by}` : ""}${e.reason ? ` · ${e.reason}` : ""}`)))) : null);
1798
+ }
1799
+ function RunDetailPane({ snap, product, api }) {
1800
+ const [sel, setSel] = react.default.useState(null);
1801
+ const [err, setErr] = react.default.useState(null);
1802
+ react.default.useEffect(() => {
1803
+ setSel(null);
1804
+ setErr(null);
1805
+ }, [snap && snap.id]);
1806
+ if (!snap) return muted(t("detail.runMissing"), { padding: 12 });
1807
+ const stages = snap.stages || [];
1808
+ const totals = stages.reduce((a, s) => {
1809
+ const u = s.usage;
1810
+ if (u) {
1811
+ a.input += u.input || 0;
1812
+ a.cacheRead += u.cacheRead || 0;
1813
+ a.cacheWrite += u.cacheWrite || 0;
1814
+ a.output += u.output || 0;
1815
+ a.calls += u.calls || 0;
1816
+ }
1817
+ return a;
1818
+ }, {
1819
+ input: 0,
1820
+ cacheRead: 0,
1821
+ cacheWrite: 0,
1822
+ output: 0,
1823
+ calls: 0
1824
+ });
1825
+ const openStage = async (s) => {
1826
+ if (!api) return;
1827
+ try {
1828
+ setSel(await api.stageDetail(snap.id, s.seq));
1829
+ setErr(null);
1830
+ } catch (e) {
1831
+ setErr(String(e && e.message || e));
1832
+ }
1833
+ };
1834
+ const logs = (snap.logs || []).slice(-60);
1835
+ return h("div", { style: {
1836
+ padding: "10px 12px",
1837
+ display: "flex",
1838
+ flexDirection: "column",
1839
+ gap: 10,
1840
+ minHeight: 0,
1841
+ overflowY: "auto"
1842
+ } }, h("div", { style: {
1843
+ ...flexRow,
1844
+ gap: 6
1845
+ } }, chip(runStatusText(snap.status), stColor(snap.status), { dot: true }), snap.options && snap.options.mode ? chip(String(snap.options.mode), T.text2) : null, h("span", { style: {
1846
+ fontFamily: MONO,
1847
+ fontSize: 10.5,
1848
+ color: T.text2
1849
+ } }, snap.id), product ? h("span", { style: {
1850
+ fontSize: 10,
1851
+ color: T.text2,
1852
+ fontFamily: MONO
1853
+ } }, product) : null), h("div", { style: {
1854
+ fontSize: 12,
1855
+ color: T.text,
1856
+ lineHeight: 1.5
1857
+ } }, snap.requirement || t("runList.noRequirement")), h("div", { style: {
1858
+ ...flexRow,
1859
+ gap: 12,
1860
+ fontSize: 10.5,
1861
+ color: T.text2,
1862
+ fontFamily: MONO
1863
+ } }, h("span", null, `${fmtTime(snap.startedAt)}${snap.endedAt ? ` → ${fmtTime(snap.endedAt)}` : " → " + t("detail.endedRunning")} · ${fmtDur(snap.startedAt, snap.endedAt)}`), h("span", null, t("runList.stageProgress", {
1864
+ done: stages.filter((s) => s.status === "done").length,
1865
+ total: stages.length
1866
+ })), h("span", null, t("detail.subagents", { n: snap.agentsStarted || 0 }))), h("div", { style: {
1867
+ ...flexRow,
1868
+ gap: 10,
1869
+ fontSize: 10.5,
1870
+ color: T.text2,
1871
+ fontFamily: MONO,
1872
+ padding: "6px 8px",
1873
+ borderRadius: 8,
1874
+ background: `color-mix(in srgb, ${T.layer2} 60%, transparent)`,
1875
+ border: `1px solid ${T.border}`
1876
+ } }, h("span", null, t("token.inputMiss", { v: fmtTokens(totals.input) || "0" })), h("span", null, t("token.inputHit", { v: fmtTokens(totals.cacheRead) || "0" })), h("span", null, t("token.cacheWrite", { v: fmtTokens(totals.cacheWrite) || "0" })), h("span", null, t("token.output", { v: fmtTokens(totals.output) || "0" })), h("span", null, t("common.callsCount", { n: totals.calls })), h("span", null, t("token.total", { v: fmtTokens(totalTokens(totals)) || "0" }))), sectionTitle(t("detail.stages", { n: stages.length })), h("div", { style: {
1877
+ display: "flex",
1878
+ flexDirection: "column",
1879
+ gap: 4
1880
+ } }, stages.map((s) => {
1881
+ const on = sel && Number(sel.seq) === Number(s.seq);
1882
+ const color = stColor(s.status);
1883
+ return h("div", {
1884
+ key: s.seq,
1885
+ onClick: () => openStage(s),
1886
+ style: {
1887
+ padding: "6px 9px",
1888
+ borderRadius: 8,
1889
+ cursor: "pointer",
1890
+ border: `1px solid ${on ? `color-mix(in srgb, ${T.brand} 45%, transparent)` : T.border}`,
1891
+ borderLeft: `3px solid ${color}`,
1892
+ background: on ? `color-mix(in srgb, ${T.brand} 7%, ${T.layer1})` : T.layer1
1893
+ }
1894
+ }, h("div", { style: {
1895
+ ...flexRow,
1896
+ gap: 6
1897
+ } }, h("span", { style: {
1898
+ fontFamily: MONO,
1899
+ fontSize: 10,
1900
+ color: T.text2
1901
+ } }, `#${s.seq}`), h("span", null, phaseIconOf(s.phase)), h("span", { style: {
1902
+ fontSize: 11.5,
1903
+ color: T.text,
1904
+ fontWeight: 500
1905
+ } }, stageLabelOf(s)), chip(stText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
1906
+ marginLeft: "auto",
1907
+ fontFamily: MONO,
1908
+ fontSize: 10,
1909
+ color: T.text2
1910
+ } }, `${fmtTime(s.startedAt)}${s.endedAt ? ` · ${fmtDur(s.startedAt, s.endedAt)}` : ""}`)), h("div", { style: {
1911
+ ...flexRow,
1912
+ gap: 10,
1913
+ marginTop: 3,
1914
+ fontFamily: MONO,
1915
+ fontSize: 10,
1916
+ color: T.text2
1917
+ } }, h("span", null, stageUsageLine(s) || t("common.noUsage")), s.childId ? h("span", { title: s.childId }, t("detail.subagentChip", { id: String(s.childId).slice(0, 14) })) : null), s.summary ? h("div", { style: {
1918
+ fontSize: 10.5,
1919
+ color: T.text2,
1920
+ marginTop: 3,
1921
+ lineHeight: 1.45
1922
+ } }, String(s.summary).slice(0, 200)) : null);
1923
+ })), err ? muted(t("detail.stageFailed", { err }), { color: T.error }) : null, sel ? h("div", { style: {
1924
+ padding: "9px 10px",
1925
+ borderRadius: 9,
1926
+ border: `1px solid ${T.border}`,
1927
+ background: T.layer2,
1928
+ display: "flex",
1929
+ flexDirection: "column",
1930
+ gap: 8
1931
+ } }, h("div", { style: {
1932
+ ...flexRow,
1933
+ justifyContent: "space-between"
1934
+ } }, h("div", { style: {
1935
+ ...flexRow,
1936
+ gap: 6
1937
+ } }, h("span", { style: {
1938
+ fontSize: 11.5,
1939
+ fontWeight: 700,
1940
+ color: T.text
1941
+ } }, t("detail.stageTitle", { seq: sel.seq })), chip(stText(sel.status), stColor(sel.status))), h("button", {
1942
+ style: panelBtn,
1943
+ onClick: () => setSel(null)
1944
+ }, t("common.collapse"))), h("div", { style: {
1945
+ fontSize: 10.5,
1946
+ color: T.text2,
1947
+ fontFamily: MONO
1948
+ } }, t("token.stageLine", { v: stageUsageLine(sel) || t("common.none") })), sel.verifyEvidence ? h("div", null, h("div", { style: {
1949
+ fontSize: 11,
1950
+ color: T.success,
1951
+ marginBottom: 3
1952
+ } }, t("stage.evidenceTitle")), h("div", { style: {
1953
+ whiteSpace: "pre-wrap",
1954
+ wordBreak: "break-word",
1955
+ fontSize: 11,
1956
+ lineHeight: 1.6,
1957
+ color: T.text,
1958
+ background: `color-mix(in srgb, ${T.layer1} 70%, transparent)`,
1959
+ border: `1px solid ${T.border}`,
1960
+ borderRadius: 8,
1961
+ padding: "7px 9px",
1962
+ maxHeight: 200,
1963
+ overflowY: "auto",
1964
+ fontFamily: MONO
1965
+ } }, sel.verifyEvidence)) : muted(t("stage.evidenceMissingShort"), { color: T.warn }), sel.output ? h("div", null, h("div", { style: {
1966
+ fontSize: 11,
1967
+ color: T.text2,
1968
+ marginBottom: 3
1969
+ } }, t("detail.output")), h(FoldableText, {
1970
+ text: sel.output,
1971
+ charLimit: 400,
1972
+ lineLimit: 8,
1973
+ style: {
1974
+ fontFamily: MONO,
1975
+ fontSize: 11
1976
+ }
1977
+ })) : null, sel.attempts && sel.attempts.length > 1 ? h("div", null, h("div", { style: {
1978
+ fontSize: 11,
1979
+ color: T.text2,
1980
+ marginBottom: 3
1981
+ } }, t("detail.attempts", { n: sel.attempts.length })), h("div", { style: {
1982
+ display: "flex",
1983
+ flexDirection: "column",
1984
+ gap: 3
1985
+ } }, sel.attempts.map((a) => h("div", {
1986
+ key: a.seq,
1987
+ style: {
1988
+ ...flexRow,
1989
+ gap: 6,
1990
+ fontSize: 10.5,
1991
+ fontFamily: MONO,
1992
+ color: T.text2
1993
+ }
1994
+ }, h("span", null, `#${a.seq}`), chip(stText(a.status), stColor(a.status)), a.outcome ? h("span", null, a.outcome) : null, h("span", { style: { marginLeft: "auto" } }, `${fmtTime(a.startedAt)} · ${fmtDur(a.startedAt, a.endedAt)}`))))) : null) : null, logs.length ? h("div", null, sectionTitle(t("detail.logs", { n: logs.length })), h("div", { style: {
1995
+ display: "flex",
1996
+ flexDirection: "column",
1997
+ gap: 2,
1998
+ fontFamily: MONO,
1999
+ fontSize: 10,
2000
+ color: T.text2,
2001
+ maxHeight: 220,
2002
+ overflowY: "auto"
2003
+ } }, logs.map((l, i) => h("div", {
2004
+ key: i,
2005
+ style: { color: l.level === "error" ? T.error : l.level === "warn" ? T.warn : T.text2 }
2006
+ }, `${fmtTime(l.t)} [${l.level}] ${l.message}`)))) : null);
2007
+ }
2008
+ /** 右栏 run 详情 tab 正文(地址来自宿主导航记录;host 只认自己的地址格式)。
2009
+ *
2010
+ * ⚠️ prop 名:宿主渲染器把 slot 的 `hooks: { tabInfo }` 绑成 **`useTabInfo`**(约定 `hooks.<name>` → `use<Name>`;
2011
+ * 官方 `ui-sidebar-documentpreview/TextPreview.tsx` 即用 `useTabInfo`)。2026-09-11 实锤:写成 `tabInfo`
2012
+ * 会恒 undefined → 地址取不到、错误也没设 → 永远停在「读取 run 详情中…」。
2013
+ */
2014
+ function RunDetailTab(props) {
2015
+ const readTab = props && (typeof props.useTabInfo === "function" ? props.useTabInfo : typeof props.tabInfo === "function" ? props.tabInfo : null);
2016
+ let info = null;
2017
+ try {
2018
+ info = readTab ? readTab() : null;
2019
+ } catch (e) {
2020
+ info = null;
2021
+ }
2022
+ const tab = info && info.tab ? info.tab : null;
2023
+ const address = tab && tab.navigation && tab.navigation.address || tab && tab.contentId || null;
2024
+ const parsed = parseRunAddress(address);
2025
+ const remote = props && props.remote;
2026
+ const [snap, setSnap] = react.default.useState(null);
2027
+ const [err, setErr] = react.default.useState(null);
2028
+ const [nonce, setNonce] = react.default.useState(0);
2029
+ const product = parsed ? parsed.product : null;
2030
+ const runId = parsed ? parsed.runId : null;
2031
+ const api = react.default.useMemo(() => remote && product ? productApi(remote, product) : null, [remote, product]);
2032
+ const [, bump] = react.default.useReducer((n) => n + 1, 0);
2033
+ react.default.useEffect(() => {
2034
+ if (address) return void 0;
2035
+ let tries = 0;
2036
+ const timer = setInterval(() => {
2037
+ tries += 1;
2038
+ bump();
2039
+ if (tries >= 12) clearInterval(timer);
2040
+ }, 150);
2041
+ return () => clearInterval(timer);
2042
+ }, [address]);
2043
+ react.default.useEffect(() => {
2044
+ let alive = true;
2045
+ if (!runId) {
2046
+ setSnap(null);
2047
+ setErr(address ? t("tab.resolveFailed", { address }) : readTab ? t("tab.readingAddress") : t("tab.noHook"));
2048
+ return;
2049
+ }
2050
+ if (!api) {
2051
+ setSnap(null);
2052
+ setErr(t("tab.remoteUnavailable"));
2053
+ return;
2054
+ }
2055
+ api.runDetail(runId).then((v) => {
2056
+ if (alive) {
2057
+ setSnap(v);
2058
+ setErr(null);
2059
+ }
2060
+ }, (e) => {
2061
+ if (alive) setErr(String(e && e.message || e));
2062
+ });
2063
+ const timer = setInterval(() => {
2064
+ api.runDetail(runId).then((v) => {
2065
+ if (!alive) return;
2066
+ setSnap(v);
2067
+ if (!v || !(v.status === "running" || v.status === "pending")) clearInterval(timer);
2068
+ }, () => {});
2069
+ }, 5e3);
2070
+ return () => {
2071
+ alive = false;
2072
+ clearInterval(timer);
2073
+ };
2074
+ }, [
2075
+ api,
2076
+ runId,
2077
+ address,
2078
+ nonce,
2079
+ readTab
2080
+ ]);
2081
+ if (err && !snap) {
2082
+ const pending = !address && !!readTab;
2083
+ return h("div", { style: {
2084
+ padding: 12,
2085
+ display: "flex",
2086
+ flexDirection: "column",
2087
+ gap: 8
2088
+ } }, muted(pending ? t("tab.readingAddress") : err, { color: pending ? T.text2 : T.error }), pending ? null : h("button", {
2089
+ style: panelBtn,
2090
+ onClick: () => {
2091
+ setErr(null);
2092
+ setNonce((n) => n + 1);
2093
+ }
2094
+ }, t("common.retry")));
2095
+ }
2096
+ if (!snap) return muted(t("tab.readingRun"), { padding: 12 });
2097
+ return h(RunDetailPane, {
2098
+ snap,
2099
+ product,
2100
+ api
2101
+ });
2102
+ }
2103
+ function productApi(remote, product) {
2104
+ return {
2105
+ view: async () => unwrap$1(await remote.productView(product), "productView"),
2106
+ runDetail: async (runId) => unwrap$1(await remote.productRunDetail(product, runId), "productRunDetail"),
2107
+ stageDetail: async (runId, seq) => unwrap$1(await remote.productStageDetail(product, runId, seq), "productStageDetail"),
2108
+ itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail")
2109
+ };
2110
+ }
2111
+ /** 全局面板 props:root scope 标准 props(useSessions 取当前会话)+ 插件注入(remote/打开回调)。 */
2112
+ function GlobalPanel(props) {
2113
+ const remote = props.remote;
2114
+ const useSessions = props.useSessions;
2115
+ const currentSessionId = (() => {
2116
+ try {
2117
+ return useSessions ? useSessions((s) => s.current) : void 0;
2118
+ } catch (e) {
2119
+ return;
2120
+ }
2121
+ })();
2122
+ const [state, setState] = react.default.useState({
2123
+ products: [],
2124
+ current: null,
2125
+ view: null,
2126
+ err: null,
2127
+ busy: false
2128
+ });
2129
+ const [detail, setDetail] = react.default.useState(null);
2130
+ const [hint, setHint] = react.default.useState(null);
2131
+ const [runsExpanded, setRunsExpanded] = react.default.useState(false);
2132
+ const [panelTab, setPanelTab] = react.default.useState("run");
2133
+ const [viewTick, setViewTick] = react.default.useState(0);
2134
+ const [runFilter, setRunFilter] = react.default.useState([]);
2135
+ /**
2136
+ * 右侧栏的**会话内容**宿主只在「对话被选中」时渲染(`RightbarRoot` 门控 `activePanelId === null`),
2137
+ * 而全局面板占着 `main` → seat 不存在 → `ctx.sidebarRight` 没有 binding(抛 `no session surface
2138
+ * is mounted`,2026-09-11 用户实测 tf-mtvrsakj-l2vj5u)。所以:先切回对话,再小步重试
2139
+ * (seat 在切换后的 effect 里才 `bindService`);彻底失败才降级,并把原因显示在面板里。
2140
+ */
2141
+ const openInConversationRightbar = (address, label, fallback) => {
2142
+ const attempt = (quiet) => !!(props.openResource && address && props.openResource(address, label, quiet));
2143
+ if (attempt(false)) return;
2144
+ if (!address) {
2145
+ setHint(t("panel.hintNoAddress", { label }));
2146
+ if (fallback) fallback();
2147
+ return;
2148
+ }
2149
+ try {
2150
+ if (props.layout && typeof props.layout.selectPanel === "function") props.layout.selectPanel(null);
2151
+ } catch (e) {}
2152
+ let tries = 0;
2153
+ const tick = () => {
2154
+ tries += 1;
2155
+ if (attempt(true)) return;
2156
+ if (tries < 12) {
2157
+ setTimeout(tick, 120);
2158
+ return;
2159
+ }
2160
+ setHint(t("panel.hintRightbarFailed", { label }) + (fallback ? t("panel.hintInlineSuffix") : ""));
2161
+ if (fallback) fallback();
2162
+ };
2163
+ setTimeout(tick, 140);
2164
+ };
2165
+ /**
2166
+ * **跳到资源所属的会话,再在那个会话的右栏打开**(全局面板的正确语义)。
2167
+ * 右侧栏是会话级的:从全局面板看 tetris 的 run 却把 tab 挂到"用户当前所在会话"上没有意义
2168
+ * (用户 2026-09-11 提出)。所以先 `sessions.open(ownerSession)`,等当前会话真的切过去、
2169
+ * 且对话 seat 挂载 bind 之后再 openResource(两者都要等,故小步重试 + 就绪判据)。
2170
+ * @param target.ownerSession - 资源所属会话(run 的发起会话 / 产物地址里的会话)
2171
+ * @param target.address - host 生成的 dsh-resource 地址
2172
+ * @param target.label - 提示用的名字
2173
+ * @param target.fallback - 没有 ownerSession 或跳转失败时的降级(通常是面板内联详情)
2174
+ */
2175
+ const goOwnerSessionAndOpen = (target) => {
2176
+ const { ownerSession, address, label, fallback } = target || {};
2177
+ const sessions = props.sessions;
2178
+ if (!ownerSession || !sessions || typeof sessions.open !== "function") {
2179
+ openInConversationRightbar(address, label, fallback);
2180
+ return;
2181
+ }
2182
+ try {
2183
+ sessions.open(ownerSession);
2184
+ } catch (e) {
2185
+ setHint(t("panel.hintSessionGone", { sid: String(ownerSession).slice(0, 8) }));
2186
+ if (fallback) fallback();
2187
+ return;
2188
+ }
2189
+ try {
2190
+ if (props.layout && typeof props.layout.selectPanel === "function") props.layout.selectPanel(null);
2191
+ } catch (e) {}
2192
+ let tries = 0;
2193
+ const tick = () => {
2194
+ tries += 1;
2195
+ let nowCurrent = null;
2196
+ try {
2197
+ nowCurrent = sessions.list && sessions.list.getSnapshot ? sessions.list.getSnapshot().current : null;
2198
+ } catch (e) {
2199
+ nowCurrent = null;
2200
+ }
2201
+ if ((nowCurrent === ownerSession || tries >= 6) && props.openResource && address && props.openResource(address, label, tries < 6)) return;
2202
+ if (tries < 14) {
2203
+ setTimeout(tick, 130);
2204
+ return;
2205
+ }
2206
+ setHint(t("panel.hintSwitchedNoRightbar", { label }));
2207
+ };
2208
+ setTimeout(tick, 140);
2209
+ };
2210
+ const loadProducts = react.default.useCallback(async (preferKey) => {
2211
+ if (!remote || typeof remote.products !== "function") {
2212
+ setState((s) => ({
2213
+ ...s,
2214
+ err: t("panel.remoteProductsUnavailable")
2215
+ }));
2216
+ return;
2217
+ }
2218
+ setState((s) => ({
2219
+ ...s,
2220
+ busy: true
2221
+ }));
2222
+ try {
2223
+ const v = unwrap$1(await remote.products(currentSessionId || null), "products") || {};
2224
+ const list = v.products || [];
2225
+ const key = preferKey || state.current || (v.current && list.some((p) => p.key === v.current) ? v.current : list[0] && list[0].key || null);
2226
+ setState({
2227
+ products: list,
2228
+ current: key,
2229
+ view: key === state.current ? state.view : null,
2230
+ err: null,
2231
+ busy: false
2232
+ });
2233
+ setViewTick((t) => t + 1);
2234
+ } catch (e) {
2235
+ setState((s) => ({
2236
+ ...s,
2237
+ busy: false,
2238
+ err: String(e && e.message || e)
2239
+ }));
2240
+ }
2241
+ }, [
2242
+ remote,
2243
+ currentSessionId,
2244
+ state.current,
2245
+ state.view
2246
+ ]);
2247
+ const loadView = react.default.useCallback(async (key, silent) => {
2248
+ if (!remote || !key) return;
2249
+ const api = productApi(remote, key);
2250
+ try {
2251
+ const v = await api.view();
2252
+ setState((s) => ({
2253
+ ...s,
2254
+ view: v,
2255
+ err: null
2256
+ }));
2257
+ } catch (e) {
2258
+ if (!silent) setState((s) => ({
2259
+ ...s,
2260
+ err: String(e && e.message || e)
2261
+ }));
2262
+ }
2263
+ }, [remote]);
2264
+ /** 选中产品线:**同一个**产品线 = 刷新(保留现有视图,不清空 → 不会卡"读取中"且不闪);不同产品线 = 清空 + 重新拉。 */
2265
+ const selectProduct = (k) => {
2266
+ closeDetail();
2267
+ setRunsExpanded(false);
2268
+ setRunFilter([]);
2269
+ setState((s) => ({
2270
+ ...s,
2271
+ current: k,
2272
+ view: s.current === k ? s.view : null
2273
+ }));
2274
+ setViewTick((t) => t + 1);
2275
+ };
2276
+ /** run 状态筛选开关(多选)。 */
2277
+ const toggleRunStatus = (st) => setRunFilter((cur) => cur.indexOf(st) === -1 ? cur.concat([st]) : cur.filter((x) => x !== st));
2278
+ react.default.useEffect(() => {
2279
+ loadProducts();
2280
+ }, [currentSessionId]);
2281
+ react.default.useEffect(() => {
2282
+ if (state.current) loadView(state.current);
2283
+ }, [
2284
+ state.current,
2285
+ viewTick,
2286
+ loadView
2287
+ ]);
2288
+ react.default.useEffect(() => {
2289
+ if (!(state.view && state.view.runs || []).some((r) => r.status === "running" || r.status === "pending") || !state.current) return void 0;
2290
+ const timer = setInterval(() => {
2291
+ loadView(state.current, true);
2292
+ }, 8e3);
2293
+ return () => clearInterval(timer);
2294
+ }, [
2295
+ state.current,
2296
+ state.view,
2297
+ loadView
2298
+ ]);
2299
+ const api = state.current && remote ? productApi(remote, state.current) : null;
2300
+ const openItem = async (kind, id) => {
2301
+ if (!api) return;
2302
+ try {
2303
+ setDetail({
2304
+ kind: "item",
2305
+ data: await api.itemDetail(kind, id, currentSessionId || null)
2306
+ });
2307
+ } catch (e) {
2308
+ setState((s) => ({
2309
+ ...s,
2310
+ err: String(e && e.message || e)
2311
+ }));
2312
+ }
2313
+ };
2314
+ const showInline = async (r) => {
2315
+ if (!api) return;
2316
+ try {
2317
+ const snap = await api.runDetail(r.id);
2318
+ setDetail({
2319
+ kind: "run",
2320
+ data: snap,
2321
+ run: r
2322
+ });
2323
+ } catch (e) {
2324
+ setState((s) => ({
2325
+ ...s,
2326
+ err: String(e && e.message || e)
2327
+ }));
2328
+ }
2329
+ };
2330
+ const closeDetail = () => setDetail(null);
2331
+ const openRun = (r) => {
2332
+ goOwnerSessionAndOpen({
2333
+ ownerSession: r.ownerSession,
2334
+ address: r.address,
2335
+ label: r.id,
2336
+ fallback: () => {
2337
+ showInline(r);
2338
+ }
2339
+ });
2340
+ };
2341
+ const openArtifactInPanel = (address, name, ownerSession) => {
2342
+ goOwnerSessionAndOpen({
2343
+ ownerSession,
2344
+ address,
2345
+ label: name
2346
+ });
2347
+ };
2348
+ const view = state.view;
2349
+ const product = view && view.product;
2350
+ const runs = view && view.runs || [];
2351
+ const detailOpen = !!detail;
2352
+ const runSel = runFilter || [];
2353
+ const runSelSet = new Set(runSel);
2354
+ const runFiltering = runSel.length > 0;
2355
+ const runsMatched = runFiltering ? runs.filter((r) => runSelSet.has(r.status)) : runs;
2356
+ const runStatusCounts = {};
2357
+ for (const r of runs) runStatusCounts[r.status] = (runStatusCounts[r.status] || 0) + 1;
2358
+ const headRuns = runsMatched.slice(0, RUN_PREVIEW);
2359
+ const headIds = new Set(headRuns.map((r) => r.id));
2360
+ const pinnedActive = runsExpanded ? [] : runsMatched.filter((r) => (r.status === "running" || r.status === "pending") && !headIds.has(r.id));
2361
+ const visibleRuns = runsExpanded || runsMatched.length <= RUN_PREVIEW ? runsMatched : pinnedActive.concat(headRuns);
2362
+ const bl = view && view.backlog;
2363
+ const backlogCount = bl ? (bl.requirements || []).length + (bl.tasks || []).filter((t) => t.type !== "subtask").length + (bl.bugs || []).length : 0;
2364
+ /** 主区标签按钮(选中态用品牌色下划线)。 */
2365
+ const panelTabBtn = (key, label) => h("button", {
2366
+ key,
2367
+ onClick: () => setPanelTab(key),
2368
+ style: {
2369
+ font: "inherit",
2370
+ fontSize: 12,
2371
+ fontWeight: panelTab === key ? 700 : 500,
2372
+ padding: "6px 12px",
2373
+ marginBottom: -1,
2374
+ cursor: "pointer",
2375
+ whiteSpace: "nowrap",
2376
+ background: "transparent",
2377
+ border: "none",
2378
+ borderBottom: `2px solid ${panelTab === key ? T.brand : "transparent"}`,
2379
+ color: panelTab === key ? T.text : T.text2
2380
+ }
2381
+ }, label);
2382
+ return h("div", { style: {
2383
+ height: "100%",
2384
+ minHeight: 0,
2385
+ display: "flex",
2386
+ flexDirection: "column",
2387
+ background: T.bg,
2388
+ color: T.text,
2389
+ fontFamily: SANS,
2390
+ fontSize: 12
2391
+ } }, h("div", { style: {
2392
+ ...flexRow,
2393
+ justifyContent: "space-between",
2394
+ padding: "10px 14px",
2395
+ borderBottom: `1px solid ${T.border}`
2396
+ } }, h("div", { style: {
2397
+ ...flexRow,
2398
+ gap: 8
2399
+ } }, h("span", { style: {
2400
+ fontSize: 13,
2401
+ fontWeight: 700
2402
+ } }, t("panel.title")), h("span", { style: {
2403
+ fontSize: 11,
2404
+ color: T.text2
2405
+ } }, t("panel.subtitle")), currentSessionId ? h("span", { style: {
2406
+ fontSize: 10,
2407
+ color: T.text2,
2408
+ fontFamily: MONO
2409
+ } }, t("panel.currentSession", { sid: String(currentSessionId).slice(0, 8) })) : h("span", { style: {
2410
+ fontSize: 10,
2411
+ color: T.text2
2412
+ } }, t("panel.noSession"))), h("div", { style: {
2413
+ ...flexRow,
2414
+ gap: 6
2415
+ } }, product ? h("button", {
2416
+ style: panelBtn,
2417
+ onClick: () => loadView(state.current)
2418
+ }, t("panel.reload")) : null, h("button", {
2419
+ style: panelBtn,
2420
+ title: t("panel.backToChatTip"),
2421
+ onClick: () => {
2422
+ try {
2423
+ const layout = props.layout;
2424
+ if (layout && layout.selectPanel) layout.selectPanel(null);
2425
+ } catch (e) {}
2426
+ }
2427
+ }, t("panel.backToChat")))), state.err ? h("div", { style: {
2428
+ padding: "6px 14px",
2429
+ fontSize: 11,
2430
+ color: T.error,
2431
+ borderBottom: `1px solid ${T.border}`
2432
+ } }, state.err) : null, hint ? h("div", { style: {
2433
+ ...flexRow,
2434
+ justifyContent: "space-between",
2435
+ gap: 8,
2436
+ padding: "6px 14px",
2437
+ fontSize: 11,
2438
+ color: T.warn,
2439
+ borderBottom: `1px solid ${T.border}`,
2440
+ background: `color-mix(in srgb, ${T.warn} 8%, transparent)`
2441
+ } }, h("span", null, hint), h("button", {
2442
+ style: panelBtn,
2443
+ onClick: () => setHint(null)
2444
+ }, t("common.gotIt"))) : null, h("div", { style: {
2445
+ flex: 1,
2446
+ minHeight: 0,
2447
+ display: "flex",
2448
+ position: "relative"
2449
+ } }, h(ProductRail, {
2450
+ products: state.products,
2451
+ current: state.current,
2452
+ loadingKey: state.view ? null : state.current,
2453
+ busy: state.busy,
2454
+ onRefresh: () => loadProducts(state.current),
2455
+ onSelect: selectProduct
2456
+ }), h("div", { style: {
2457
+ flex: 1,
2458
+ minWidth: 0,
2459
+ minHeight: 0,
2460
+ display: "flex",
2461
+ flexDirection: "column"
2462
+ } }, !state.current ? h("div", { style: { padding: "12px 14px" } }, muted(t("panel.pickProduct"))) : !view ? h("div", { style: { padding: "12px 14px" } }, muted(t("panel.loadingView"))) : h(react.default.Fragment, null, h("div", { style: {
2463
+ ...flexRow,
2464
+ justifyContent: "space-between",
2465
+ gap: 10,
2466
+ padding: "12px 14px 6px"
2467
+ } }, h("div", { style: { minWidth: 0 } }, h("div", { style: {
2468
+ fontSize: 15,
2469
+ fontWeight: 700,
2470
+ color: T.text
2471
+ } }, product.title || product.key), h("div", { style: {
2472
+ fontSize: 10.5,
2473
+ color: T.text2,
2474
+ fontFamily: MONO,
2475
+ marginTop: 2,
2476
+ overflow: "hidden",
2477
+ textOverflow: "ellipsis",
2478
+ whiteSpace: "nowrap"
2479
+ } }, product.path || product.key)), h("div", { style: {
2480
+ ...flexRow,
2481
+ gap: 6,
2482
+ flex: "0 0 auto"
2483
+ } }, chip(t("panel.runChip", { n: product.totalRuns }), T.text2), product.activeRuns > 0 ? chip(t("panel.activeChip", { n: product.activeRuns }), T.brand, { dot: true }) : null, product.lastVerdict ? chip(t("panel.verdictChip", { v: product.lastVerdict }), stColor("accepted")) : null)), h("div", { style: {
2484
+ display: "flex",
2485
+ alignItems: "flex-end",
2486
+ gap: 2,
2487
+ padding: "0 14px",
2488
+ borderBottom: `1px solid ${T.border}`
2489
+ } }, panelTabBtn("run", t("panel.tabRuns", { n: runs.length })), panelTabBtn("backlog", t("panel.tabBacklog", { n: backlogCount })), h("div", { style: {
2490
+ marginLeft: "auto",
2491
+ ...flexRow,
2492
+ gap: 6,
2493
+ paddingBottom: 7
2494
+ } }, panelTab === "run" && !runsExpanded && pinnedActive.length > 0 ? chip(t("panel.pinnedActive", { n: pinnedActive.length }), T.brand, { dot: true }) : null, panelTab === "run" && runsMatched.length > RUN_PREVIEW ? h("button", {
2495
+ style: panelBtn,
2496
+ onClick: () => setRunsExpanded((v) => !v)
2497
+ }, runsExpanded ? t("panel.showRecent", { n: RUN_PREVIEW }) : t("panel.showAll", { n: runsMatched.length })) : null)), h("div", { style: {
2498
+ flex: 1,
2499
+ minHeight: 0,
2500
+ overflowY: "auto",
2501
+ overflowX: "hidden",
2502
+ padding: "12px 14px 18px"
2503
+ } }, panelTab === "run" ? h(react.default.Fragment, null, h("div", { style: {
2504
+ ...flexRow,
2505
+ gap: 6,
2506
+ marginBottom: 8
2507
+ } }, ...RUN_STATUS_ORDER.filter((st) => runStatusCounts[st]).map((st) => filterChip(`${runStatusText(st)} ${runStatusCounts[st]}`, stColor(st), runSelSet.has(st), () => toggleRunStatus(st))), runFiltering ? h("button", {
2508
+ style: panelBtn,
2509
+ title: t("panel.clearRunFilterTip"),
2510
+ onClick: () => setRunFilter([])
2511
+ }, t("panel.filtered", {
2512
+ sel: runSel.length,
2513
+ shown: runsMatched.length,
2514
+ total: runs.length
2515
+ })) : null), visibleRuns.length ? h(RunList, {
2516
+ runs: visibleRuns,
2517
+ activeRunId: detail && detail.kind === "run" && detail.run ? detail.run.id : null,
2518
+ onOpenRun: openRun,
2519
+ onInlineRun: showInline
2520
+ }) : muted(t("panel.emptyRunFilter"), { fontSize: 10.5 }), muted(t("panel.runHint"), {
2521
+ fontSize: 10,
2522
+ marginTop: 8
2523
+ })) : h(react.default.Fragment, null, muted(t("panel.boardHint"), {
2524
+ fontSize: 10,
2525
+ marginBottom: 8
2526
+ }), h(BacklogGroups, {
2527
+ backlog: view.backlog,
2528
+ onOpen: openItem,
2529
+ productKey: state.current
2530
+ }))))), detailOpen ? h("div", { style: {
2531
+ position: "absolute",
2532
+ top: 8,
2533
+ right: 12,
2534
+ bottom: 8,
2535
+ width: 440,
2536
+ zIndex: 9,
2537
+ overflowY: "auto",
2538
+ overflowX: "hidden",
2539
+ borderRadius: 12,
2540
+ border: `1px solid ${T.border}`,
2541
+ background: `color-mix(in srgb, ${T.layer1} 96%, transparent)`,
2542
+ backdropFilter: "blur(12px)",
2543
+ boxShadow: "0 14px 44px rgba(0,0,0,.28)"
2544
+ } }, detail && detail.kind === "item" ? h(ItemDetailPane, {
2545
+ det: detail.data,
2546
+ openArtifact: openArtifactInPanel,
2547
+ onClose: closeDetail
2548
+ }) : h("div", null, h("div", { style: {
2549
+ ...flexRow,
2550
+ justifyContent: "space-between",
2551
+ padding: "10px 12px 0"
2552
+ } }, h("span", { style: {
2553
+ fontSize: 11.5,
2554
+ fontWeight: 700,
2555
+ color: T.text
2556
+ } }, t("panel.runDetail")), h("div", { style: {
2557
+ ...flexRow,
2558
+ gap: 6
2559
+ } }, detail && detail.data && detail.data.address ? h("button", {
2560
+ style: brandBtn,
2561
+ title: t("panel.goOwnerSessionTip"),
2562
+ onClick: () => goOwnerSessionAndOpen({
2563
+ ownerSession: detail.data.ownerSession,
2564
+ address: detail.data.address,
2565
+ label: detail.data.id
2566
+ })
2567
+ }, t("panel.goOwnerSession")) : null, h("button", {
2568
+ style: panelBtn,
2569
+ onClick: closeDetail
2570
+ }, t("common.close")))), h(RunDetailPane, {
2571
+ snap: detail && detail.data,
2572
+ product: state.current,
2573
+ api
2574
+ }))) : null));
481
2575
  }
482
- /** 官方口径工具函数(与 DeepSeek usage 账单对齐,零额外概念):
483
- * - u.input : 输入(缓存未命中)
484
- * - u.cacheRead : 输入(缓存命中)
485
- * - u.cacheWrite : 输入写入缓存
486
- * - u.output : 输出
487
- * billed input = input + cacheRead + cacheWrite;命中率 = cacheRead / (input + cacheRead)。
2576
+ //#endregion
2577
+ //#region client/index.tsx
2578
+ /**
2579
+ * dsh-plugin-teamflow — browser half(v0.7 视觉重构:「精密工程控制台」)。
2580
+ *
2581
+ * 注册 conversation.view tab「🏭 团队工作台」(与 chat / 轨迹并列):
2582
+ * - 流水线图形工作流:阶段泳道 + 节点卡片(状态/耗时/token/子代理会话)+ 实时轮询
2583
+ * - Backlog 拖拽看板:需求/任务/缺陷三组状态泳道,卡片拖拽流转(原生 HTML5 DnD,零依赖)
2584
+ * - 成本中心:每阶段 token + 总计 + 运行时长
2585
+ * - 人工介入中心:needs-human 项聚合 + 一键终态
2586
+ * - 历史 run 切换
2587
+ *
2588
+ * 视觉:全部使用 DSH 主题 token(--dsw-alias-*,自动适配深浅色)。
2589
+ * Remote 接入:$mount 后 ctx.get('remote.teamflow') 取全局实例传给组件
2590
+ * (不能 inject 'remote.teamflow'——自挂载自注入会让 fiber 死锁)。
488
2591
  */
489
- function hitRate(u) {
490
- const total = (u.input || 0) + (u.cacheRead || 0);
491
- return total > 0 ? Math.round((u.cacheRead || 0) / total * 100) : null;
492
- }
493
- function usageDetail(s) {
494
- const k = (n) => n === null || n === void 0 ? "—" : fmtTokens(n);
495
- if (s.usage) {
496
- const u = s.usage;
497
- const hit = hitRate(u);
498
- return `输入(未命中) ${k(u.input)} / 输入(命中) ${k(u.cacheRead)} / 写缓存 ${k(u.cacheWrite)} / 输出 ${k(u.output)} · ${u.calls} 次调用${hit !== null ? ` · 缓存命中 ${hit}%` : ""}`;
499
- }
500
- return "无 usage 明细";
501
- }
502
- /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
503
- function stageUsageLine(s) {
504
- const u = s && s.usage;
505
- if (u && (u.input || u.cacheRead || u.cacheWrite || u.output)) {
506
- const hit = hitRate(u);
507
- return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""}`;
508
- }
509
- return null;
510
- }
511
- const ROLE_NAME = {
512
- pm: "产品",
513
- design: "设计",
514
- arch: "架构",
515
- tech: "方案",
516
- dev: "开发",
517
- qa: "测试",
518
- acceptance: "验收",
519
- other: "其他"
520
- };
521
- const roleUsage = (u) => {
522
- if (!u) return "";
523
- const hit = hitRate(u);
524
- return `⇅${fmtTokens(u.input || 0)}/${fmtTokens(u.cacheRead || 0)}·⬆${fmtTokens(u.output || 0)}${hit !== null ? `·${hit}%` : ""}`;
525
- };
526
- /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
527
- function byRoleLine(task) {
528
- const roles = task && task.byRole || {};
529
- return Object.keys(roles).filter((k) => roles[k] && roles[k].input + roles[k].output + roles[k].cacheRead + roles[k].cacheWrite > 0).map((k) => `${ROLE_NAME[k] || k} ${roleUsage(roles[k])}`).join(" · ");
530
- }
531
- /** 多阶段 usage 汇总(官方口径)。 */
532
- function totalUsage(stages) {
533
- const t = {
534
- input: 0,
535
- cacheRead: 0,
536
- cacheWrite: 0,
537
- output: 0,
538
- calls: 0
539
- };
540
- for (const s of stages || []) {
541
- const u = s && s.usage;
542
- if (!u) continue;
543
- t.input += u.input || 0;
544
- t.cacheRead += u.cacheRead || 0;
545
- t.cacheWrite += u.cacheWrite || 0;
546
- t.output += u.output || 0;
547
- t.calls += u.calls || 0;
548
- }
549
- return t;
550
- }
551
- const stText = (s) => STATUS_TEXT[s] || s;
552
- const stColor = (s) => STATUS_COLOR[s] || T.text2;
2592
+ const inject = [
2593
+ "remote",
2594
+ "slots",
2595
+ "sessions",
2596
+ "locale"
2597
+ ];
553
2598
  const NODE_W = 300;
554
2599
  const PHASE_GAP = 88;
555
2600
  const WAVE = 56;
@@ -615,7 +2660,7 @@ window.__ModuleLoader__.load({
615
2660
  const usage = stageUsageLine(s);
616
2661
  return h("div", {
617
2662
  key,
618
- title: `${s.label} —— 点击查看阶段详情`,
2663
+ title: t("stage.cardTip", { label: stageLabelOf(s) }),
619
2664
  onMouseDown: (e) => e.stopPropagation(),
620
2665
  onClick: () => onOpen && onOpen(s),
621
2666
  style: {
@@ -664,8 +2709,11 @@ window.__ModuleLoader__.load({
664
2709
  textOverflow: "ellipsis",
665
2710
  whiteSpace: "nowrap"
666
2711
  }
667
- }, s.__taskKey || s.label), s.attempts && s.attempts.length > 1 ? h("span", {
668
- title: `重试 ${s.attempts.length - 1} 次(共 ${s.attempts.length} 次尝试)`,
2712
+ }, stageLabelOf(s)), s.attempts && s.attempts.length > 1 ? h("span", {
2713
+ title: t("stage.retryTip", {
2714
+ n: s.attempts.length - 1,
2715
+ m: s.attempts.length
2716
+ }),
669
2717
  style: {
670
2718
  fontFamily: MONO,
671
2719
  fontSize: 10,
@@ -812,7 +2860,7 @@ window.__ModuleLoader__.load({
812
2860
  });
813
2861
  } catch (e) {}
814
2862
  };
815
- const outText = cur && cur.output ? cur.output : cur && cur.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${cur.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
2863
+ const outText = cur && cur.output ? cur.output : cur && cur.summary ? t("stage.summaryFallback", { summary: cur.summary }) : det.err ? t("stage.loadFailed", { err: det.err }) : det.loading ? t("common.loading") : t("stage.noOutput");
816
2864
  const closeBtn = {
817
2865
  font: "inherit",
818
2866
  width: 26,
@@ -874,7 +2922,7 @@ window.__ModuleLoader__.load({
874
2922
  textOverflow: "ellipsis",
875
2923
  whiteSpace: "nowrap"
876
2924
  }
877
- }, st ? st.label : "阶段详情"), h("div", { style: {
2925
+ }, st ? stageLabelOf(st) : t("stage.detailTitle")), h("div", { style: {
878
2926
  fontSize: 10.5,
879
2927
  color: T.text2,
880
2928
  marginTop: 1,
@@ -883,7 +2931,7 @@ window.__ModuleLoader__.load({
883
2931
  } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stText(st.status), color, { dot: true }) : null, h("button", {
884
2932
  onClick: onClose,
885
2933
  style: closeBtn,
886
- title: "关闭"
2934
+ title: t("common.close")
887
2935
  }, "✕")), h("div", { style: {
888
2936
  flex: 1,
889
2937
  overflowY: "auto",
@@ -900,7 +2948,7 @@ window.__ModuleLoader__.load({
900
2948
  fontWeight: 700,
901
2949
  color: T.text2,
902
2950
  letterSpacing: .3
903
- } }, "TOKEN · 官方口径"), h("span", { style: {
2951
+ } }, t("token.officialTitle")), h("span", { style: {
904
2952
  fontSize: 11.5,
905
2953
  fontFamily: MONO,
906
2954
  color: T.text,
@@ -914,7 +2962,7 @@ window.__ModuleLoader__.load({
914
2962
  fontWeight: 700,
915
2963
  color: T.text2,
916
2964
  letterSpacing: .3
917
- } }, `↻ 尝试历史(${attempts.length} 次 · 点击查看该次详情)`), h("div", { style: {
2965
+ } }, t("stage.attemptHistory", { n: attempts.length })), h("div", { style: {
918
2966
  display: "flex",
919
2967
  flexDirection: "column",
920
2968
  gap: 4
@@ -945,7 +2993,7 @@ window.__ModuleLoader__.load({
945
2993
  color: aColor,
946
2994
  flex: "0 0 64px",
947
2995
  fontWeight: 700
948
- } }, a.status === "done" ? "✅ 成功" : a.status === "failed" ? `❌ ${a.outcome || "失败"}` : "⏳ 进行中"), h("span", { style: {
2996
+ } }, a.status === "done" ? t("stage.attemptDone") : a.status === "failed" ? t("stage.attemptFailed", { outcome: a.outcome || t("common.failed") }) : t("stage.attemptRunning")), h("span", { style: {
949
2997
  flex: 1,
950
2998
  minWidth: 0,
951
2999
  fontSize: 10.5,
@@ -953,7 +3001,7 @@ window.__ModuleLoader__.load({
953
3001
  overflow: "hidden",
954
3002
  textOverflow: "ellipsis",
955
3003
  whiteSpace: "nowrap"
956
- } }, (a.summary || a.outcome || "(无摘要)").slice(0, 80)), h("span", { style: {
3004
+ } }, (a.summary || a.outcome || t("common.noSummary")).slice(0, 80)), h("span", { style: {
957
3005
  fontFamily: MONO,
958
3006
  fontSize: 10,
959
3007
  color: T.text2,
@@ -971,7 +3019,7 @@ window.__ModuleLoader__.load({
971
3019
  } }, h("button", {
972
3020
  onClick: openChild,
973
3021
  disabled: !hasChild,
974
- title: crossSession ? `该子代理由会话 ${ownerSession.slice(-6)} 发起,跨会话跳转暂不支持——请打开其发起会话的团队工作台查看` : hasChild ? "跳转到该阶段子代理会话(完整推理与工具调用轨迹);跳转后请切换「对话」tab 查看" : "该阶段无可用子代理会话",
3022
+ title: crossSession ? t("stage.childCrossSessionTip", { sid: ownerSession.slice(-6) }) : hasChild ? t("stage.childJumpTip") : t("stage.childNoneTip"),
975
3023
  style: {
976
3024
  font: "inherit",
977
3025
  fontSize: 12,
@@ -988,17 +3036,17 @@ window.__ModuleLoader__.load({
988
3036
  color: T.brand,
989
3037
  opacity: hasChild ? 1 : .45
990
3038
  }
991
- }, "🎬 跳转子代理会话"), crossSession ? h("div", { style: {
3039
+ }, t("stage.childJumpBtn")), crossSession ? h("div", { style: {
992
3040
  fontSize: 10.5,
993
3041
  color: T.text2,
994
3042
  textAlign: "center",
995
3043
  lineHeight: 1.55
996
- } }, `跨会话暂不支持:该子代理由会话 ${ownerSession ? ownerSession.slice(-6) : ""} 发起。如需查看轨迹,请打开其发起会话的团队工作台。`) : hasChild ? h("div", { style: {
3044
+ } }, t("stage.childCrossSessionNote", { sid: ownerSession ? ownerSession.slice(-6) : "" })) : hasChild ? h("div", { style: {
997
3045
  fontSize: 10.5,
998
3046
  color: T.text2,
999
3047
  textAlign: "center",
1000
3048
  lineHeight: 1.55
1001
- } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
3049
+ } }, t("stage.childJumpNote")) : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
1002
3050
  display: "flex",
1003
3051
  flexDirection: "column",
1004
3052
  gap: 5
@@ -1007,7 +3055,7 @@ window.__ModuleLoader__.load({
1007
3055
  fontWeight: 700,
1008
3056
  color: T.text2,
1009
3057
  letterSpacing: .3
1010
- } }, "🔬 验证证据"), cur && cur.verifyEvidence ? h("div", { style: {
3058
+ } }, t("stage.evidenceTitle")), cur && cur.verifyEvidence ? h("div", { style: {
1011
3059
  whiteSpace: "pre-wrap",
1012
3060
  wordBreak: "break-word",
1013
3061
  fontSize: 11.5,
@@ -1028,7 +3076,7 @@ window.__ModuleLoader__.load({
1028
3076
  borderRadius: 10,
1029
3077
  padding: "8px 12px",
1030
3078
  lineHeight: 1.55
1031
- } }, "(缺失——契约未兑现,host 已记警告;可与 logs/teamflow/<runId>/ 命令输出日志对照)")) : null, h("div", { style: {
3079
+ } }, t("stage.evidenceMissing"))) : null, h("div", { style: {
1032
3080
  display: "flex",
1033
3081
  flexDirection: "column",
1034
3082
  gap: 5
@@ -1037,7 +3085,7 @@ window.__ModuleLoader__.load({
1037
3085
  fontWeight: 700,
1038
3086
  color: T.text2,
1039
3087
  letterSpacing: .3
1040
- } }, "📄 阶段性产物"), h("div", { style: {
3088
+ } }, t("stage.artifactsTitle")), h("div", { style: {
1041
3089
  whiteSpace: "pre-wrap",
1042
3090
  wordBreak: "break-word",
1043
3091
  fontSize: 12,
@@ -1060,7 +3108,7 @@ window.__ModuleLoader__.load({
1060
3108
  } }, h("div", { style: {
1061
3109
  fontSize: 28,
1062
3110
  marginBottom: 8
1063
- } }, "🏭"), "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求");
3111
+ } }, "🏭"), t("pipeline.empty"));
1064
3112
  const groups = [];
1065
3113
  const taskKeyOf = (s) => String(s.taskKey || String(s.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
1066
3114
  for (const st of active.stages || []) {
@@ -1261,7 +3309,8 @@ window.__ModuleLoader__.load({
1261
3309
  onMouseLeave: onUp,
1262
3310
  style: {
1263
3311
  position: "relative",
1264
- height: 560,
3312
+ flex: 1,
3313
+ minHeight: 320,
1265
3314
  borderRadius: 12,
1266
3315
  overflow: "hidden",
1267
3316
  touchAction: "none",
@@ -1334,7 +3383,7 @@ window.__ModuleLoader__.load({
1334
3383
  justifyContent: "center",
1335
3384
  color: T.text2,
1336
3385
  fontSize: 13
1337
- } }, "流水线还没有开始执行节点") : null, h("div", {
3386
+ } }, t("pipeline.noNodes")) : null, h("div", {
1338
3387
  onMouseDown: (e) => e.stopPropagation(),
1339
3388
  style: {
1340
3389
  position: "absolute",
@@ -1352,7 +3401,7 @@ window.__ModuleLoader__.load({
1352
3401
  boxShadow: "0 6px 20px rgba(0,0,0,.16)"
1353
3402
  }
1354
3403
  }, h("button", {
1355
- title: "缩小",
3404
+ title: t("pipeline.zoomOut"),
1356
3405
  onClick: () => zoomBy(.86),
1357
3406
  style: zoomStyle
1358
3407
  }, "−"), h("span", { style: {
@@ -1362,7 +3411,7 @@ window.__ModuleLoader__.load({
1362
3411
  minWidth: 34,
1363
3412
  textAlign: "center"
1364
3413
  } }, `${Math.round(view.s * 100)}%`), h("button", {
1365
- title: "放大",
3414
+ title: t("pipeline.zoomIn"),
1366
3415
  onClick: () => zoomBy(1.16),
1367
3416
  style: zoomStyle
1368
3417
  }, "+"), h("span", { style: {
@@ -1370,7 +3419,7 @@ window.__ModuleLoader__.load({
1370
3419
  height: 14,
1371
3420
  background: T.border
1372
3421
  } }), h("button", {
1373
- title: "适应画布",
3422
+ title: t("pipeline.fitCanvas"),
1374
3423
  onClick: fitNow,
1375
3424
  style: {
1376
3425
  ...zoomStyle,
@@ -1385,7 +3434,7 @@ window.__ModuleLoader__.load({
1385
3434
  color: T.text2,
1386
3435
  paddingRight: 4,
1387
3436
  opacity: .85
1388
- } }, "✥ 拖动画布 · 滚轮缩放")), det ? h(StageDetailDrawer, {
3437
+ } }, t("pipeline.canvasHint"))), det ? h(StageDetailDrawer, {
1389
3438
  det,
1390
3439
  onClose: closeDet,
1391
3440
  sessionId,
@@ -1431,12 +3480,12 @@ window.__ModuleLoader__.load({
1431
3480
  } }, h("div", { style: {
1432
3481
  fontSize: 28,
1433
3482
  marginBottom: 8
1434
- } }, "📋"), "backlog 为空(还没有流水线运行过)");
3483
+ } }, "📋"), t("board.empty"));
1435
3484
  const subtaskMap = {};
1436
3485
  for (const t of backlog.tasks || []) if (t.type === "subtask" && t.id) subtaskMap[t.id] = t;
1437
3486
  const move = async (kind, id, to) => {
1438
3487
  try {
1439
- await api.backlogUpdate(kind, id, to, sessionId, "看板拖拽流转");
3488
+ await api.backlogUpdate(kind, id, to, sessionId, t("board.dragReason"));
1440
3489
  } catch (e) {}
1441
3490
  onRefresh();
1442
3491
  };
@@ -1458,33 +3507,52 @@ window.__ModuleLoader__.load({
1458
3507
  padding: "7px 9px",
1459
3508
  cursor: "grab",
1460
3509
  fontSize: 12,
3510
+ boxSizing: "border-box",
3511
+ minWidth: 0,
3512
+ maxWidth: "100%",
3513
+ overflow: "hidden",
1461
3514
  opacity: drag && drag.id === item.id ? .35 : 1,
1462
3515
  transition: "opacity .1s ease, transform .12s ease",
1463
3516
  boxShadow: "0 1px 2px rgba(0,0,0,.05)"
1464
3517
  },
1465
- title: `${item.id} · ${item.status}${item.summary ? "\n" + item.summary : ""}(点击查看详情)`
3518
+ title: t("board.cardTip", {
3519
+ id: item.id,
3520
+ status: item.status,
3521
+ summary: item.summary ? "\n" + item.summary : ""
3522
+ })
1466
3523
  }, h("div", { style: {
1467
3524
  display: "flex",
1468
3525
  alignItems: "center",
1469
- gap: 4
3526
+ gap: 4,
3527
+ minWidth: 0
1470
3528
  } }, h("span", { style: {
1471
3529
  fontFamily: MONO,
1472
3530
  color: T.text2,
1473
- fontSize: 10.5
3531
+ fontSize: 10.5,
3532
+ minWidth: 0,
3533
+ overflow: "hidden",
3534
+ textOverflow: "ellipsis",
3535
+ whiteSpace: "nowrap"
1474
3536
  } }, 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
3537
  marginLeft: "auto",
1476
3538
  fontSize: 10.5,
1477
- color: T.text2
3539
+ color: T.text2,
3540
+ minWidth: 0,
3541
+ overflow: "hidden",
3542
+ textOverflow: "ellipsis",
3543
+ whiteSpace: "nowrap"
1478
3544
  } }, `👤 ${item.owner}`) : null), h("div", {
1479
3545
  title: item.title || void 0,
1480
3546
  style: {
1481
3547
  fontWeight: 500,
1482
3548
  margin: "3px 0 4px",
1483
- lineHeight: 1.4
3549
+ lineHeight: 1.4,
3550
+ overflowWrap: "anywhere"
1484
3551
  }
1485
3552
  }, (item.title || "").slice(0, 30)), h("div", { style: {
1486
3553
  ...flexRow,
1487
- marginTop: 2
3554
+ marginTop: 2,
3555
+ minWidth: 0
1488
3556
  } }, chip(stText(item.status), stColor(item.status)), typeof item.retries === "number" && item.retries > 0 ? h("span", { style: {
1489
3557
  fontSize: 10.5,
1490
3558
  color: T.warn,
@@ -1498,38 +3566,42 @@ window.__ModuleLoader__.load({
1498
3566
  marginTop: 3,
1499
3567
  fontSize: 10.5,
1500
3568
  color: T.text2,
1501
- fontFamily: MONO
3569
+ fontFamily: MONO,
3570
+ minWidth: 0
1502
3571
  } }, item.devAssign ? h("span", {
1503
- title: `dev 分配\n${item.devAssign}`,
3572
+ title: t("board.assignDevTip", { who: item.devAssign }),
1504
3573
  style: {
1505
3574
  display: "inline-flex",
1506
3575
  alignItems: "center",
1507
3576
  gap: 3,
1508
3577
  overflow: "hidden",
1509
3578
  textOverflow: "ellipsis",
1510
- maxWidth: 130,
3579
+ maxWidth: "100%",
3580
+ minWidth: 0,
1511
3581
  whiteSpace: "nowrap"
1512
3582
  }
1513
3583
  }, `👨‍💻${item.devAssign}`) : null, item.qaAssign ? h("span", {
1514
- title: `qa 分配\n${item.qaAssign}`,
3584
+ title: t("board.assignQaTip", { who: item.qaAssign }),
1515
3585
  style: {
1516
3586
  display: "inline-flex",
1517
3587
  alignItems: "center",
1518
3588
  gap: 3,
1519
3589
  overflow: "hidden",
1520
3590
  textOverflow: "ellipsis",
1521
- maxWidth: 130,
3591
+ maxWidth: "100%",
3592
+ minWidth: 0,
1522
3593
  whiteSpace: "nowrap"
1523
3594
  }
1524
3595
  }, `🧪${item.qaAssign}`) : null, item.acceptBy ? h("span", {
1525
- title: `验收/汇报人\n${item.acceptBy}`,
3596
+ title: t("board.acceptTip", { who: item.acceptBy }),
1526
3597
  style: {
1527
3598
  display: "inline-flex",
1528
3599
  alignItems: "center",
1529
3600
  gap: 3,
1530
3601
  overflow: "hidden",
1531
3602
  textOverflow: "ellipsis",
1532
- maxWidth: 130,
3603
+ maxWidth: "100%",
3604
+ minWidth: 0,
1533
3605
  whiteSpace: "nowrap"
1534
3606
  }
1535
3607
  }, `✅${item.acceptBy}`) : null) : null, kind === "task" && byRoleLine(item) ? h("div", { style: {
@@ -1537,8 +3609,21 @@ window.__ModuleLoader__.load({
1537
3609
  marginTop: 3,
1538
3610
  fontSize: 10.5,
1539
3611
  color: T.warn,
1540
- fontFamily: MONO
1541
- } }, h("span", { style: { color: T.text2 } }, "⛽"), byRoleLine(item)) : null, kind === "task" && (item.subtaskIds || []).length > 0 ? (() => {
3612
+ fontFamily: MONO,
3613
+ minWidth: 0,
3614
+ maxWidth: "100%"
3615
+ } }, h("span", { style: {
3616
+ color: T.text2,
3617
+ flex: "0 0 auto"
3618
+ } }, "⛽"), h("span", {
3619
+ title: byRoleLine(item),
3620
+ style: {
3621
+ minWidth: 0,
3622
+ flex: "1 1 auto",
3623
+ overflowWrap: "anywhere",
3624
+ wordBreak: "break-word"
3625
+ }
3626
+ }, byRoleLine(item))) : null, kind === "task" && (item.subtaskIds || []).length > 0 ? (() => {
1542
3627
  const subs = item.subtaskIds.map((id) => subtaskMap[id]).filter(Boolean);
1543
3628
  if (subs.length === 0) return null;
1544
3629
  const done = subs.filter((s) => s.status === "done").length;
@@ -1550,7 +3635,7 @@ window.__ModuleLoader__.load({
1550
3635
  color: T.text2,
1551
3636
  fontFamily: MONO,
1552
3637
  marginBottom: 3
1553
- } }, 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", {
3638
+ } }, h("span", null, t("board.subtaskCount", { n: 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", {
1554
3639
  key: sub.id,
1555
3640
  style: {
1556
3641
  display: "flex",
@@ -1561,29 +3646,33 @@ window.__ModuleLoader__.load({
1561
3646
  padding: "2px 6px",
1562
3647
  borderRadius: 4,
1563
3648
  marginBottom: 2,
3649
+ minWidth: 0,
1564
3650
  background: sub.status === "failed" ? `color-mix(in srgb, ${T.error} 7%, transparent)` : T.layer2,
1565
3651
  border: `1px solid ${stColor(sub.status)}30`
1566
3652
  }
1567
3653
  }, h("span", { style: {
1568
3654
  color: stColor(sub.status),
1569
3655
  fontWeight: 600,
1570
- minWidth: 12
3656
+ minWidth: 12,
3657
+ flex: "0 0 auto"
1571
3658
  } }, sub.status === "done" ? "✓" : sub.status === "failed" ? "✗" : sub.status === "running" ? "⟳" : "…"), h("span", {
1572
3659
  title: sub.title || void 0,
1573
3660
  style: {
3661
+ flex: "1 1 auto",
3662
+ minWidth: 0,
1574
3663
  overflow: "hidden",
1575
3664
  textOverflow: "ellipsis",
1576
- whiteSpace: "nowrap",
1577
- maxWidth: 130
3665
+ whiteSpace: "nowrap"
1578
3666
  }
1579
3667
  }, (sub.title || "").replace(/^开发 · /, "")), sub.devAssign ? h("span", {
1580
- title: `dev 分配\n${sub.devAssign}`,
3668
+ title: t("board.assignDevTip", { who: sub.devAssign }),
1581
3669
  style: {
1582
- marginLeft: "auto",
3670
+ flex: "0 1 auto",
1583
3671
  color: T.text2,
1584
3672
  overflow: "hidden",
1585
3673
  textOverflow: "ellipsis",
1586
3674
  maxWidth: 70,
3675
+ minWidth: 0,
1587
3676
  whiteSpace: "nowrap"
1588
3677
  }
1589
3678
  }, sub.devAssign) : null)));
@@ -1603,24 +3692,35 @@ window.__ModuleLoader__.load({
1603
3692
  }
1604
3693
  ];
1605
3694
  return h("div", { style: {
3695
+ position: "relative",
3696
+ flex: 1,
3697
+ minHeight: 0,
3698
+ display: "flex",
3699
+ flexDirection: "column"
3700
+ } }, h("div", { style: {
3701
+ flex: 1,
3702
+ minHeight: 0,
3703
+ overflowY: "auto",
1606
3704
  display: "flex",
1607
3705
  flexDirection: "column",
1608
3706
  gap: 14,
1609
- maxHeight: "72vh",
1610
- overflowY: "auto",
1611
3707
  paddingRight: 4
1612
3708
  } }, groups.map(({ kind, list }) => {
1613
3709
  const counts = {};
1614
3710
  for (const s of COLUMNS[kind]) counts[s] = 0;
1615
3711
  for (const item of list) counts[item.status] = (counts[item.status] || 0) + 1;
1616
3712
  return h("div", { key: kind }, h("div", { style: {
3713
+ position: "sticky",
3714
+ top: 0,
3715
+ zIndex: 3,
3716
+ background: T.bg,
1617
3717
  display: "flex",
1618
3718
  alignItems: "center",
1619
3719
  gap: 8,
1620
3720
  fontWeight: 700,
1621
3721
  fontSize: 13,
1622
- marginBottom: 8
1623
- } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), KIND_TITLE[kind], h("span", { style: {
3722
+ padding: "6px 0 8px"
3723
+ } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), kindTitle(kind), h("span", { style: {
1624
3724
  fontSize: 11,
1625
3725
  fontWeight: 600,
1626
3726
  color: T.text2,
@@ -1636,6 +3736,7 @@ window.__ModuleLoader__.load({
1636
3736
  } }, COLUMNS[kind].map((s) => {
1637
3737
  const isOver = over === `${kind}:${s}`;
1638
3738
  const color = stColor(s);
3739
+ const colBg = isOver ? `color-mix(in srgb, ${color} 8%, ${T.layer1})` : T.layer2;
1639
3740
  return h("div", {
1640
3741
  key: s,
1641
3742
  onDragOver: (e) => {
@@ -1653,11 +3754,12 @@ window.__ModuleLoader__.load({
1653
3754
  maxWidth: 172,
1654
3755
  flex: "0 0 auto",
1655
3756
  borderRadius: 10,
1656
- padding: 7,
3757
+ padding: 0,
1657
3758
  minHeight: 84,
1658
3759
  maxHeight: 340,
1659
3760
  overflowY: "auto",
1660
- background: isOver ? `color-mix(in srgb, ${color} 8%, ${T.layer1})` : T.layer2,
3761
+ overflowX: "hidden",
3762
+ background: colBg,
1661
3763
  border: `1px dashed ${isOver ? color : T.border}`,
1662
3764
  transition: "background .12s ease, border-color .12s ease"
1663
3765
  }
@@ -1666,8 +3768,13 @@ window.__ModuleLoader__.load({
1666
3768
  fontSize: 11,
1667
3769
  fontWeight: 600,
1668
3770
  color,
1669
- marginBottom: 6,
1670
- padding: "0 2px"
3771
+ position: "sticky",
3772
+ top: 0,
3773
+ zIndex: 2,
3774
+ background: colBg,
3775
+ padding: "7px 9px 5px",
3776
+ borderTopLeftRadius: 10,
3777
+ borderTopRightRadius: 10
1671
3778
  } }, h("span", null, stText(s)), h("span", { style: {
1672
3779
  marginLeft: "auto",
1673
3780
  fontFamily: MONO,
@@ -1676,10 +3783,12 @@ window.__ModuleLoader__.load({
1676
3783
  } }, counts[s] || 0)), h("div", { style: {
1677
3784
  display: "flex",
1678
3785
  flexDirection: "column",
1679
- gap: 6
3786
+ gap: 6,
3787
+ padding: "0 7px 7px",
3788
+ minWidth: 0
1680
3789
  } }, list.filter((item) => item.status === s).map((item) => card(item, kind))));
1681
3790
  })));
1682
- }), det ? h(ItemDetailDrawer, {
3791
+ })), det ? h(ItemDetailDrawer, {
1683
3792
  det,
1684
3793
  onClose: () => setDet(null),
1685
3794
  onShowRun,
@@ -1687,7 +3796,7 @@ window.__ModuleLoader__.load({
1687
3796
  }) : null);
1688
3797
  }
1689
3798
  function fmtAt(ts) {
1690
- return ts ? new Date(ts).toLocaleTimeString("zh-CN", {
3799
+ return ts ? new Date(ts).toLocaleTimeString(localeTag(), {
1691
3800
  hour: "2-digit",
1692
3801
  minute: "2-digit"
1693
3802
  }) : "—";
@@ -1765,7 +3874,7 @@ window.__ModuleLoader__.load({
1765
3874
  color: T.warn,
1766
3875
  flex: "0 0 auto"
1767
3876
  } }, `⛽${fmtTokens((extra.usage.input || 0) + (extra.usage.cacheRead || 0) + (extra.usage.cacheWrite || 0) + (extra.usage.output || 0))}`) : null, extra && extra.assignee ? h("span", {
1768
- title: `dev 分配\n${extra.assignee}`,
3877
+ title: t("board.assignDevTip", { who: extra.assignee }),
1769
3878
  style: {
1770
3879
  display: "inline-flex",
1771
3880
  alignItems: "center",
@@ -1844,7 +3953,7 @@ window.__ModuleLoader__.load({
1844
3953
  } }, `${d.id} · ${d.kind}${d.severity ? " · " + d.severity : ""}`) : null), d ? chip(stText(d.status), color, { dot: true }) : null, h("button", {
1845
3954
  onClick: onClose,
1846
3955
  style: closeBtn,
1847
- title: "关闭"
3956
+ title: t("common.close")
1848
3957
  }, "✕")), h("div", { style: {
1849
3958
  flex: 1,
1850
3959
  overflowY: "auto",
@@ -1856,7 +3965,7 @@ window.__ModuleLoader__.load({
1856
3965
  color: T.text2,
1857
3966
  fontSize: 12,
1858
3967
  padding: 12
1859
- } }, "加载中…") : err ? h("div", { style: {
3968
+ } }, t("common.loading")) : err ? h("div", { style: {
1860
3969
  color: T.error,
1861
3970
  fontSize: 12,
1862
3971
  padding: 12
@@ -1865,7 +3974,7 @@ window.__ModuleLoader__.load({
1865
3974
  display: "flex",
1866
3975
  flexDirection: "column",
1867
3976
  gap: 4
1868
- } }, secTitle("概览"), d.spec ? h(FoldableText, {
3977
+ } }, secTitle(t("item.overview")), d.spec ? h(FoldableText, {
1869
3978
  text: d.spec,
1870
3979
  charLimit: 300,
1871
3980
  lineLimit: 4,
@@ -1883,7 +3992,28 @@ window.__ModuleLoader__.load({
1883
3992
  flexDirection: "column",
1884
3993
  gap: 2,
1885
3994
  marginTop: 2
1886
- } }, 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) || "—"))),
3995
+ } }, 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(t("item.acceptRow"), 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(t("item.retryRow"), String(d.retries)) : null, d.humanIntervention ? kv(t("item.humanRow"), t("common.needsHuman")) : null, kv(t("item.updatedAt"), fmtAt(d.updatedAt) || "—"))),
3996
+ kind === "bug" ? h("div", { style: {
3997
+ display: "flex",
3998
+ flexDirection: "column",
3999
+ gap: 4,
4000
+ padding: "8px 10px",
4001
+ borderRadius: 8,
4002
+ background: T.layer2,
4003
+ border: `1px solid ${T.border}`
4004
+ } }, secTitle(t("panelItem.defectSection")), d.spec ? h(FoldableText, {
4005
+ text: d.spec,
4006
+ charLimit: 300,
4007
+ lineLimit: 4
4008
+ }) : null, h("div", { style: {
4009
+ display: "flex",
4010
+ flexDirection: "column",
4011
+ gap: 2
4012
+ } }, d.defectId ? kv(t("panelItem.row.defectId"), String(d.defectId)) : null, d.severity ? kv(t("panelItem.row.severity"), String(d.severity)) : null, d.module ? kv(t("panelItem.row.module"), String(d.module)) : null, d.reproduce ? kv(t("panelItem.row.reproduce"), String(d.reproduce)) : null, d.expected ? kv(t("panelItem.row.expected"), String(d.expected)) : null, d.actual ? kv(t("panelItem.row.actual"), String(d.actual)) : null, d.defectCheck ? kv(t("panelItem.row.defectCheck"), String(d.defectCheck)) : null, d.defectCriterion ? kv(t("panelItem.row.defectCriterion"), String(d.defectCriterion)) : null, d.defectAc ? kv(t("panelItem.row.defectAc"), String(d.defectAc)) : null), d.reproduce || d.expected || d.actual ? null : h("div", { style: {
4013
+ fontSize: 10.5,
4014
+ color: T.warn,
4015
+ lineHeight: 1.5
4016
+ } }, t("panelItem.noDefectDetail"))) : null,
1887
4017
  d.runInfo ? (() => {
1888
4018
  const ri = d.runInfo;
1889
4019
  return h("div", { style: {
@@ -1898,14 +4028,14 @@ window.__ModuleLoader__.load({
1898
4028
  display: "flex",
1899
4029
  alignItems: "center",
1900
4030
  gap: 8
1901
- } }, secTitle("运行"), h("span", { style: {
4031
+ } }, secTitle(t("item.runSection")), h("span", { style: {
1902
4032
  fontFamily: MONO,
1903
4033
  fontSize: 10.5,
1904
4034
  color: T.text2,
1905
4035
  whiteSpace: "nowrap"
1906
4036
  } }, ri.runId), chip(stText(ri.status), stColor(ri.status)), onShowRun ? h("button", {
1907
4037
  onClick: () => onShowRun(ri.runId),
1908
- title: `跳转到该需求的流水线视图 #${String(ri.runId).slice(-6)}`,
4038
+ title: t("item.jumpRunTip", { id: String(ri.runId).slice(-6) }),
1909
4039
  style: {
1910
4040
  marginLeft: "auto",
1911
4041
  flex: "0 0 auto",
@@ -1920,7 +4050,7 @@ window.__ModuleLoader__.load({
1920
4050
  color: T.brand,
1921
4051
  lineHeight: "16px"
1922
4052
  }
1923
- }, "▶ 流水线") : null), ri.startedAt || ri.endedAt ? h("span", { style: {
4053
+ }, t("item.runBtn")) : null), ri.startedAt || ri.endedAt ? h("span", { style: {
1924
4054
  fontSize: 10.5,
1925
4055
  color: T.text2,
1926
4056
  fontFamily: MONO
@@ -1928,8 +4058,8 @@ window.__ModuleLoader__.load({
1928
4058
  display: "flex",
1929
4059
  flexDirection: "column",
1930
4060
  gap: 4
1931
- } }, secTitle("需求原文"), h(FoldableText, {
1932
- text: ri.requirement || "(无原文)",
4061
+ } }, secTitle(t("item.requirement")), h(FoldableText, {
4062
+ text: ri.requirement || t("item.noRequirement"),
1933
4063
  charLimit: 300,
1934
4064
  lineLimit: 4,
1935
4065
  style: {
@@ -1943,7 +4073,7 @@ window.__ModuleLoader__.load({
1943
4073
  display: "flex",
1944
4074
  flexDirection: "column",
1945
4075
  gap: 4
1946
- } }, secTitle("任务夹"), h("div", { style: {
4076
+ } }, secTitle(t("item.runDocs")), h("div", { style: {
1947
4077
  fontSize: 11.5,
1948
4078
  fontFamily: MONO,
1949
4079
  color: T.brand,
@@ -1961,7 +4091,7 @@ window.__ModuleLoader__.load({
1961
4091
  onClick: () => {
1962
4092
  if (openArtifact) openArtifact(a.address, a.name);
1963
4093
  },
1964
- title: `在右侧栏预览 ${d.runDocs}/${a.name}`,
4094
+ title: t("item.previewTip", { path: `${d.runDocs}/${a.name}` }),
1965
4095
  style: {
1966
4096
  fontSize: 10.5,
1967
4097
  fontWeight: 600,
@@ -1979,7 +4109,7 @@ window.__ModuleLoader__.load({
1979
4109
  display: "flex",
1980
4110
  flexDirection: "column",
1981
4111
  gap: 4
1982
- } }, secTitle("TOKEN · 官方口径"), h("span", { style: {
4112
+ } }, secTitle(t("token.officialTitle")), h("span", { style: {
1983
4113
  fontSize: 11.5,
1984
4114
  fontFamily: MONO,
1985
4115
  color: T.text,
@@ -1991,7 +4121,7 @@ window.__ModuleLoader__.load({
1991
4121
  marginTop: 2
1992
4122
  } }, Object.entries(d.byRole).sort((a, b) => totalTokens(b[1]) - totalTokens(a[1])).map(([role, uRaw]) => {
1993
4123
  const u = uRaw;
1994
- const label = role === "dev" ? "👨‍💻 开发" : role === "qa" ? "🧪 QA" : role === "acceptance" ? "✅ 验收" : role === "pm" ? "📌 产品" : role === "design" ? "🎨 设计" : role === "arch" ? "🏗 架构" : "⚙️ " + role;
4124
+ const label = roleChip(role);
1995
4125
  return h("div", {
1996
4126
  key: role,
1997
4127
  style: {
@@ -2002,13 +4132,13 @@ window.__ModuleLoader__.load({
2002
4132
  fontFamily: MONO,
2003
4133
  color: T.text2
2004
4134
  }
2005
- }, 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} 次`));
4135
+ }, 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 · ${t("common.calls", { n: u.calls || 0 })}`));
2006
4136
  })) : null) : null,
2007
4137
  d.subtasks && d.subtasks.length > 0 ? h("div", { style: {
2008
4138
  display: "flex",
2009
4139
  flexDirection: "column",
2010
4140
  gap: 4
2011
- } }, secTitle(`关联子卡(${d.subtasks.length})`), h("div", { style: {
4141
+ } }, secTitle(t("item.subtasks", { n: d.subtasks.length })), h("div", { style: {
2012
4142
  display: "flex",
2013
4143
  flexDirection: "column",
2014
4144
  gap: 5
@@ -2020,7 +4150,7 @@ window.__ModuleLoader__.load({
2020
4150
  display: "flex",
2021
4151
  flexDirection: "column",
2022
4152
  gap: 4
2023
- } }, secTitle(`关联缺陷(${d.bugs.length})`), h("div", { style: {
4153
+ } }, secTitle(t("item.bugs", { n: d.bugs.length })), h("div", { style: {
2024
4154
  display: "flex",
2025
4155
  flexDirection: "column",
2026
4156
  gap: 5
@@ -2029,7 +4159,7 @@ window.__ModuleLoader__.load({
2029
4159
  display: "flex",
2030
4160
  flexDirection: "column",
2031
4161
  gap: 4
2032
- } }, secTitle(`流转时间线(${d.events.length})`), h("div", { style: {
4162
+ } }, secTitle(t("item.timeline", { n: d.events.length })), h("div", { style: {
2033
4163
  display: "flex",
2034
4164
  flexDirection: "column",
2035
4165
  gap: 0
@@ -2065,7 +4195,7 @@ window.__ModuleLoader__.load({
2065
4195
  }, ev.reason || ""))))) : null
2066
4196
  ]));
2067
4197
  }
2068
- function TeamSelector({ sessionId, remote }) {
4198
+ function TeamSelector({ sessionId, remote, locale }) {
2069
4199
  const [teams, setTeams] = react.default.useState([]);
2070
4200
  const [active, setActive] = react.default.useState(null);
2071
4201
  const [open, setOpen] = react.default.useState(false);
@@ -2078,7 +4208,11 @@ window.__ModuleLoader__.load({
2078
4208
  const at = unwrap(await remote.getActiveTeam(sessionId), "getActiveTeam");
2079
4209
  setActive(at && at.team ? at.team : null);
2080
4210
  } catch (e) {}
2081
- }, [remote, sessionId]);
4211
+ }, [
4212
+ remote,
4213
+ sessionId,
4214
+ locale
4215
+ ]);
2082
4216
  react.default.useEffect(() => {
2083
4217
  load();
2084
4218
  }, [load]);
@@ -2109,7 +4243,7 @@ window.__ModuleLoader__.load({
2109
4243
  style: { position: "relative" }
2110
4244
  }, h("button", {
2111
4245
  onClick: () => setOpen(!open),
2112
- title: active ? `当前团队:${active.name}(点击切换)` : "选择团队",
4246
+ title: active ? t("team.currentTip", { name: active.name }) : t("team.pick"),
2113
4247
  style: {
2114
4248
  display: "inline-flex",
2115
4249
  alignItems: "center",
@@ -2126,14 +4260,14 @@ window.__ModuleLoader__.load({
2126
4260
  transition: "all .12s ease"
2127
4261
  }
2128
4262
  }, h("span", { style: { fontSize: 12 } }, active ? active.icon : "🏭"), h("span", {
2129
- title: active && active.name ? String(active.name) : "团队",
4263
+ title: active && active.name ? String(active.name) : t("team.label"),
2130
4264
  style: {
2131
4265
  maxWidth: 80,
2132
4266
  overflow: "hidden",
2133
4267
  textOverflow: "ellipsis",
2134
4268
  whiteSpace: "nowrap"
2135
4269
  }
2136
- }, active ? active.name : "团队"), h("span", { style: {
4270
+ }, active ? active.name : t("team.label")), h("span", { style: {
2137
4271
  fontSize: 8,
2138
4272
  opacity: .6
2139
4273
  } }, open ? "▲" : "▼")), open ? h("div", { style: {
@@ -2153,7 +4287,7 @@ window.__ModuleLoader__.load({
2153
4287
  fontSize: 10,
2154
4288
  color: T.text2,
2155
4289
  borderBottom: `1px solid ${T.border}`
2156
- } }, "选择团队"), h("button", {
4290
+ } }, t("team.pick")), h("button", {
2157
4291
  onClick: () => select(null),
2158
4292
  style: {
2159
4293
  display: "flex",
@@ -2182,14 +4316,14 @@ window.__ModuleLoader__.load({
2182
4316
  } }, h("div", { style: {
2183
4317
  fontWeight: 600,
2184
4318
  lineHeight: 1.35
2185
- } }, "无团队(直接对话)"), h("div", { style: {
4319
+ } }, t("team.none")), h("div", { style: {
2186
4320
  fontSize: 10.5,
2187
4321
  color: T.text2,
2188
4322
  marginTop: 3,
2189
4323
  lineHeight: 1.45,
2190
4324
  whiteSpace: "normal",
2191
4325
  wordBreak: "break-word"
2192
- } }, "不走 teamflow,模型直接工作")), !active ? h("span", { style: {
4326
+ } }, t("team.noneNote"))), !active ? h("span", { style: {
2193
4327
  marginLeft: "auto",
2194
4328
  color: T.text2,
2195
4329
  fontSize: 12,
@@ -2238,7 +4372,10 @@ window.__ModuleLoader__.load({
2238
4372
  }
2239
4373
  /** 解包 remote 信封:失败抛错;成功返回 value。 */
2240
4374
  function unwrap(res, what) {
2241
- if (!res || !res.ok) throw new Error(`${what || "remote"} 调用失败:${res && res.error && (res.error.message || res.error.code) || "未知错误"}`);
4375
+ if (!res || !res.ok) throw new Error(t("common.remoteCallFailed", {
4376
+ what: what || "remote",
4377
+ detail: res && res.error && (res.error.message || res.error.code) || t("common.unknownError")
4378
+ }));
2242
4379
  return res.value;
2243
4380
  }
2244
4381
  function TeamFlowView(props) {
@@ -2257,7 +4394,7 @@ window.__ModuleLoader__.load({
2257
4394
  if (!api) {
2258
4395
  setState((s) => ({
2259
4396
  ...s,
2260
- err: "remote 未就绪"
4397
+ err: t("common.remoteNotReady")
2261
4398
  }));
2262
4399
  return;
2263
4400
  }
@@ -2360,7 +4497,10 @@ window.__ModuleLoader__.load({
2360
4497
  display: "flex",
2361
4498
  flexDirection: "column",
2362
4499
  gap: 10,
2363
- padding: "4px 16px"
4500
+ padding: "4px 16px 10px",
4501
+ height: "100%",
4502
+ minHeight: 0,
4503
+ overflow: "hidden"
2364
4504
  } }, h("div", { style: {
2365
4505
  display: "flex",
2366
4506
  alignItems: "center",
@@ -2383,7 +4523,7 @@ window.__ModuleLoader__.load({
2383
4523
  fontWeight: 700,
2384
4524
  fontSize: 14,
2385
4525
  lineHeight: "18px"
2386
- } }, "团队工作台"), h("span", { style: {
4526
+ } }, t("workbench.title")), h("span", { style: {
2387
4527
  fontSize: 11,
2388
4528
  color: T.text2,
2389
4529
  display: "flex",
@@ -2396,7 +4536,7 @@ window.__ModuleLoader__.load({
2396
4536
  display: "inline-block",
2397
4537
  background: anyRunning ? T.success : T.text2,
2398
4538
  animation: anyRunning ? "tf-pulse 1.6s ease-in-out infinite" : "none"
2399
- } }), anyRunning ? "流水线运行中" : "空闲")), h("button", {
4539
+ } }), anyRunning ? t("workbench.running") : t("workbench.idle"))), h("button", {
2400
4540
  onClick: refresh,
2401
4541
  style: {
2402
4542
  ...btn,
@@ -2405,10 +4545,13 @@ window.__ModuleLoader__.load({
2405
4545
  alignItems: "center",
2406
4546
  gap: 5
2407
4547
  }
2408
- }, "🔄 刷新"), canResume ? h("button", {
4548
+ }, t("workbench.refresh")), canResume ? h("button", {
2409
4549
  onClick: onResume,
2410
4550
  disabled: busy,
2411
- title: `断点续跑 ${activeRun.id}\n当前状态:${RUN_STATUS_TEXT[activeRun.status] || activeRun.status};跳过已完成阶段,从第一个未完成阶段重跑`,
4551
+ title: t("workbench.resumeTip", {
4552
+ id: activeRun.id,
4553
+ status: runStatusText(activeRun.status)
4554
+ }),
2412
4555
  style: {
2413
4556
  ...btn,
2414
4557
  background: T.error,
@@ -2416,14 +4559,14 @@ window.__ModuleLoader__.load({
2416
4559
  border: "none",
2417
4560
  fontWeight: 600
2418
4561
  }
2419
- }, busy ? "续跑中…" : `↻ 从断点重跑 #${String(activeRun.id).slice(-6)}`) : null), err ? h("div", { style: {
4562
+ }, busy ? t("workbench.resuming") : t("workbench.resumeBtn", { id: String(activeRun.id).slice(-6) })) : null), err ? h("div", { style: {
2420
4563
  color: T.error,
2421
4564
  fontSize: 12,
2422
4565
  background: `color-mix(in srgb, ${T.error} 8%, transparent)`,
2423
4566
  border: `1px solid color-mix(in srgb, ${T.error} 30%, transparent)`,
2424
4567
  borderRadius: 8,
2425
4568
  padding: "7px 11px"
2426
- } }, `⚠ ${err}(确认已安装 dsh-plugin-teamflow 且 web 已重启)`) : null, needHuman.length > 0 ? h("div", { style: {
4569
+ } }, t("workbench.loadFailed", { err })) : null, needHuman.length > 0 ? h("div", { style: {
2427
4570
  display: "flex",
2428
4571
  alignItems: "center",
2429
4572
  gap: 10,
@@ -2436,13 +4579,13 @@ window.__ModuleLoader__.load({
2436
4579
  fontWeight: 700,
2437
4580
  color: T.warn,
2438
4581
  fontSize: 12.5
2439
- } }, `⚠ ${needHuman.length} 项需人工介入`), needHuman.slice(0, 5).map((item) => {
4582
+ } }, t("workbench.needsHumanBanner", { n: needHuman.length })), needHuman.slice(0, 5).map((item) => {
2440
4583
  const kind = (backlog.requirements || []).some((r) => r.id === item.id) ? "req" : (backlog.tasks || []).some((t) => t.id === item.id) ? "task" : "bug";
2441
4584
  const fin = kind === "bug" ? "verified" : "accepted";
2442
4585
  return h("button", {
2443
4586
  key: item.id,
2444
4587
  onClick: async () => {
2445
- await api.backlogUpdate(kind, item.id, fin, props.sessionId, "人工处理");
4588
+ await api.backlogUpdate(kind, item.id, fin, props.sessionId, t("workbench.manualReason"));
2446
4589
  refresh();
2447
4590
  },
2448
4591
  style: {
@@ -2452,7 +4595,7 @@ window.__ModuleLoader__.load({
2452
4595
  border: "none",
2453
4596
  fontWeight: 600
2454
4597
  }
2455
- }, `处理 ${item.id}`);
4598
+ }, t("workbench.handle", { id: item.id }));
2456
4599
  })) : null, h("div", { style: {
2457
4600
  display: "flex",
2458
4601
  alignItems: "center",
@@ -2461,10 +4604,10 @@ window.__ModuleLoader__.load({
2461
4604
  } }, h("button", {
2462
4605
  onClick: () => setTab("pipeline"),
2463
4606
  style: tabBtn(tab === "pipeline")
2464
- }, "🔄 流水线"), h("button", {
4607
+ }, t("workbench.tabPipeline")), h("button", {
2465
4608
  onClick: () => setTab("board"),
2466
4609
  style: tabBtn(tab === "board")
2467
- }, "📋 Backlog 看板"), h("div", { style: {
4610
+ }, t("workbench.tabBoard")), h("div", { style: {
2468
4611
  marginLeft: "auto",
2469
4612
  display: "flex",
2470
4613
  alignItems: "center",
@@ -2480,14 +4623,14 @@ window.__ModuleLoader__.load({
2480
4623
  color: activeRun.status === "interrupted" ? T.warn : T.text2,
2481
4624
  border: `1px solid ${T.border}`
2482
4625
  }
2483
- }, `#${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", {
4626
+ }, `#${String(activeRun.id).slice(-8)} · ${runStatusText(activeRun.status)}`) : null, total && total.input + total.cacheRead + total.cacheWrite + total.output > 0 ? h("span", {
2484
4627
  style: {
2485
4628
  fontSize: 11.5,
2486
4629
  fontFamily: MONO,
2487
4630
  color: T.text2,
2488
4631
  cursor: "help"
2489
4632
  },
2490
- title: "输入(未命中)/输入(命中)/输出 全部阶段合计"
4633
+ title: t("token.allStagesTip")
2491
4634
  }, `∑ ⇅${fmtTokens(total.input)}/⇅${fmtTokens(total.cacheRead)}·⬆${fmtTokens(total.output)}`) : null)), h("div", { style: {
2492
4635
  display: "flex",
2493
4636
  alignItems: "center",
@@ -2495,7 +4638,7 @@ window.__ModuleLoader__.load({
2495
4638
  fontSize: 12,
2496
4639
  flexWrap: "wrap"
2497
4640
  } }, h("span", {
2498
- title: `当前工作区(workspace 级隔离):${workspace && workspace.path || "未连接工作区"}`,
4641
+ title: t("workbench.workspaceTip", { path: workspace && workspace.path || t("workbench.noWorkspace") }),
2499
4642
  style: {
2500
4643
  display: "inline-flex",
2501
4644
  alignItems: "center",
@@ -2520,7 +4663,7 @@ window.__ModuleLoader__.load({
2520
4663
  display: "flex",
2521
4664
  alignItems: "center",
2522
4665
  gap: 5
2523
- } }, h("span", { style: { color: T.text2 } }, "历史"), runs.length ? runs.map((r) => {
4666
+ } }, h("span", { style: { color: T.text2 } }, t("workbench.history")), runs.length ? runs.map((r) => {
2524
4667
  const sel = r.id === (runId || runs[0] && runs[0].id);
2525
4668
  return h("button", {
2526
4669
  key: r.id,
@@ -2531,7 +4674,18 @@ window.__ModuleLoader__.load({
2531
4674
  }) : h("span", { style: {
2532
4675
  color: T.text2,
2533
4676
  fontSize: 11.5
2534
- } }, "(暂无)")) : null), tab === "pipeline" ? h(PipelinePanel, {
4677
+ } }, t("common.noneDash"))) : null, tab === "pipeline" && activeRun && activeRun.address ? h("button", {
4678
+ style: chipBtn(false),
4679
+ title: t("workbench.openRightBarTip"),
4680
+ onClick: () => {
4681
+ if (!(props.openResource && props.openResource(activeRun.address, activeRun.id))) console.warn("[teamflow] 右侧栏不可用,run 详情请在画布节点里查看");
4682
+ }
4683
+ }, t("workbench.openRightBarBtn")) : null), h("div", { style: {
4684
+ flex: 1,
4685
+ minHeight: 0,
4686
+ display: "flex",
4687
+ flexDirection: "column"
4688
+ } }, tab === "pipeline" ? h(PipelinePanel, {
2535
4689
  active,
2536
4690
  api,
2537
4691
  runId: activeRun ? activeRun.id : null,
@@ -2547,7 +4701,7 @@ window.__ModuleLoader__.load({
2547
4701
  setTab("pipeline");
2548
4702
  setRunId(rid);
2549
4703
  }
2550
- }));
4704
+ })));
2551
4705
  }
2552
4706
  if (typeof document !== "undefined") {
2553
4707
  const styleEl = document.createElement("style");
@@ -2557,41 +4711,109 @@ window.__ModuleLoader__.load({
2557
4711
  @keyframes tf-flow { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -28 } }`;
2558
4712
  document.head.appendChild(styleEl);
2559
4713
  }
2560
- /** 注册 conversation.view tab「团队工作台」+ input.right 团队选择按钮。 */
4714
+ /**
4715
+ * 注册三处:
4716
+ * 1. `sidebar.panellist` + `main`(key = 'teamflow'):**全局面板**——侧边栏图标 + 中央主区。
4717
+ * 两者 id 必须一致:宿主 `layout.selectPanel(id)` 对未注册的 main key 直接抛错。
4718
+ * 2. `conversation.view` tab(会话内工作台)+ `conversation.input.right`(团队选择按钮)。
4719
+ * 3. 右栏 run 详情 tab 类型 + body(`sidebarRightTabs` + `sidebar.right.pane.tab`)。
4720
+ */
2561
4721
  async function apply(ctx) {
2562
4722
  await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
2563
4723
  const teamflow = ctx.get("remote.teamflow");
2564
- const openArtifact = (address, name) => {
4724
+ const t = ctx.locale.bind(NS);
4725
+ ctx.effect(() => ctx.locale.register(NS, {
4726
+ zh,
4727
+ en
4728
+ }), "teamflow: dictionaries");
4729
+ ctx.effect(() => ctx.locale.subscribe(() => setTranslator(t, () => ctx.locale.getSnapshot().active)), "teamflow: translator sync");
4730
+ setTranslator(t, () => ctx.locale.getSnapshot().active);
4731
+ const pushLocaleToHost = (active) => {
4732
+ try {
4733
+ if (typeof active !== "string" || !active) return;
4734
+ const p = teamflow.setLocale(active);
4735
+ if (p && typeof p.catch === "function") p.catch(() => {});
4736
+ } catch (e) {}
4737
+ };
4738
+ ctx.effect(() => {
4739
+ pushLocaleToHost(ctx.locale.getSnapshot().active);
4740
+ return ctx.locale.subscribe(() => pushLocaleToHost(ctx.locale.getSnapshot().active));
4741
+ }, "teamflow: host locale push");
4742
+ const openResourceSafe = (address, label, quiet) => {
2565
4743
  try {
2566
4744
  const sidebarRight = ctx.get("sidebarRight");
2567
4745
  if (!sidebarRight || typeof sidebarRight.openResource !== "function") {
2568
- console.warn("[teamflow] 右侧栏服务不可用,无法预览产物", name);
2569
- return;
4746
+ if (!quiet) console.warn("[teamflow] 右侧栏服务不可用", label);
4747
+ return false;
2570
4748
  }
2571
4749
  sidebarRight.openResource(address);
4750
+ return true;
2572
4751
  } catch (e) {
2573
- console.warn("[teamflow] 打开产物失败", name, e && e.message);
4752
+ if (!quiet) console.warn("[teamflow] 打开右侧栏失败", label, e && e.message);
4753
+ return false;
2574
4754
  }
2575
4755
  };
4756
+ const openArtifact = (address, name) => {
4757
+ openResourceSafe(address, name);
4758
+ };
4759
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
4760
+ name: "sidebar.panellist",
4761
+ id: "teamflow",
4762
+ order: 60,
4763
+ locale: NS,
4764
+ label: () => t("workbench.title")
4765
+ }, TeamflowPanelIcon));
4766
+ ctx.slots.inject("main", () => ctx.slots.register({
4767
+ name: "main",
4768
+ key: "teamflow",
4769
+ locale: NS,
4770
+ inject: () => ({
4771
+ remote: teamflow,
4772
+ sessions: ctx.get("sessions"),
4773
+ layout: ctx.get("layout"),
4774
+ openResource: openResourceSafe,
4775
+ openArtifact
4776
+ })
4777
+ }, GlobalPanel));
4778
+ try {
4779
+ const tabs = ctx.get("sidebarRightTabs");
4780
+ if (tabs && typeof tabs.register === "function") ctx.effect(() => tabs.register(runTabDefinition()), "teamflow: run tab type");
4781
+ else console.warn("[teamflow] sidebarRightTabs 不可用,run 详情右栏 tab 未注册(面板内联详情仍可用)");
4782
+ } catch (e) {
4783
+ console.warn("[teamflow] 注册 run tab 类型失败", e && e.message);
4784
+ }
4785
+ ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
4786
+ name: "sidebar.right.pane.tab",
4787
+ key: RUN_TAB_ID,
4788
+ locale: NS,
4789
+ inject: () => ({
4790
+ remote: teamflow,
4791
+ openArtifact
4792
+ })
4793
+ }, RunDetailTab));
2576
4794
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
2577
4795
  name: "conversation.view",
2578
4796
  id: "teamflow",
2579
4797
  order: 20,
2580
- label: "🏭 团队工作台",
4798
+ locale: NS,
4799
+ label: () => `🏭 ${t("workbench.title")}`,
2581
4800
  inject: (sessionId) => ({
2582
4801
  sessionId,
2583
4802
  remote: teamflow,
2584
4803
  sessions: ctx.get("sessions"),
2585
- openArtifact
4804
+ openArtifact,
4805
+ openResource: openResourceSafe
2586
4806
  })
2587
4807
  }, TeamFlowView));
2588
4808
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2589
4809
  name: "conversation.input.right",
2590
4810
  id: "teamflow-team-select",
2591
4811
  order: 5,
4812
+ locale: NS,
2592
4813
  inject: (sessionId) => ({
2593
4814
  sessionId,
2594
- remote: teamflow
4815
+ remote: teamflow,
4816
+ locale: ctx.locale.getSnapshot().active
2595
4817
  })
2596
4818
  }, TeamSelector));
2597
4819
  }