dsh-plugin-teamflow 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -29,11 +29,15 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  //#region descriptors.ts
32
+ /** 恒等 parse:接受任意 JSON 值,原样返回。 */
33
+ const JSON_SCHEMA = { parse: (value) => value };
32
34
  /** 统一 strict codec(本插件所有参数/结果均为自由 JSON)。 */
33
35
  const strict = {
34
36
  mode: "strict",
35
37
  typeSymbol: "dsh-plugin-teamflow/types#Json",
36
- schema: { parse: (value) => value }
38
+ schema: JSON_SCHEMA,
39
+ /** 首次跨边界使用时物化 schema(宿主按需调用,只调一次并缓存)。 */
40
+ create: () => JSON_SCHEMA
37
41
  };
38
42
  const p = (name) => ({
39
43
  name,
@@ -51,6 +55,15 @@ window.__ModuleLoader__.load({
51
55
  parameters: [],
52
56
  result: strict
53
57
  },
58
+ {
59
+ id: "dsh-plugin-teamflow#teamflow/setLocale",
60
+ service: "teamflow",
61
+ namespace: "teamflow",
62
+ method: "setLocale",
63
+ invocation: { kind: "direct" },
64
+ parameters: [p("locale")],
65
+ result: strict
66
+ },
54
67
  {
55
68
  id: "dsh-plugin-teamflow#teamflow/list",
56
69
  service: "teamflow",
@@ -287,7 +300,60 @@ window.__ModuleLoader__.load({
287
300
  * 会话内工作台(index.tsx)与全局面板(panel.tsx)共用:**只放无状态纯展示件**,
288
301
  * 不放任何 Remote 调用或会话上下文逻辑——两边数据来源不同(sessionId / productKey),
289
302
  * 展示语言必须一致。
303
+ *
304
+ * 双语(v0.1.9):文案统一走宿主 locale 服务(机制说明见 client/locales.ts)。
305
+ * 组件里能拿到注入的 `t`,但**词表/格式化/折叠件是纯函数**,拿不到 prop,
306
+ * 故由 `apply()` 调 `setTranslator()` 注入模块级翻译函数:`bind()` 每次调用都读当前语言,
307
+ * 且宿主在切语言时会重渲染每个 slot outlet(ui-renderer `useLocaleRevision`),
308
+ * 因此模块级函数不会持有过期语言。
309
+ */
310
+ /** 翻译函数(默认恒等:未注入时显示 key 本身,便于发现漏注册)。 */
311
+ let translate = (key) => key;
312
+ /** 当前语言 id 的读取器(默认 en)。 */
313
+ let localeIdOf = () => "en";
314
+ /**
315
+ * 注入翻译函数与语言读取器(apply 时调用一次)。
316
+ * @param fn - `ctx.locale.bind(NS)` 的返回值(调用时读当前语言)。
317
+ * @param idOf - 返回当前语言 id 的函数(如 `() => ctx.locale.getSnapshot().active`)。
290
318
  */
319
+ function setTranslator(fn, idOf) {
320
+ if (typeof fn === "function") translate = fn;
321
+ if (typeof idOf === "function") localeIdOf = idOf;
322
+ }
323
+ /** 翻译(`{name}` 占位符由宿主替换)。 */
324
+ const t = (key, params) => translate(key, params);
325
+ /** 当前语言的 BCP 47 标签(时间格式化用;未知语言回退 en)。 */
326
+ function localeTag() {
327
+ let id = "en";
328
+ try {
329
+ id = String(localeIdOf() || "en");
330
+ } catch (e) {}
331
+ return id === "zh" ? "zh-CN" : id;
332
+ }
333
+ /** 词表查表:命中返回译文,未命中回退原始值(未知状态/角色等)。 */
334
+ function vocab(prefix, raw) {
335
+ const key = `${prefix}.${raw}`;
336
+ const hit = t(key);
337
+ return hit === key ? String(raw === null || raw === void 0 ? "" : raw) : hit;
338
+ }
339
+ const stText = (s) => vocab("status", s);
340
+ const runStatusText = (s) => vocab("runStatus", s);
341
+ /**
342
+ * **阶段**状态文案:只有 `cancelled` 与 backlog 词表分道。
343
+ *
344
+ * `stText` 是 backlog 卡片词表(`status.cancelled`/`closed`/`verified` 都译「已关闭」),阶段渲染若直接复用它,
345
+ * 被中断的阶段会显示成「已关闭」(en:Closed)——2026-09-16 中断功能实测截图里,同一屏 run 行写「已取消」、
346
+ * 阶段节点写「已关闭」。阶段是被**中止**而不是被关闭,故单独取词;其余阶段状态仍走同一张表(不开两套词表)。
347
+ */
348
+ const stageStatusText = (s) => s === "cancelled" ? t("stageStatus.cancelled") : stText(s);
349
+ const kindTitle = (k) => vocab("kind", k);
350
+ const roleName = (r) => vocab("role", r);
351
+ /** 角色 chip(带图标;未知角色回退「⚙️ <raw>」)。 */
352
+ function roleChip(r) {
353
+ const key = `roleChip.${r}`;
354
+ const hit = t(key);
355
+ return hit === key ? `⚙️ ${String(r)}` : hit;
356
+ }
291
357
  const T = {
292
358
  bg: "var(--dsw-alias-bg-base)",
293
359
  layer1: "var(--dsw-alias-bg-layer-1)",
@@ -301,30 +367,7 @@ window.__ModuleLoader__.load({
301
367
  success: "var(--dsw-alias-state-success-primary)",
302
368
  warn: "var(--dsw-alias-state-warn-primary)"
303
369
  };
304
- const STATUS_TEXT = {
305
- created: "立项",
306
- "in-progress": "进行中",
307
- "pending-acceptance": "待验收",
308
- accepted: "已验收",
309
- closed: "已关闭",
310
- pending: "待办",
311
- running: "开发中",
312
- testable: "待测试",
313
- testing: "测试中",
314
- rework: "打回",
315
- "needs-human": "需人工",
316
- cancelled: "已关闭",
317
- open: "待认领",
318
- claimed: "处理中",
319
- fixed: "已修复待验",
320
- verified: "已关闭",
321
- reopened: "重开",
322
- done: "已完成",
323
- completed: "已完成",
324
- failed: "失败",
325
- interrupted: "已中断",
326
- superseded: "已取代"
327
- };
370
+ /** 状态色表(与语言无关的纯视觉映射)。 */
328
371
  const STATUS_COLOR = {
329
372
  created: T.text2,
330
373
  pending: T.text2,
@@ -349,7 +392,7 @@ window.__ModuleLoader__.load({
349
392
  interrupted: T.warn,
350
393
  superseded: T.text2
351
394
  };
352
- /** 阶段英文键 → 图标/中文展示名(2026-09-06 英文化:journal.phase 为英文键,展示名统一走映射——未来 i18n 换表即换语言)。 */
395
+ /** 阶段英文键 → 图标(2026-09-06 英文化:journal.phase 为英文键,展示名统一走词表——换语言即换表)。 */
353
396
  const PHASE_ICON = {
354
397
  prd: "📋",
355
398
  design: "🎨",
@@ -359,17 +402,6 @@ window.__ModuleLoader__.load({
359
402
  qa: "🧪",
360
403
  acceptance: "✅"
361
404
  };
362
- const PHASE_NAME = {
363
- prd: "PRD 产品需求",
364
- design: "UI/UX 设计",
365
- scaffold: "架构规划",
366
- tech: "技术方案",
367
- dev: "开发",
368
- qa: "QA 测试",
369
- acceptance: "产品验收"
370
- };
371
- const phaseNameOf = (p) => PHASE_NAME[p] || p || "—";
372
- const phaseIconOf = (p) => PHASE_ICON[p] || "⚙️";
373
405
  /** phase 归一:英文键直通;存量中文映射(防御性——新数据全英文)。 */
374
406
  const phaseKeyOf = (p) => ({
375
407
  "PRD 产品需求": "prd",
@@ -380,15 +412,22 @@ window.__ModuleLoader__.load({
380
412
  "QA 测试": "qa",
381
413
  "产品验收": "acceptance"
382
414
  })[p] || String(p || "");
383
- const RUN_STATUS_TEXT = {
384
- pending: "等待中",
385
- running: "进行中",
386
- completed: "已完成",
387
- failed: "失败",
388
- cancelled: "已取消",
389
- interrupted: "已中断",
390
- superseded: "已取代"
391
- };
415
+ const phaseNameOf = (p) => vocab("phase", phaseKeyOf(p));
416
+ const phaseIconOf = (p) => PHASE_ICON[phaseKeyOf(p)] || "⚙️";
417
+ /**
418
+ * 阶段的展示名(双语安全的取法)。
419
+ *
420
+ * journal 里 `stage.label` 是**持久化中文**(teams.json 配置 + 历史数据),而每个阶段都带
421
+ * 英文 `phase` 键——所以展示时:任务级阶段(dev 子卡,`taskKey` 有值)保留任务名(LLM 数据,
422
+ * 不该翻译),其余阶段用 `phase` 键查当前语言词表。**只影响展示,不动数据**:
423
+ * `__taskKey`/`taskKeyOf` 的任务聚合身份仍走 label 清理(聚合语义不能随语言变)。
424
+ */
425
+ function stageLabelOf(s) {
426
+ const raw = String(s && s.label || "");
427
+ if (s && s.taskKey) return raw || String(s.taskKey);
428
+ if (s && phaseKeyOf(s.phase) === "dev" && raw) return raw;
429
+ return (s && s.phase ? phaseNameOf(s.phase) : "") || raw;
430
+ }
392
431
  const COLUMNS = {
393
432
  req: [
394
433
  "created",
@@ -418,11 +457,6 @@ window.__ModuleLoader__.load({
418
457
  "needs-human"
419
458
  ]
420
459
  };
421
- const KIND_TITLE = {
422
- req: "需求",
423
- task: "任务",
424
- bug: "缺陷"
425
- };
426
460
  const h = react.default.createElement;
427
461
  const MONO = "ui-monospace, SFMono-Regular, Consolas, \"Cascadia Mono\", monospace";
428
462
  const SANS = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif";
@@ -461,9 +495,9 @@ window.__ModuleLoader__.load({
461
495
  function FoldableText({ text, charLimit = 280, lineLimit = 5, style }) {
462
496
  const [open, setOpen] = react.default.useState(false);
463
497
  if (!text) return null;
464
- const t = String(text);
465
- const lines = t.split("\n");
466
- const compact = lines.length <= lineLimit && t.length <= charLimit;
498
+ const s = String(text);
499
+ const lines = s.split("\n");
500
+ const compact = lines.length <= lineLimit && s.length <= charLimit;
467
501
  const body = (txt) => h("div", { style: {
468
502
  fontSize: 11.5,
469
503
  color: T.text,
@@ -472,10 +506,10 @@ window.__ModuleLoader__.load({
472
506
  wordBreak: "break-word",
473
507
  ...style || {}
474
508
  } }, txt);
475
- if (compact) return body(t);
476
- if (open) return h("div", null, body(t), h("button", {
509
+ if (compact) return body(s);
510
+ if (open) return h("div", null, body(s), h("button", {
477
511
  onClick: () => setOpen(false),
478
- title: "收起全文",
512
+ title: t("common.collapseFull"),
479
513
  style: {
480
514
  marginTop: 3,
481
515
  font: "inherit",
@@ -487,12 +521,12 @@ window.__ModuleLoader__.load({
487
521
  padding: 0,
488
522
  cursor: "pointer"
489
523
  }
490
- }, "收起"));
491
- const pre = lines.length > lineLimit ? lines.slice(0, lineLimit).join("\n") : t.slice(0, charLimit);
492
- const more = lines.length > lineLimit ? `… +${lines.length - lineLimit} 行` : "…";
524
+ }, t("common.collapse")));
525
+ const pre = lines.length > lineLimit ? lines.slice(0, lineLimit).join("\n") : s.slice(0, charLimit);
526
+ const more = lines.length > lineLimit ? t("common.moreLines", { n: lines.length - lineLimit }) : "…";
493
527
  return h("div", null, body(pre), h("button", {
494
528
  onClick: () => setOpen(true),
495
- title: "点击查看全文",
529
+ title: t("common.clickToExpand"),
496
530
  style: {
497
531
  marginTop: 3,
498
532
  font: "inherit",
@@ -504,11 +538,75 @@ window.__ModuleLoader__.load({
504
538
  padding: 0,
505
539
  cursor: "pointer"
506
540
  }
507
- }, `展开全文${more}`));
541
+ }, `${t("common.expandFull")}${more}`));
508
542
  }
509
- function fmtTime(t) {
510
- if (!t) return "—";
511
- const d = new Date(t);
543
+ /**
544
+ * 中断运行按钮(**两段式内联确认**):首次点击进入待确认态,3 秒内再点一次才真正执行。
545
+ *
546
+ * 为什么不自造对话框:客户端全目录 grep `confirm|dialog` 0 命中——目前没有宿主确认能力依赖,
547
+ * 内联两段式不引新依赖、也不改宿主能力面,且天然贴合「误点一个跑了几十分钟的 run」的防护诉求。
548
+ *
549
+ * 数据面由调用方给(`onConfirm(runId)`):会话内工作台与全局面板/右栏共用同一个 `teamflow/cancel`
550
+ * 方法,本件只负责待确认窗口、忙碌态与**点击不冒泡**(面板 run 行整行可点开详情)。
551
+ * `onConfirm` **必须自行消化错误**(它由调用方 try/catch 并落到可见的 err 提示);本件兜底只记 console,
552
+ * 不让点击处理里出现未处理的 rejection。
553
+ */
554
+ function CancelButton({ runId, label, title, onConfirm, style }) {
555
+ const [arm, setArm] = react.default.useState(false);
556
+ const [busy, setBusy] = react.default.useState(false);
557
+ react.default.useEffect(() => {
558
+ if (!arm) return void 0;
559
+ const timer = setTimeout(() => setArm(false), 3e3);
560
+ return () => clearTimeout(timer);
561
+ }, [arm]);
562
+ const onClick = async (e) => {
563
+ if (e && typeof e.stopPropagation === "function") e.stopPropagation();
564
+ if (busy) return;
565
+ if (!arm) {
566
+ setArm(true);
567
+ return;
568
+ }
569
+ setArm(false);
570
+ setBusy(true);
571
+ try {
572
+ await onConfirm(runId);
573
+ } catch (err) {
574
+ console.warn("[teamflow] cancel failed", err);
575
+ } finally {
576
+ setBusy(false);
577
+ }
578
+ };
579
+ const tone = busy ? {
580
+ opacity: .6,
581
+ cursor: "default"
582
+ } : arm ? {
583
+ background: T.error,
584
+ color: "#fff",
585
+ borderColor: T.error
586
+ } : null;
587
+ return h("button", {
588
+ onClick,
589
+ disabled: busy,
590
+ title: title || void 0,
591
+ style: {
592
+ font: "inherit",
593
+ fontSize: 12,
594
+ padding: "4px 12px",
595
+ borderRadius: 8,
596
+ cursor: "pointer",
597
+ border: `1px solid ${T.error}`,
598
+ background: "transparent",
599
+ color: T.error,
600
+ fontWeight: 600,
601
+ transition: "background .12s ease, color .12s ease",
602
+ ...style || {},
603
+ ...tone || {}
604
+ }
605
+ }, busy ? t("cancel.busy") : arm ? t("cancel.arm") : label || t("cancel.btn"));
606
+ }
607
+ function fmtTime(tm) {
608
+ if (!tm) return "—";
609
+ const d = new Date(tm);
512
610
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
513
611
  }
514
612
  function fmtDur(a, b) {
@@ -545,9 +643,15 @@ window.__ModuleLoader__.load({
545
643
  if (s.usage) {
546
644
  const u = s.usage;
547
645
  const hit = hitRate(u);
548
- return `输入(未命中) ${k(u.input)} / 输入(命中) ${k(u.cacheRead)} / 写缓存 ${k(u.cacheWrite)} / 输出 ${k(u.output)} · ${u.calls} 次调用${hit !== null ? ` · 缓存命中 ${hit}%` : ""}`;
646
+ return t("token.usageLine", {
647
+ input: k(u.input),
648
+ cacheRead: k(u.cacheRead),
649
+ cacheWrite: k(u.cacheWrite),
650
+ output: k(u.output),
651
+ calls: u.calls
652
+ }) + (hit !== null ? t("token.usageHit", { hit }) : "");
549
653
  }
550
- return "无 usage 明细";
654
+ return t("token.usageMissing");
551
655
  }
552
656
  /** 节点卡主 token 行:官方口径 —— 输入(未命中)/输入(命中)/输出 + 缓存命中率。 */
553
657
  function stageUsageLine(s) {
@@ -558,16 +662,6 @@ window.__ModuleLoader__.load({
558
662
  }
559
663
  return null;
560
664
  }
561
- const ROLE_NAME = {
562
- pm: "产品",
563
- design: "设计",
564
- arch: "架构",
565
- tech: "方案",
566
- dev: "开发",
567
- qa: "测试",
568
- acceptance: "验收",
569
- other: "其他"
570
- };
571
665
  const roleUsage = (u) => {
572
666
  if (!u) return "";
573
667
  const hit = hitRate(u);
@@ -576,11 +670,11 @@ window.__ModuleLoader__.load({
576
670
  /** 任务卡按角色累计的真实 token 摘要(官方口径:未命中/命中输入 + 输出 + 命中率)。 */
577
671
  function byRoleLine(task) {
578
672
  const roles = task && task.byRole || {};
579
- return Object.keys(roles).filter((k) => roles[k] && roles[k].input + roles[k].output + roles[k].cacheRead + roles[k].cacheWrite > 0).map((k) => `${ROLE_NAME[k] || k} ${roleUsage(roles[k])}`).join(" · ");
673
+ 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(" · ");
580
674
  }
581
675
  /** 多阶段 usage 汇总(官方口径)。 */
582
676
  function totalUsage(stages) {
583
- const t = {
677
+ const sum = {
584
678
  input: 0,
585
679
  cacheRead: 0,
586
680
  cacheWrite: 0,
@@ -590,17 +684,575 @@ window.__ModuleLoader__.load({
590
684
  for (const s of stages || []) {
591
685
  const u = s && s.usage;
592
686
  if (!u) continue;
593
- t.input += u.input || 0;
594
- t.cacheRead += u.cacheRead || 0;
595
- t.cacheWrite += u.cacheWrite || 0;
596
- t.output += u.output || 0;
597
- t.calls += u.calls || 0;
687
+ sum.input += u.input || 0;
688
+ sum.cacheRead += u.cacheRead || 0;
689
+ sum.cacheWrite += u.cacheWrite || 0;
690
+ sum.output += u.output || 0;
691
+ sum.calls += u.calls || 0;
598
692
  }
599
- return t;
693
+ return sum;
600
694
  }
601
- const stText = (s) => STATUS_TEXT[s] || s;
602
695
  const stColor = (s) => STATUS_COLOR[s] || T.text2;
603
696
  //#endregion
697
+ //#region client/locales.ts
698
+ /**
699
+ * dsh-plugin-teamflow — 客户端文案词典(zh / en)。
700
+ *
701
+ * 机制:走宿主 `ctx.locale`(@deepseek-ai/dsh-client-locale),不自建 i18n:
702
+ * - `apply()` 里 `ctx.locale.register(NS, { zh, en })` 注册两个词典;
703
+ * - `ctx.locale.bind(NS)` 取翻译函数(调用时读当前语言,故可常驻模块变量);
704
+ * - slot 注册项声明 `locale: NS`,切语言时宿主重渲染每个 outlet
705
+ * (ui-renderer `useLocaleRevision`),面板/tab 名称用 thunk `() => t(...)`。
706
+ *
707
+ * 约定:
708
+ * - key 扁平点号命名,按区域前缀分组(common/status/phase/kind/role/stage/…);
709
+ * - 占位符用 `{name}`(宿主 `translate` 的替换语法);
710
+ * - **zh 与 en 的 key 集合必须完全一致**(en 是兜底语言,缺 key 会直接显示 key 本身;
711
+ * 由 test/smoke.js 断言守门);
712
+ * - 数据面文案(LLM 产出的标题/摘要、teams.json 的团队名、journal 里既有中文 label)不在此表,
713
+ * 属 host/产物语言层,见 AGENTS §5「客户端面」锚点。
714
+ */
715
+ /** 词典命名空间(宿主 LocaleRuntime 的单占位命名空间)。 */
716
+ const NS = "teamflow";
717
+ /** 中文词典(key 为单一事实来源,en 必须与之同形)。 */
718
+ const zh = {
719
+ "common.close": "关闭",
720
+ "common.loading": "加载中…",
721
+ "common.retry": "重试",
722
+ "common.collapse": "收起",
723
+ "common.collapseFull": "收起全文",
724
+ "common.clickToExpand": "点击查看全文",
725
+ "common.expandFull": "展开全文",
726
+ "common.moreLines": "… +{n} 行",
727
+ "common.none": "无",
728
+ "common.noneDash": "(暂无)",
729
+ "common.noSummary": "(无摘要)",
730
+ "common.failed": "失败",
731
+ "common.running": "进行中",
732
+ "common.needsHuman": "需人工介入",
733
+ "common.unknownError": "未知错误",
734
+ "common.gotIt": "知道了",
735
+ "common.remoteNotReady": "remote 未就绪",
736
+ "common.remoteCallFailed": "{what} 调用失败:{detail}",
737
+ "common.remoteEmptyResult": "{what} 返回空结果(ok=true 但无 value;原始信封={raw})",
738
+ "common.calls": "{n} 次",
739
+ "common.callsCount": "{n} 次调用",
740
+ "common.noUsage": "无 token 数据",
741
+ "status.created": "立项",
742
+ "status.in-progress": "进行中",
743
+ "status.pending-acceptance": "待验收",
744
+ "status.accepted": "已验收",
745
+ "status.closed": "已关闭",
746
+ "status.pending": "待办",
747
+ "status.running": "开发中",
748
+ "status.testable": "待测试",
749
+ "status.testing": "测试中",
750
+ "status.rework": "打回",
751
+ "status.needs-human": "需人工",
752
+ "status.cancelled": "已关闭",
753
+ "status.open": "待认领",
754
+ "status.claimed": "处理中",
755
+ "status.fixed": "已修复待验",
756
+ "status.verified": "已关闭",
757
+ "status.reopened": "重开",
758
+ "status.done": "已完成",
759
+ "status.completed": "已完成",
760
+ "status.failed": "失败",
761
+ "status.interrupted": "已中断",
762
+ "status.superseded": "已取代",
763
+ "runStatus.pending": "等待中",
764
+ "runStatus.running": "进行中",
765
+ "runStatus.completed": "已完成",
766
+ "runStatus.failed": "失败",
767
+ "runStatus.cancelled": "已取消",
768
+ "runStatus.interrupted": "已中断",
769
+ "runStatus.superseded": "已取代",
770
+ "stageStatus.cancelled": "已中止",
771
+ "phase.prd": "PRD 产品需求",
772
+ "phase.design": "UI/UX 设计",
773
+ "phase.scaffold": "架构规划",
774
+ "phase.tech": "技术方案",
775
+ "phase.dev": "开发",
776
+ "phase.qa": "QA 测试",
777
+ "phase.acceptance": "产品验收",
778
+ "kind.req": "需求",
779
+ "kind.task": "任务",
780
+ "kind.bug": "缺陷",
781
+ "role.pm": "产品",
782
+ "role.design": "设计",
783
+ "role.arch": "架构",
784
+ "role.tech": "方案",
785
+ "role.dev": "开发",
786
+ "role.qa": "测试",
787
+ "role.acceptance": "验收",
788
+ "role.other": "其他",
789
+ "roleChip.pm": "📌 产品",
790
+ "roleChip.design": "🎨 设计",
791
+ "roleChip.arch": "🏗 架构",
792
+ "roleChip.tech": "📐 方案",
793
+ "roleChip.dev": "👨‍💻 开发",
794
+ "roleChip.qa": "🧪 QA",
795
+ "roleChip.acceptance": "✅ 验收",
796
+ "token.officialTitle": "TOKEN · 官方口径",
797
+ "token.usageLine": "输入(未命中) {input} / 输入(命中) {cacheRead} / 写缓存 {cacheWrite} / 输出 {output} · {calls} 次调用",
798
+ "token.usageHit": " · 缓存命中 {hit}%",
799
+ "token.usageMissing": "无 usage 明细",
800
+ "token.inputMiss": "输入(未命中) {v}",
801
+ "token.inputHit": "输入(命中) {v}",
802
+ "token.cacheWrite": "写缓存 {v}",
803
+ "token.output": "输出 {v}",
804
+ "token.total": "合计 {v}",
805
+ "token.allStagesTip": "输入(未命中)/输入(命中)/输出 全部阶段合计",
806
+ "token.stageLine": "阶段 token:{v}",
807
+ "stage.cardTip": "{label} —— 点击查看阶段详情",
808
+ "stage.retryTip": "重试 {n} 次(共 {m} 次尝试)",
809
+ "stage.detailTitle": "阶段详情",
810
+ "stage.summaryFallback": "(该 run 未保存完整正文,展示摘要)\n\n{summary}",
811
+ "stage.loadFailed": "⚠ 加载失败:{err}",
812
+ "stage.noOutput": "(无产物正文)",
813
+ "stage.attemptHistory": "↻ 尝试历史({n} 次 · 点击查看该次详情)",
814
+ "stage.attemptDone": "✅ 成功",
815
+ "stage.attemptFailed": "❌ {outcome}",
816
+ "stage.attemptRunning": "⏳ 进行中",
817
+ "stage.childJumpBtn": "🎬 跳转子代理会话",
818
+ "stage.childJumpTip": "跳转到该阶段子代理会话(完整推理与工具调用轨迹);跳转后请切换「对话」tab 查看",
819
+ "stage.childNoneTip": "该阶段无可用子代理会话",
820
+ "stage.childCrossSessionTip": "该子代理由会话 {sid} 发起,跨会话跳转暂不支持——请打开其发起会话的团队工作台查看",
821
+ "stage.childCrossSessionNote": "跨会话暂不支持:该子代理由会话 {sid} 发起。如需查看轨迹,请打开其发起会话的团队工作台。",
822
+ "stage.childJumpNote": "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹",
823
+ "stage.evidenceTitle": "🔬 验证证据",
824
+ "stage.evidenceMissing": "(缺失——契约未兑现,host 已记警告;可与该 run 的命令日志对照:run 期间在 logs/teamflow/<runId>/,结束后由 host 归档到 $DSH_HOME/teamflow/<workspace>/logs/<runId>/)",
825
+ "stage.evidenceMissingShort": "(缺失——契约未兑现,host 已记警告)",
826
+ "stage.artifactsTitle": "📄 阶段性产物",
827
+ "pipeline.empty": "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求",
828
+ "pipeline.noNodes": "流水线还没有开始执行节点",
829
+ "pipeline.zoomOut": "缩小",
830
+ "pipeline.zoomIn": "放大",
831
+ "pipeline.fitCanvas": "适应画布",
832
+ "pipeline.canvasHint": "✥ 拖动画布 · 滚轮缩放",
833
+ "workbench.title": "团队工作台",
834
+ "workbench.running": "流水线运行中",
835
+ "workbench.idle": "空闲",
836
+ "workbench.refresh": "🔄 刷新",
837
+ "workbench.resumeTip": "断点续跑 {id}\n当前状态:{status};跳过已完成阶段,从第一个未完成阶段重跑",
838
+ "workbench.resuming": "续跑中…",
839
+ "workbench.resumeBtn": "↻ 从断点重跑 #{id}",
840
+ "workbench.loadFailed": "⚠ {err}(确认已安装 dsh-plugin-teamflow 且 web 已重启)",
841
+ "workbench.needsHumanBanner": "⚠ {n} 项需人工介入",
842
+ "workbench.handle": "处理 {id}",
843
+ "workbench.manualReason": "人工处理",
844
+ "workbench.tabPipeline": "🔄 流水线",
845
+ "workbench.tabBoard": "📋 Backlog 看板",
846
+ "workbench.workspaceTip": "当前工作区(workspace 级隔离):{path}",
847
+ "workbench.noWorkspace": "未连接工作区",
848
+ "workbench.history": "历史",
849
+ "workbench.openRightBarTip": "在右侧栏打开该 run 详情(与任务夹产物并排看)",
850
+ "workbench.openRightBarBtn": "⇥ 右栏打开",
851
+ "cancel.btn": "⏹ 中断",
852
+ "cancel.btnWithId": "⏹ 中断 #{id}",
853
+ "cancel.arm": "确认中断?",
854
+ "cancel.busy": "中断中…",
855
+ "cancel.sent": "已请求中断,等待当前阶段收尾…",
856
+ "cancel.tip": "中断 {id}\n立即中止当前阶段子代理;已完成阶段保留,之后可用「从断点重跑」继续;取消的 run 不产生提交",
857
+ "cancel.failed": "中断未生效:该流水线已不在运行中(可能刚结束)",
858
+ "board.empty": "backlog 为空(还没有流水线运行过)",
859
+ "board.dragReason": "看板拖拽流转",
860
+ "board.cardTip": "{id} · {status}{summary}(点击查看详情)",
861
+ "board.assignDevTip": "dev 分配\n{who}",
862
+ "board.assignQaTip": "qa 分配\n{who}",
863
+ "board.acceptTip": "验收/汇报人\n{who}",
864
+ "board.subtaskCount": "📦 {n} 子卡",
865
+ "board.retries": "重试 {n}",
866
+ "item.overview": "概览",
867
+ "item.acceptRow": "✅ 验收",
868
+ "item.retryRow": "↻ 重试",
869
+ "item.humanRow": "⚠ 人工介入",
870
+ "item.updatedAt": "更新于",
871
+ "item.runSection": "运行",
872
+ "item.runBtn": "▶ 流水线",
873
+ "item.jumpRunTip": "跳转到该需求的流水线视图 #{id}",
874
+ "item.requirement": "需求原文",
875
+ "item.noRequirement": "(无原文)",
876
+ "item.runDocs": "任务夹",
877
+ "item.previewTip": "在右侧栏预览 {path}",
878
+ "item.subtasks": "关联子卡({n})",
879
+ "item.bugs": "关联缺陷({n})",
880
+ "item.timeline": "流转时间线({n})",
881
+ "team.currentTip": "当前团队:{name}(点击切换)",
882
+ "team.pick": "选择团队",
883
+ "team.label": "团队",
884
+ "team.none": "无团队(直接对话)",
885
+ "team.noneNote": "不走 teamflow,模型直接工作",
886
+ "panel.title": "🏭 团队工作台",
887
+ "panel.subtitle": "全局面板 · 按产品线",
888
+ "panel.currentSession": "当前会话 {sid}",
889
+ "panel.noSession": "无当前会话",
890
+ "panel.reload": "刷新数据",
891
+ "panel.backToChat": "回到对话",
892
+ "panel.backToChatTip": "回到对话(再点侧边栏图标即可切回本面板)",
893
+ "panel.railTitle": "产品线 · {n}",
894
+ "panel.rescanTip": "重新扫描 $DSH_HOME/teamflow",
895
+ "panel.refreshing": "刷新中",
896
+ "panel.refresh": "刷新",
897
+ "panel.noProducts": "还没有产品线。在某个工作区跑过一次流水线后,这里会出现对应产品线($DSH_HOME/teamflow/<key>)。",
898
+ "panel.activeRuns": "运行 {n}",
899
+ "panel.updated": "更新 {time}",
900
+ "panel.reading": "读取中…",
901
+ "panel.verdict": "验收 {v}",
902
+ "panel.pickProduct": "选择左侧产品线查看 backlog 与 run(首次进入默认选最近更新的产品线)。",
903
+ "panel.loadingView": "读取产品线数据中…",
904
+ "panel.runChip": "run {n}",
905
+ "panel.activeChip": "活跃 {n}",
906
+ "panel.verdictChip": "验收 {v}",
907
+ "panel.tabRuns": "🚀 流水线 run · {n}",
908
+ "panel.tabBacklog": "📋 Backlog · {n}",
909
+ "panel.pinnedActive": "已置顶进行中 {n}",
910
+ "panel.showRecent": "只看最近 {n} 条",
911
+ "panel.showAll": "展开全部 {n} 条",
912
+ "panel.clearRunFilterTip": "清除 run 状态筛选",
913
+ "panel.filtered": "筛选中 {sel} 项 · 显示 {shown}/{total} × 清除",
914
+ "panel.emptyRunFilter": "该筛选下没有 run。",
915
+ "panel.runHint": "点一行看详情浮层;「去会话右栏」= 跳到该 run 的发起会话并在其右侧栏打开(与任务夹产物并排)",
916
+ "panel.boardHint": "点状态徽章可筛选(多选);终态卡片默认收起,筛选时自动显示",
917
+ "panel.runDetail": "run 详情",
918
+ "panel.goOwnerSession": "去发起会话",
919
+ "panel.goOwnerSessionTip": "跳到该 run 的发起会话,并在那个会话的右侧栏打开(与任务夹产物并排看)",
920
+ "panel.remoteProductsUnavailable": "remote.products 不可用(插件未挂载或版本过旧)",
921
+ "panel.hintNoAddress": "{label}:host 未生成可打开的地址(可能缺少会话上下文)",
922
+ "panel.hintRightbarFailed": "右侧栏打开失败(宿主只在对话视图挂载它):{label}——已在本面板内联显示",
923
+ "panel.hintSessionGone": "发起会话 {sid}… 不在会话列表里(可能已被清理)——已在本面板展示详情",
924
+ "panel.hintSwitchedNoRightbar": "已切到发起会话,但右栏没打开({label})——可在该会话里用「⇥ 右栏打开」重试",
925
+ "panel.hintInlineSuffix": "——已在本面板内联显示",
926
+ "runList.empty": "该产品线还没有 run 记录。",
927
+ "runList.running": "进行中",
928
+ "runList.noRequirement": "(无需求描述)",
929
+ "runList.stageProgress": "阶段 {done}/{total}",
930
+ "runList.openRightBar": "去会话右栏",
931
+ "runList.openRightBarTip": "跳到该 run 的发起会话,并在那个会话的右侧栏打开详情(右侧栏是会话级的:挂到无关会话上没有意义)",
932
+ "runList.callsSuffix": " · {n} 次",
933
+ "panelBoard.empty": "backlog 为空(该产品线还没有立项卡片)。",
934
+ "panelBoard.groupCount": "{kind} · {n}",
935
+ "panelBoard.activeCount": "活动 {n}",
936
+ "panelBoard.clearFilterTip": "清除本组筛选",
937
+ "panelBoard.collapseDone": "收起已完成 {n}",
938
+ "panelBoard.expandDone": "已完成 {n} ▸",
939
+ "panelBoard.collapseDoneTip": "收起已完成/已关闭卡片",
940
+ "panelBoard.expandDoneTip": "展开已完成/已关闭卡片",
941
+ "panelBoard.emptyFiltered": "该筛选下没有卡片。",
942
+ "panelBoard.filterChipOn": "点击取消该状态筛选",
943
+ "panelBoard.filterChipOff": "点击只看该状态(可多选)",
944
+ "panelItem.row.req": "需求",
945
+ "panelItem.row.owner": "负责人",
946
+ "panelItem.row.dev": "开发",
947
+ "panelItem.row.qa": "测试",
948
+ "panelItem.row.accept": "验收",
949
+ "panelItem.row.retries": "重试",
950
+ "panelItem.row.runDocs": "任务夹",
951
+ "panelItem.defectSection": "缺陷详情(QA 报告导入)",
952
+ "panelItem.row.defectId": "缺陷编号",
953
+ "panelItem.row.severity": "严重级",
954
+ "panelItem.row.module": "功能模块",
955
+ "panelItem.row.reproduce": "复现步骤",
956
+ "panelItem.row.expected": "期望行为",
957
+ "panelItem.row.actual": "实际行为",
958
+ "panelItem.row.defectAc": "关联验收项",
959
+ "panelItem.row.defectCheck": "检测命令",
960
+ "panelItem.row.defectCriterion": "通过判据",
961
+ "panelItem.noDefectDetail": "(该卡无缺陷描述:登记时 QA 报告只给了三要素,或为旧版本登记的卡;细节见关联 run 的 QA-REPORT.md)",
962
+ "panelItem.spec": "规格",
963
+ "panelItem.summary": "结论摘要",
964
+ "panelItem.artifacts": "任务夹产物 · {n}",
965
+ "panelItem.artifactTip": "{address}\n(跳到产物所属会话后在该会话右侧栏打开)",
966
+ "panelItem.noArtifacts": "该条目没有可预览的任务夹产物(或缺少会话上下文,无法生成文件地址)。",
967
+ "panelItem.subtasks": "子卡 · {n}",
968
+ "panelItem.bugs": "关联缺陷 · {n}",
969
+ "panelItem.events": "流转时间线 · 最近 {n} 条",
970
+ "detail.runMissing": "未找到该 run(可能已被清理,或地址已过期)。",
971
+ "detail.subagents": "子代理 {n}",
972
+ "detail.stages": "阶段 · {n}",
973
+ "detail.stageFailed": "阶段详情读取失败:{err}",
974
+ "detail.stageTitle": "阶段 #{seq} 详情",
975
+ "detail.output": "阶段产出",
976
+ "detail.attempts": "同任务尝试 · {n}",
977
+ "detail.logs": "日志 · 最近 {n} 条",
978
+ "detail.endedRunning": "进行中",
979
+ "detail.subagentChip": "子代理 {id}",
980
+ "tab.resolveFailed": "无法解析 run 地址:{address}",
981
+ "tab.readingAddress": "读取 tab 地址中…",
982
+ "tab.noHook": "宿主 tab 信息钩子不可用(useTabInfo 缺失)",
983
+ "tab.remoteUnavailable": "remote 不可用(插件未挂载或版本过旧)",
984
+ "tab.readingRun": "读取 run 详情中…"
985
+ };
986
+ /** 英文词典(key 集合必须与 zh 完全一致)。 */
987
+ const en = {
988
+ "common.close": "Close",
989
+ "common.loading": "Loading…",
990
+ "common.retry": "Retry",
991
+ "common.collapse": "Collapse",
992
+ "common.collapseFull": "Collapse",
993
+ "common.clickToExpand": "Click to expand",
994
+ "common.expandFull": "Show more",
995
+ "common.moreLines": "… +{n} lines",
996
+ "common.none": "none",
997
+ "common.noneDash": "(none)",
998
+ "common.noSummary": "(no summary)",
999
+ "common.failed": "Failed",
1000
+ "common.running": "Running",
1001
+ "common.needsHuman": "needs human intervention",
1002
+ "common.unknownError": "unknown error",
1003
+ "common.gotIt": "Got it",
1004
+ "common.remoteNotReady": "remote not ready",
1005
+ "common.remoteCallFailed": "{what} call failed: {detail}",
1006
+ "common.remoteEmptyResult": "{what} returned an empty result (ok=true but no value; raw envelope={raw})",
1007
+ "common.calls": "{n} calls",
1008
+ "common.callsCount": "{n} calls",
1009
+ "common.noUsage": "no token data",
1010
+ "status.created": "Created",
1011
+ "status.in-progress": "In progress",
1012
+ "status.pending-acceptance": "Pending acceptance",
1013
+ "status.accepted": "Accepted",
1014
+ "status.closed": "Closed",
1015
+ "status.pending": "Todo",
1016
+ "status.running": "Developing",
1017
+ "status.testable": "Ready for test",
1018
+ "status.testing": "Testing",
1019
+ "status.rework": "Rework",
1020
+ "status.needs-human": "Needs human",
1021
+ "status.cancelled": "Closed",
1022
+ "status.open": "Unclaimed",
1023
+ "status.claimed": "In progress",
1024
+ "status.fixed": "Fixed (pending verify)",
1025
+ "status.verified": "Closed",
1026
+ "status.reopened": "Reopened",
1027
+ "status.done": "Done",
1028
+ "status.completed": "Completed",
1029
+ "status.failed": "Failed",
1030
+ "status.interrupted": "Interrupted",
1031
+ "status.superseded": "Superseded",
1032
+ "runStatus.pending": "Queued",
1033
+ "runStatus.running": "Running",
1034
+ "runStatus.completed": "Completed",
1035
+ "runStatus.failed": "Failed",
1036
+ "runStatus.cancelled": "Cancelled",
1037
+ "runStatus.interrupted": "Interrupted",
1038
+ "runStatus.superseded": "Superseded",
1039
+ "stageStatus.cancelled": "Stopped",
1040
+ "phase.prd": "PRD",
1041
+ "phase.design": "UI/UX design",
1042
+ "phase.scaffold": "Architecture",
1043
+ "phase.tech": "Technical design",
1044
+ "phase.dev": "Development",
1045
+ "phase.qa": "QA testing",
1046
+ "phase.acceptance": "Acceptance",
1047
+ "kind.req": "Requirements",
1048
+ "kind.task": "Tasks",
1049
+ "kind.bug": "Defects",
1050
+ "role.pm": "Product",
1051
+ "role.design": "Design",
1052
+ "role.arch": "Architecture",
1053
+ "role.tech": "Tech design",
1054
+ "role.dev": "Dev",
1055
+ "role.qa": "QA",
1056
+ "role.acceptance": "Acceptance",
1057
+ "role.other": "Other",
1058
+ "roleChip.pm": "📌 Product",
1059
+ "roleChip.design": "🎨 Design",
1060
+ "roleChip.arch": "🏗 Architecture",
1061
+ "roleChip.tech": "📐 Tech design",
1062
+ "roleChip.dev": "👨‍💻 Dev",
1063
+ "roleChip.qa": "🧪 QA",
1064
+ "roleChip.acceptance": "✅ Acceptance",
1065
+ "token.officialTitle": "TOKEN · official metering",
1066
+ "token.usageLine": "Input (miss) {input} / Input (hit) {cacheRead} / Cache write {cacheWrite} / Output {output} · {calls} calls",
1067
+ "token.usageHit": " · cache hit {hit}%",
1068
+ "token.usageMissing": "no usage detail",
1069
+ "token.inputMiss": "Input (miss) {v}",
1070
+ "token.inputHit": "Input (hit) {v}",
1071
+ "token.cacheWrite": "Cache write {v}",
1072
+ "token.output": "Output {v}",
1073
+ "token.total": "Total {v}",
1074
+ "token.allStagesTip": "Input (miss) / Input (hit) / Output totals across all stages",
1075
+ "token.stageLine": "Stage tokens: {v}",
1076
+ "stage.cardTip": "{label} — click for stage detail",
1077
+ "stage.retryTip": "Retried {n}× ({m} attempts in total)",
1078
+ "stage.detailTitle": "Stage detail",
1079
+ "stage.summaryFallback": "(Full text was not saved for this run — showing the summary)\n\n{summary}",
1080
+ "stage.loadFailed": "⚠ Load failed: {err}",
1081
+ "stage.noOutput": "(no output text)",
1082
+ "stage.attemptHistory": "↻ Attempt history ({n} · click to inspect one)",
1083
+ "stage.attemptDone": "✅ OK",
1084
+ "stage.attemptFailed": "❌ {outcome}",
1085
+ "stage.attemptRunning": "⏳ Running",
1086
+ "stage.childJumpBtn": "🎬 Open subagent session",
1087
+ "stage.childJumpTip": "Open this stage's subagent session (full reasoning and tool-call trail); then switch to the \"Chat\" tab",
1088
+ "stage.childNoneTip": "No subagent session is available for this stage",
1089
+ "stage.childCrossSessionTip": "This subagent was started by session {sid}; cross-session navigation is not supported yet — open the Team Workbench in its originating session",
1090
+ "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.",
1091
+ "stage.childJumpNote": "After jumping, switch to the \"Chat\" tab to see this subagent's full session trail",
1092
+ "stage.evidenceTitle": "🔬 Verification evidence",
1093
+ "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)",
1094
+ "stage.evidenceMissingShort": "(missing — the contract was not honored and the host logged a warning)",
1095
+ "stage.artifactsTitle": "📄 Stage artifacts",
1096
+ "pipeline.empty": "No active pipeline — ask the model to call teamflow_start, or type a requirement above",
1097
+ "pipeline.noNodes": "The pipeline has not started any stage node yet",
1098
+ "pipeline.zoomOut": "Zoom out",
1099
+ "pipeline.zoomIn": "Zoom in",
1100
+ "pipeline.fitCanvas": "Fit canvas",
1101
+ "pipeline.canvasHint": "✥ Drag to pan · scroll to zoom",
1102
+ "workbench.title": "Team Workbench",
1103
+ "workbench.running": "Pipeline running",
1104
+ "workbench.idle": "Idle",
1105
+ "workbench.refresh": "🔄 Refresh",
1106
+ "workbench.resumeTip": "Resume {id}\nCurrent status: {status}; completed stages are skipped and the run restarts from the first unfinished stage",
1107
+ "workbench.resuming": "Resuming…",
1108
+ "workbench.resumeBtn": "↻ Resume from #{id}",
1109
+ "workbench.loadFailed": "⚠ {err} (make sure dsh-plugin-teamflow is installed and the web host was restarted)",
1110
+ "workbench.needsHumanBanner": "⚠ {n} item(s) need human intervention",
1111
+ "workbench.handle": "Handle {id}",
1112
+ "workbench.manualReason": "manual handling",
1113
+ "workbench.tabPipeline": "🔄 Pipeline",
1114
+ "workbench.tabBoard": "📋 Backlog board",
1115
+ "workbench.workspaceTip": "Current workspace (workspace-level isolation): {path}",
1116
+ "workbench.noWorkspace": "no workspace connected",
1117
+ "workbench.history": "History",
1118
+ "workbench.openRightBarTip": "Open this run's detail in the right sidebar (side by side with task-folder artifacts)",
1119
+ "workbench.openRightBarBtn": "⇥ Open in right bar",
1120
+ "cancel.btn": "⏹ Stop",
1121
+ "cancel.btnWithId": "⏹ Stop #{id}",
1122
+ "cancel.arm": "Confirm stop?",
1123
+ "cancel.busy": "Stopping…",
1124
+ "cancel.sent": "Stop requested — waiting for the current stage to wind down…",
1125
+ "cancel.tip": "Stop {id}\nAborts the subagent of the current stage immediately; finished stages are kept and can be resumed from the checkpoint later; a cancelled run never commits",
1126
+ "cancel.failed": "Stop had no effect: the pipeline is no longer running (it may have just finished)",
1127
+ "board.empty": "Backlog is empty (no pipeline has run yet)",
1128
+ "board.dragReason": "kanban drag transition",
1129
+ "board.cardTip": "{id} · {status}{summary} (click for details)",
1130
+ "board.assignDevTip": "dev assignee\n{who}",
1131
+ "board.assignQaTip": "qa assignee\n{who}",
1132
+ "board.acceptTip": "acceptance/report owner\n{who}",
1133
+ "board.subtaskCount": "📦 {n} subtasks",
1134
+ "board.retries": "retries {n}",
1135
+ "item.overview": "Overview",
1136
+ "item.acceptRow": "✅ Acceptance",
1137
+ "item.retryRow": "↻ Retries",
1138
+ "item.humanRow": "⚠ Human intervention",
1139
+ "item.updatedAt": "Updated",
1140
+ "item.runSection": "Run",
1141
+ "item.runBtn": "▶ Pipeline",
1142
+ "item.jumpRunTip": "Open this requirement's pipeline view #{id}",
1143
+ "item.requirement": "Raw requirement",
1144
+ "item.noRequirement": "(no raw text)",
1145
+ "item.runDocs": "Task folder",
1146
+ "item.previewTip": "Preview {path} in the right sidebar",
1147
+ "item.subtasks": "Linked subtasks ({n})",
1148
+ "item.bugs": "Linked defects ({n})",
1149
+ "item.timeline": "Transition timeline ({n})",
1150
+ "team.currentTip": "Current team: {name} (click to switch)",
1151
+ "team.pick": "Select team",
1152
+ "team.label": "Team",
1153
+ "team.none": "No team (chat directly)",
1154
+ "team.noneNote": "teamflow is not used; the model works directly",
1155
+ "panel.title": "🏭 Team Workbench",
1156
+ "panel.subtitle": "Global panel · by product line",
1157
+ "panel.currentSession": "Session {sid}",
1158
+ "panel.noSession": "No active session",
1159
+ "panel.reload": "Reload data",
1160
+ "panel.backToChat": "Back to chat",
1161
+ "panel.backToChatTip": "Back to chat (click the sidebar icon again to return to this panel)",
1162
+ "panel.railTitle": "Product lines · {n}",
1163
+ "panel.rescanTip": "Rescan $DSH_HOME/teamflow",
1164
+ "panel.refreshing": "Refreshing",
1165
+ "panel.refresh": "Refresh",
1166
+ "panel.noProducts": "No product lines yet. Once a pipeline has run in a workspace, its product line shows up here ($DSH_HOME/teamflow/<key>).",
1167
+ "panel.activeRuns": "{n} running",
1168
+ "panel.updated": "updated {time}",
1169
+ "panel.reading": "Loading…",
1170
+ "panel.verdict": "acceptance {v}",
1171
+ "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).",
1172
+ "panel.loadingView": "Loading product-line data…",
1173
+ "panel.runChip": "runs {n}",
1174
+ "panel.activeChip": "active {n}",
1175
+ "panel.verdictChip": "acceptance {v}",
1176
+ "panel.tabRuns": "🚀 Pipeline runs · {n}",
1177
+ "panel.tabBacklog": "📋 Backlog · {n}",
1178
+ "panel.pinnedActive": "{n} running pinned",
1179
+ "panel.showRecent": "Show latest {n}",
1180
+ "panel.showAll": "Show all {n}",
1181
+ "panel.clearRunFilterTip": "Clear the run status filter",
1182
+ "panel.filtered": "Filtered {sel} · showing {shown}/{total} × clear",
1183
+ "panel.emptyRunFilter": "No runs match this filter.",
1184
+ "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)",
1185
+ "panel.boardHint": "Click a status badge to filter (multi-select); terminal cards are collapsed by default and appear automatically while filtering",
1186
+ "panel.runDetail": "Run detail",
1187
+ "panel.goOwnerSession": "Go to originating session",
1188
+ "panel.goOwnerSessionTip": "Jump to the run's originating session and open its right bar there (side by side with task-folder artifacts)",
1189
+ "panel.remoteProductsUnavailable": "remote.products unavailable (plugin not mounted or outdated)",
1190
+ "panel.hintNoAddress": "{label}: the host produced no openable address (session context may be missing)",
1191
+ "panel.hintRightbarFailed": "Failed to open the right sidebar (the host mounts it only in the chat view): {label} — shown inline in this panel instead",
1192
+ "panel.hintSessionGone": "Originating session {sid}… is not in the session list (it may have been cleaned up) — showing the detail in this panel",
1193
+ "panel.hintSwitchedNoRightbar": "Switched to the originating session but the right bar did not open ({label}) — retry with \"⇥ Open in right bar\" in that session",
1194
+ "panel.hintInlineSuffix": " — shown inline in this panel instead",
1195
+ "runList.empty": "No runs recorded for this product line yet.",
1196
+ "runList.running": "Running",
1197
+ "runList.noRequirement": "(no requirement text)",
1198
+ "runList.stageProgress": "Stages {done}/{total}",
1199
+ "runList.openRightBar": "Open in session right bar",
1200
+ "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)",
1201
+ "runList.callsSuffix": " · {n} calls",
1202
+ "panelBoard.empty": "Backlog is empty (this product line has no cards yet).",
1203
+ "panelBoard.groupCount": "{kind} · {n}",
1204
+ "panelBoard.activeCount": "Active {n}",
1205
+ "panelBoard.clearFilterTip": "Clear this group's filter",
1206
+ "panelBoard.collapseDone": "Hide {n} done",
1207
+ "panelBoard.expandDone": "{n} done ▸",
1208
+ "panelBoard.collapseDoneTip": "Hide done/closed cards",
1209
+ "panelBoard.expandDoneTip": "Show done/closed cards",
1210
+ "panelBoard.emptyFiltered": "No cards match this filter.",
1211
+ "panelBoard.filterChipOn": "Click to clear this status filter",
1212
+ "panelBoard.filterChipOff": "Click to filter by this status only (multi-select)",
1213
+ "panelItem.row.req": "Requirement",
1214
+ "panelItem.row.owner": "Owner",
1215
+ "panelItem.row.dev": "Dev",
1216
+ "panelItem.row.qa": "QA",
1217
+ "panelItem.row.accept": "Acceptance",
1218
+ "panelItem.row.retries": "Retries",
1219
+ "panelItem.row.runDocs": "Task folder",
1220
+ "panelItem.defectSection": "Defect detail (imported from the QA report)",
1221
+ "panelItem.row.defectId": "Defect id",
1222
+ "panelItem.row.severity": "Severity",
1223
+ "panelItem.row.module": "Module",
1224
+ "panelItem.row.reproduce": "Steps to reproduce",
1225
+ "panelItem.row.expected": "Expected",
1226
+ "panelItem.row.actual": "Actual",
1227
+ "panelItem.row.defectAc": "Related AC",
1228
+ "panelItem.row.defectCheck": "Check command",
1229
+ "panelItem.row.defectCriterion": "Pass criterion",
1230
+ "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)",
1231
+ "panelItem.spec": "Spec",
1232
+ "panelItem.summary": "Summary",
1233
+ "panelItem.artifacts": "Task-folder artifacts · {n}",
1234
+ "panelItem.artifactTip": "{address}\n(opens in the right sidebar of the session that owns this artifact)",
1235
+ "panelItem.noArtifacts": "This item has no previewable task-folder artifacts (or no session context to build a file address).",
1236
+ "panelItem.subtasks": "Subtasks · {n}",
1237
+ "panelItem.bugs": "Linked defects · {n}",
1238
+ "panelItem.events": "Transition timeline · last {n}",
1239
+ "detail.runMissing": "Run not found (it may have been cleaned up, or the address is stale).",
1240
+ "detail.subagents": "Subagents {n}",
1241
+ "detail.stages": "Stages · {n}",
1242
+ "detail.stageFailed": "Failed to load stage detail: {err}",
1243
+ "detail.stageTitle": "Stage #{seq} detail",
1244
+ "detail.output": "Stage output",
1245
+ "detail.attempts": "Attempts for this task · {n}",
1246
+ "detail.logs": "Logs · last {n}",
1247
+ "detail.endedRunning": "running",
1248
+ "detail.subagentChip": "Subagent {id}",
1249
+ "tab.resolveFailed": "Cannot parse the run address: {address}",
1250
+ "tab.readingAddress": "Reading the tab address…",
1251
+ "tab.noHook": "Host tab-info hook unavailable (useTabInfo missing)",
1252
+ "tab.remoteUnavailable": "remote unavailable (plugin not mounted or outdated)",
1253
+ "tab.readingRun": "Loading run detail…"
1254
+ };
1255
+ //#endregion
604
1256
  //#region client/panel.tsx
605
1257
  /**
606
1258
  * dsh-plugin-teamflow — 全局面板 + 右栏 run 详情 tab(v0.1.8 ①)。
@@ -649,14 +1301,17 @@ window.__ModuleLoader__.load({
649
1301
  * 此时不能静默返回 undefined(调用方会把它再包成「未知错误」,丢掉定位信息),故显式报出原始信封。 */
650
1302
  function unwrap$1(res, what) {
651
1303
  if (!res || !res.ok) {
652
- let detail = "未知错误";
1304
+ let detail = t("common.unknownError");
653
1305
  try {
654
- detail = res && res.error && (res.error.message || res.error.code) || JSON.stringify(res) || "未知错误";
1306
+ detail = res && res.error && (res.error.message || res.error.code) || JSON.stringify(res) || t("common.unknownError");
655
1307
  } catch (e) {}
656
1308
  try {
657
1309
  console.warn("[teamflow] remote 调用失败", what, res);
658
1310
  } catch (e) {}
659
- throw new Error(`${what || "remote"} 调用失败:${detail}`);
1311
+ throw new Error(t("common.remoteCallFailed", {
1312
+ what: what || "remote",
1313
+ detail
1314
+ }));
660
1315
  }
661
1316
  if (res.value === void 0) {
662
1317
  try {
@@ -668,7 +1323,10 @@ window.__ModuleLoader__.load({
668
1323
  } catch (e) {
669
1324
  raw = String(res);
670
1325
  }
671
- throw new Error(`${what || "remote"} 返回空结果(ok=true 但无 value;原始信封=${raw})`);
1326
+ throw new Error(t("common.remoteEmptyResult", {
1327
+ what: what || "remote",
1328
+ raw
1329
+ }));
672
1330
  }
673
1331
  return res.value;
674
1332
  }
@@ -744,9 +1402,9 @@ window.__ModuleLoader__.load({
744
1402
  ...style
745
1403
  } }, text);
746
1404
  const runUsageText = (u) => {
747
- if (!u || !(u.input || u.cacheRead || u.cacheWrite || u.output)) return "无 token 数据";
1405
+ if (!u || !(u.input || u.cacheRead || u.cacheWrite || u.output)) return t("common.noUsage");
748
1406
  const hit = hitRate(u);
749
- return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""} · ${u.calls} 次`;
1407
+ return `⇅${fmtTokens(u.input)} ⇅${fmtTokens(u.cacheRead)} ⬆${fmtTokens(u.output)}${hit !== null ? ` ·${hit}%` : ""} · ${t("common.calls", { n: u.calls })}`;
750
1408
  };
751
1409
  const RUN_PREVIEW = 8;
752
1410
  const TERMINAL_STATUSES = [
@@ -772,17 +1430,17 @@ window.__ModuleLoader__.load({
772
1430
  fontSize: 11.5,
773
1431
  fontWeight: 700,
774
1432
  color: T.text2
775
- } }, `产品线 · ${products.length}`), h("button", {
1433
+ } }, t("panel.railTitle", { n: products.length })), h("button", {
776
1434
  style: panelBtn,
777
1435
  onClick: onRefresh,
778
1436
  disabled: busy,
779
- title: "重新扫描 $DSH_HOME/teamflow"
780
- }, busy ? "刷新中" : "刷新")), h("div", { style: {
1437
+ title: t("panel.rescanTip")
1438
+ }, busy ? t("panel.refreshing") : t("panel.refresh"))), h("div", { style: {
781
1439
  flex: 1,
782
1440
  minHeight: 0,
783
1441
  overflowY: "auto",
784
1442
  padding: "0 8px 12px"
785
- } }, products.length === 0 ? muted("还没有产品线。在某个工作区跑过一次流水线后,这里会出现对应产品线($DSH_HOME/teamflow/<key>)。", { padding: "10px 4px" }) : products.map((p) => {
1443
+ } }, products.length === 0 ? muted(t("panel.noProducts"), { padding: "10px 4px" }) : products.map((p) => {
786
1444
  const on = p.key === current;
787
1445
  return h("div", {
788
1446
  key: p.key,
@@ -806,7 +1464,7 @@ window.__ModuleLoader__.load({
806
1464
  overflow: "hidden",
807
1465
  textOverflow: "ellipsis",
808
1466
  whiteSpace: "nowrap"
809
- } }, p.title || p.key), p.activeRuns > 0 ? chip(`运行 ${p.activeRuns}`, T.brand, { dot: true }) : null), h("div", { style: {
1467
+ } }, p.title || p.key), p.activeRuns > 0 ? chip(t("panel.activeRuns", { n: p.activeRuns }), T.brand, { dot: true }) : null), h("div", { style: {
810
1468
  fontSize: 10,
811
1469
  color: T.text2,
812
1470
  fontFamily: MONO,
@@ -820,10 +1478,10 @@ window.__ModuleLoader__.load({
820
1478
  marginTop: 3,
821
1479
  fontSize: 10,
822
1480
  color: T.text2
823
- } }, h("span", null, `run ${p.totalRuns}`), p.updatedAt ? h("span", null, `更新 ${fmtTime(p.updatedAt)}`) : null, p.key === loadingKey ? h("span", { style: {
1481
+ } }, 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: {
824
1482
  color: T.brand,
825
1483
  fontWeight: 600
826
- } }, "读取中…") : null), p.lastRequirement ? h("div", { style: {
1484
+ } }, t("panel.reading")) : null), p.lastRequirement ? h("div", { style: {
827
1485
  fontSize: 10.5,
828
1486
  color: T.text2,
829
1487
  marginTop: 3,
@@ -832,11 +1490,11 @@ window.__ModuleLoader__.load({
832
1490
  WebkitLineClamp: 2,
833
1491
  WebkitBoxOrient: "vertical",
834
1492
  overflow: "hidden"
835
- } }, p.lastRequirement) : null, p.lastVerdict ? h("div", { style: { marginTop: 4 } }, chip(`验收 ${p.lastVerdict}`, stColor(p.lastVerdict === "accepted" ? "accepted" : p.lastVerdict))) : null);
1493
+ } }, p.lastRequirement) : null, p.lastVerdict ? h("div", { style: { marginTop: 4 } }, chip(t("panel.verdict", { v: p.lastVerdict }), stColor(p.lastVerdict === "accepted" ? "accepted" : p.lastVerdict))) : null);
836
1494
  })));
837
1495
  }
838
- function RunList({ runs, activeRunId, onOpenRun, onInlineRun }) {
839
- if (!runs.length) return muted("该产品线还没有 run 记录。", { padding: "2px 2px 8px" });
1496
+ function RunList({ runs, activeRunId, onOpenRun, onInlineRun, onCancel }) {
1497
+ if (!runs.length) return muted(t("runList.empty"), { padding: "2px 2px 8px" });
840
1498
  return h("div", { style: {
841
1499
  display: "flex",
842
1500
  flexDirection: "column",
@@ -864,35 +1522,46 @@ window.__ModuleLoader__.load({
864
1522
  } }, h("div", { style: {
865
1523
  ...flexRow,
866
1524
  gap: 6
867
- } }, chip(RUN_STATUS_TEXT[r.status] || r.status, stColor(r.status), { dot: true }), r.mode ? chip(String(r.mode), T.text2) : null, h("span", { style: {
1525
+ } }, chip(runStatusText(r.status), stColor(r.status), { dot: true }), r.mode ? chip(String(r.mode), T.text2) : null, h("span", { style: {
868
1526
  fontFamily: MONO,
869
1527
  fontSize: 10,
870
1528
  color: T.text2
871
1529
  } }, r.id), active ? h("span", { style: {
872
1530
  fontSize: 10,
873
1531
  color: T.brand
874
- } }, "进行中") : null), h("div", { style: {
1532
+ } }, t("runList.running")) : null), h("div", { style: {
875
1533
  fontSize: 11.5,
876
1534
  color: T.text,
877
1535
  marginTop: 3,
878
1536
  overflow: "hidden",
879
1537
  textOverflow: "ellipsis",
880
1538
  whiteSpace: "nowrap"
881
- } }, r.requirement || "(无需求描述)"), h("div", { style: {
1539
+ } }, r.requirement || t("runList.noRequirement")), h("div", { style: {
882
1540
  ...flexRow,
883
1541
  gap: 10,
884
1542
  marginTop: 3,
885
1543
  fontSize: 10,
886
1544
  color: T.text2,
887
1545
  fontFamily: MONO
888
- } }, h("span", null, `阶段 ${r.doneStages}/${r.stageCount}`), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), h("button", {
1546
+ } }, h("span", null, t("runList.stageProgress", {
1547
+ done: r.doneStages,
1548
+ total: r.stageCount
1549
+ })), h("span", null, runUsageText(r.usage)), h("span", null, `${fmtTime(r.startedAt)}${r.endedAt ? ` → ${fmtTime(r.endedAt)}` : ""} ${fmtDur(r.startedAt, r.endedAt)}`))), r.status === "running" && onCancel ? h(CancelButton, {
1550
+ runId: r.id,
1551
+ title: t("cancel.tip", { id: r.id }),
1552
+ onConfirm: onCancel,
1553
+ style: {
1554
+ fontSize: 11,
1555
+ padding: "2px 9px"
1556
+ }
1557
+ }) : null, h("button", {
889
1558
  style: brandBtn,
890
- title: "跳到该 run 的发起会话,并在那个会话的右侧栏打开详情(右侧栏是会话级的:挂到无关会话上没有意义)",
1559
+ title: t("runList.openRightBarTip"),
891
1560
  onClick: (e) => {
892
1561
  e.stopPropagation();
893
1562
  onOpenRun(r);
894
1563
  }
895
- }, "去会话右栏"));
1564
+ }, t("runList.openRightBar")));
896
1565
  }));
897
1566
  }
898
1567
  function BacklogCard({ kind, item, onOpen }) {
@@ -950,12 +1619,12 @@ window.__ModuleLoader__.load({
950
1619
  marginTop: 3,
951
1620
  fontSize: 10,
952
1621
  color: T.text2
953
- } }, item.devAssign ? h("span", null, `dev ${item.devAssign}`) : null, item.qaAssign ? h("span", null, `qa ${item.qaAssign}`) : null, item.acceptBy ? h("span", null, `验收 ${item.acceptBy}`) : null, item.retries ? h("span", { style: { color: T.warn } }, `重试 ${item.retries}`) : null, item.humanIntervention ? h("span", { style: { color: T.error } }, "需人工") : null));
1622
+ } }, 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));
954
1623
  }
955
1624
  /** 可点筛选徽章(多选):单击切换该状态,选中态用状态色实心;再点取消。 */
956
1625
  const filterChip = (text, color, on, onToggle) => h("button", {
957
1626
  onClick: onToggle,
958
- title: on ? "点击取消该状态筛选" : "点击只看该状态(可多选)",
1627
+ title: on ? t("panelBoard.filterChipOn") : t("panelBoard.filterChipOff"),
959
1628
  style: {
960
1629
  font: "inherit",
961
1630
  fontSize: 11,
@@ -993,7 +1662,7 @@ window.__ModuleLoader__.load({
993
1662
  ["task", (backlog.tasks || []).filter((t) => t.type !== "subtask")],
994
1663
  ["bug", backlog.bugs || []]
995
1664
  ];
996
- if (!groups.reduce((a, [, arr]) => a + arr.length, 0)) return muted("backlog 为空(该产品线还没有立项卡片)。", { padding: "2px 2px 8px" });
1665
+ if (!groups.reduce((a, [, arr]) => a + arr.length, 0)) return muted(t("panelBoard.empty"), { padding: "2px 2px 8px" });
997
1666
  return h("div", null, groups.map(([kind, list]) => {
998
1667
  if (!list.length) return null;
999
1668
  const done = list.filter((it) => TERMINAL_STATUSES.indexOf(it.status) !== -1 && !it.humanIntervention);
@@ -1028,33 +1697,40 @@ window.__ModuleLoader__.load({
1028
1697
  fontWeight: 700,
1029
1698
  color: T.text,
1030
1699
  flex: "0 0 auto"
1031
- } }, `${KIND_TITLE[kind]} · ${list.length}`), h("span", { style: {
1700
+ } }, t("panelBoard.groupCount", {
1701
+ kind: kindTitle(kind),
1702
+ n: list.length
1703
+ })), h("span", { style: {
1032
1704
  fontSize: 10,
1033
1705
  color: T.text2,
1034
1706
  flex: "0 0 auto"
1035
- } }, `活动 ${active.length}`), ...Object.keys(byStatus).map((s) => filterChip(`${stText(s)} ${byStatus[s]}`, stColor(s), selSet.has(s), () => toggle(s))), filtering ? h("button", {
1707
+ } }, 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", {
1036
1708
  style: {
1037
1709
  ...panelBtn,
1038
1710
  marginLeft: "auto",
1039
1711
  flex: "0 0 auto"
1040
1712
  },
1041
- title: "清除本组筛选",
1713
+ title: t("panelBoard.clearFilterTip"),
1042
1714
  onClick: () => setFilters((m) => ({
1043
1715
  ...m,
1044
1716
  [kind]: []
1045
1717
  }))
1046
- }, `筛选中 ${sel.length} 项 · 显示 ${matched.length}/${list.length} × 清除`) : done.length ? h("button", {
1718
+ }, t("panel.filtered", {
1719
+ sel: sel.length,
1720
+ shown: matched.length,
1721
+ total: list.length
1722
+ })) : done.length ? h("button", {
1047
1723
  style: {
1048
1724
  ...panelBtn,
1049
1725
  marginLeft: "auto",
1050
1726
  flex: "0 0 auto"
1051
1727
  },
1052
- title: open ? "收起已完成/已关闭卡片" : "展开已完成/已关闭卡片",
1728
+ title: open ? t("panelBoard.collapseDoneTip") : t("panelBoard.expandDoneTip"),
1053
1729
  onClick: () => setShowDone((m) => ({
1054
1730
  ...m,
1055
1731
  [kind]: !open
1056
1732
  }))
1057
- }, open ? `收起已完成 ${done.length}` : `已完成 ${done.length} ▸`) : null), visible.length ? h("div", { style: {
1733
+ }, open ? t("panelBoard.collapseDone", { n: done.length }) : t("panelBoard.expandDone", { n: done.length })) : null), visible.length ? h("div", { style: {
1058
1734
  display: "grid",
1059
1735
  gridTemplateColumns: "repeat(auto-fill, minmax(228px, 1fr))",
1060
1736
  gap: 6
@@ -1063,7 +1739,7 @@ window.__ModuleLoader__.load({
1063
1739
  kind,
1064
1740
  item: it,
1065
1741
  onOpen
1066
- }))) : muted("该筛选下没有卡片。", { fontSize: 10.5 }));
1742
+ }))) : muted(t("panelBoard.emptyFiltered"), { fontSize: 10.5 }));
1067
1743
  }));
1068
1744
  }
1069
1745
  function ItemDetailPane({ det, openArtifact, onClose }) {
@@ -1099,43 +1775,62 @@ window.__ModuleLoader__.load({
1099
1775
  } }, det.id), chip(stText(det.status), stColor(det.status), { dot: true }), h("span", { style: {
1100
1776
  fontSize: 10.5,
1101
1777
  color: T.text2
1102
- } }, KIND_TITLE[det.kind] || det.kind)), h("button", {
1778
+ } }, kindTitle(det.kind))), h("button", {
1103
1779
  style: panelBtn,
1104
1780
  onClick: onClose
1105
- }, "关闭")), h("div", { style: {
1781
+ }, t("common.close"))), h("div", { style: {
1106
1782
  fontSize: 13,
1107
1783
  fontWeight: 600,
1108
1784
  color: T.text,
1109
1785
  lineHeight: 1.45
1110
- } }, det.title), h("div", { style: {
1786
+ } }, det.title), det.kind === "bug" ? h("div", { style: {
1787
+ display: "flex",
1788
+ flexDirection: "column",
1789
+ gap: 4,
1790
+ padding: "8px 10px",
1791
+ borderRadius: 8,
1792
+ background: T.layer2,
1793
+ border: `1px solid ${T.border}`
1794
+ } }, h("div", { style: {
1795
+ ...flexRow,
1796
+ gap: 6
1797
+ } }, h("span", { style: {
1798
+ fontSize: 11,
1799
+ fontWeight: 700,
1800
+ color: T.text
1801
+ } }, 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: {
1802
+ fontSize: 10.5,
1803
+ color: T.warn,
1804
+ lineHeight: 1.5
1805
+ } }, t("panelItem.noDefectDetail"))) : null, h("div", { style: {
1111
1806
  display: "flex",
1112
1807
  flexDirection: "column",
1113
1808
  gap: 3
1114
- } }, row("需求", det.reqId), row("负责人", det.owner), row("开发", det.devAssign), row("测试", det.qaAssign), row("验收", det.assignBy), row("重试", det.retries), row("任务夹", det.runDocs)), det.spec ? h("div", null, h("div", { style: {
1809
+ } }, 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: {
1115
1810
  fontSize: 11,
1116
1811
  color: T.text2,
1117
1812
  marginBottom: 3
1118
- } }, "规格"), h(FoldableText, { text: det.spec })) : null, det.summary ? h("div", null, h("div", { style: {
1813
+ } }, t("panelItem.spec")), h(FoldableText, { text: det.spec })) : null, det.summary ? h("div", null, h("div", { style: {
1119
1814
  fontSize: 11,
1120
1815
  color: T.text2,
1121
1816
  marginBottom: 3
1122
- } }, "结论摘要"), h(FoldableText, { text: det.summary })) : null, det.artifacts && det.artifacts.length ? h("div", null, h("div", { style: {
1817
+ } }, t("panelItem.summary")), h(FoldableText, { text: det.summary })) : null, det.artifacts && det.artifacts.length ? h("div", null, h("div", { style: {
1123
1818
  fontSize: 11,
1124
1819
  color: T.text2,
1125
1820
  marginBottom: 4
1126
- } }, `任务夹产物 · ${det.artifacts.length}`), h("div", { style: {
1821
+ } }, t("panelItem.artifacts", { n: det.artifacts.length })), h("div", { style: {
1127
1822
  ...flexRow,
1128
1823
  gap: 5
1129
1824
  } }, det.artifacts.map((a) => h("button", {
1130
1825
  key: a.name,
1131
1826
  style: brandBtn,
1132
- title: `${a.address}\n(跳到产物所属会话后在该会话右侧栏打开)`,
1827
+ title: t("panelItem.artifactTip", { address: a.address }),
1133
1828
  onClick: () => openArtifact && openArtifact(a.address, a.name, det.runInfo && det.runInfo.ownerSession || null)
1134
- }, a.name)))) : muted("该条目没有可预览的任务夹产物(或缺少会话上下文,无法生成文件地址)。"), det.subtasks && det.subtasks.length ? h("div", null, h("div", { style: {
1829
+ }, a.name)))) : muted(t("panelItem.noArtifacts")), det.subtasks && det.subtasks.length ? h("div", null, h("div", { style: {
1135
1830
  fontSize: 11,
1136
1831
  color: T.text2,
1137
1832
  marginBottom: 4
1138
- } }, `子卡 · ${det.subtasks.length}`), h("div", { style: {
1833
+ } }, t("panelItem.subtasks", { n: det.subtasks.length })), h("div", { style: {
1139
1834
  display: "flex",
1140
1835
  flexDirection: "column",
1141
1836
  gap: 4
@@ -1160,11 +1855,11 @@ window.__ModuleLoader__.load({
1160
1855
  overflow: "hidden",
1161
1856
  textOverflow: "ellipsis",
1162
1857
  whiteSpace: "nowrap"
1163
- } }, s.title), s.failed ? chip("失败", T.error) : null)))) : null, det.bugs && det.bugs.length ? h("div", null, h("div", { style: {
1858
+ } }, s.title), s.failed ? chip(t("common.failed"), T.error) : null)))) : null, det.bugs && det.bugs.length ? h("div", null, h("div", { style: {
1164
1859
  fontSize: 11,
1165
1860
  color: T.text2,
1166
1861
  marginBottom: 4
1167
- } }, `关联缺陷 · ${det.bugs.length}`), h("div", { style: {
1862
+ } }, t("panelItem.bugs", { n: det.bugs.length })), h("div", { style: {
1168
1863
  display: "flex",
1169
1864
  flexDirection: "column",
1170
1865
  gap: 4
@@ -1193,7 +1888,7 @@ window.__ModuleLoader__.load({
1193
1888
  fontSize: 11,
1194
1889
  color: T.text2,
1195
1890
  marginBottom: 4
1196
- } }, `流转时间线 · 最近 ${Math.min(det.events.length, 30)} 条`), h("div", { style: {
1891
+ } }, t("panelItem.events", { n: Math.min(det.events.length, 30) })), h("div", { style: {
1197
1892
  display: "flex",
1198
1893
  flexDirection: "column",
1199
1894
  gap: 3,
@@ -1202,14 +1897,14 @@ window.__ModuleLoader__.load({
1202
1897
  color: T.text2
1203
1898
  } }, det.events.slice(-30).map((e, i) => h("div", { key: i }, `${fmtTime(e.at)} ${e.from || "—"} → ${e.to || "—"}${e.by ? ` · ${e.by}` : ""}${e.reason ? ` · ${e.reason}` : ""}`)))) : null);
1204
1899
  }
1205
- function RunDetailPane({ snap, product, api }) {
1900
+ function RunDetailPane({ snap, product, api, onCancel }) {
1206
1901
  const [sel, setSel] = react.default.useState(null);
1207
1902
  const [err, setErr] = react.default.useState(null);
1208
1903
  react.default.useEffect(() => {
1209
1904
  setSel(null);
1210
1905
  setErr(null);
1211
1906
  }, [snap && snap.id]);
1212
- if (!snap) return muted("未找到该 run(可能已被清理,或地址已过期)。", { padding: 12 });
1907
+ if (!snap) return muted(t("detail.runMissing"), { padding: 12 });
1213
1908
  const stages = snap.stages || [];
1214
1909
  const totals = stages.reduce((a, s) => {
1215
1910
  const u = s.usage;
@@ -1248,7 +1943,7 @@ window.__ModuleLoader__.load({
1248
1943
  } }, h("div", { style: {
1249
1944
  ...flexRow,
1250
1945
  gap: 6
1251
- } }, chip(RUN_STATUS_TEXT[snap.status] || snap.status, stColor(snap.status), { dot: true }), snap.options && snap.options.mode ? chip(String(snap.options.mode), T.text2) : null, h("span", { style: {
1946
+ } }, 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: {
1252
1947
  fontFamily: MONO,
1253
1948
  fontSize: 10.5,
1254
1949
  color: T.text2
@@ -1256,17 +1951,29 @@ window.__ModuleLoader__.load({
1256
1951
  fontSize: 10,
1257
1952
  color: T.text2,
1258
1953
  fontFamily: MONO
1259
- } }, product) : null), h("div", { style: {
1954
+ } }, product) : null, snap.status === "running" && onCancel ? h(CancelButton, {
1955
+ runId: snap.id,
1956
+ title: t("cancel.tip", { id: snap.id }),
1957
+ onConfirm: onCancel,
1958
+ style: {
1959
+ marginLeft: "auto",
1960
+ fontSize: 10.5,
1961
+ padding: "2px 9px"
1962
+ }
1963
+ }) : null), h("div", { style: {
1260
1964
  fontSize: 12,
1261
1965
  color: T.text,
1262
1966
  lineHeight: 1.5
1263
- } }, snap.requirement || "(无需求描述)"), h("div", { style: {
1967
+ } }, snap.requirement || t("runList.noRequirement")), h("div", { style: {
1264
1968
  ...flexRow,
1265
1969
  gap: 12,
1266
1970
  fontSize: 10.5,
1267
1971
  color: T.text2,
1268
1972
  fontFamily: MONO
1269
- } }, h("span", null, `${fmtTime(snap.startedAt)}${snap.endedAt ? ` → ${fmtTime(snap.endedAt)}` : " → 进行中"} · ${fmtDur(snap.startedAt, snap.endedAt)}`), h("span", null, `阶段 ${stages.filter((s) => s.status === "done").length}/${stages.length}`), h("span", null, `子代理 ${snap.agentsStarted || 0}`)), h("div", { style: {
1973
+ } }, 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", {
1974
+ done: stages.filter((s) => s.status === "done").length,
1975
+ total: stages.length
1976
+ })), h("span", null, t("detail.subagents", { n: snap.agentsStarted || 0 }))), h("div", { style: {
1270
1977
  ...flexRow,
1271
1978
  gap: 10,
1272
1979
  fontSize: 10.5,
@@ -1276,7 +1983,7 @@ window.__ModuleLoader__.load({
1276
1983
  borderRadius: 8,
1277
1984
  background: `color-mix(in srgb, ${T.layer2} 60%, transparent)`,
1278
1985
  border: `1px solid ${T.border}`
1279
- } }, h("span", null, `输入(未命中) ${fmtTokens(totals.input) || "0"}`), h("span", null, `输入(命中) ${fmtTokens(totals.cacheRead) || "0"}`), h("span", null, `写缓存 ${fmtTokens(totals.cacheWrite) || "0"}`), h("span", null, `输出 ${fmtTokens(totals.output) || "0"}`), h("span", null, `${totals.calls} 次调用`), h("span", null, `合计 ${fmtTokens(totalTokens(totals)) || "0"}`)), sectionTitle(`阶段 · ${stages.length}`), h("div", { style: {
1986
+ } }, 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: {
1280
1987
  display: "flex",
1281
1988
  flexDirection: "column",
1282
1989
  gap: 4
@@ -1305,7 +2012,7 @@ window.__ModuleLoader__.load({
1305
2012
  fontSize: 11.5,
1306
2013
  color: T.text,
1307
2014
  fontWeight: 500
1308
- } }, s.label || phaseNameOf(s.phase)), chip(stText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
2015
+ } }, stageLabelOf(s)), chip(stageStatusText(s.status), color, { dot: true }), s.outcome && s.outcome !== "completed" && s.outcome !== "cancelled" ? chip(String(s.outcome), stColor(s.outcome)) : null, h("span", { style: {
1309
2016
  marginLeft: "auto",
1310
2017
  fontFamily: MONO,
1311
2018
  fontSize: 10,
@@ -1317,13 +2024,13 @@ window.__ModuleLoader__.load({
1317
2024
  fontFamily: MONO,
1318
2025
  fontSize: 10,
1319
2026
  color: T.text2
1320
- } }, h("span", null, stageUsageLine(s) || "无 token 数据"), s.childId ? h("span", { title: s.childId }, `子代理 ${String(s.childId).slice(0, 14)}`) : null), s.summary ? h("div", { style: {
2027
+ } }, 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: {
1321
2028
  fontSize: 10.5,
1322
2029
  color: T.text2,
1323
2030
  marginTop: 3,
1324
2031
  lineHeight: 1.45
1325
2032
  } }, String(s.summary).slice(0, 200)) : null);
1326
- })), err ? muted(`阶段详情读取失败:${err}`, { color: T.error }) : null, sel ? h("div", { style: {
2033
+ })), err ? muted(t("detail.stageFailed", { err }), { color: T.error }) : null, sel ? h("div", { style: {
1327
2034
  padding: "9px 10px",
1328
2035
  borderRadius: 9,
1329
2036
  border: `1px solid ${T.border}`,
@@ -1341,18 +2048,18 @@ window.__ModuleLoader__.load({
1341
2048
  fontSize: 11.5,
1342
2049
  fontWeight: 700,
1343
2050
  color: T.text
1344
- } }, `阶段 #${sel.seq} 详情`), chip(stText(sel.status), stColor(sel.status))), h("button", {
2051
+ } }, t("detail.stageTitle", { seq: sel.seq })), chip(stageStatusText(sel.status), stColor(sel.status))), h("button", {
1345
2052
  style: panelBtn,
1346
2053
  onClick: () => setSel(null)
1347
- }, "收起")), h("div", { style: {
2054
+ }, t("common.collapse"))), h("div", { style: {
1348
2055
  fontSize: 10.5,
1349
2056
  color: T.text2,
1350
2057
  fontFamily: MONO
1351
- } }, `阶段 token:${stageUsageLine(sel) || ""}`), sel.verifyEvidence ? h("div", null, h("div", { style: {
2058
+ } }, t("token.stageLine", { v: stageUsageLine(sel) || t("common.none") })), sel.verifyEvidence ? h("div", null, h("div", { style: {
1352
2059
  fontSize: 11,
1353
2060
  color: T.success,
1354
2061
  marginBottom: 3
1355
- } }, "🔬 验证证据"), h("div", { style: {
2062
+ } }, t("stage.evidenceTitle")), h("div", { style: {
1356
2063
  whiteSpace: "pre-wrap",
1357
2064
  wordBreak: "break-word",
1358
2065
  fontSize: 11,
@@ -1365,11 +2072,11 @@ window.__ModuleLoader__.load({
1365
2072
  maxHeight: 200,
1366
2073
  overflowY: "auto",
1367
2074
  fontFamily: MONO
1368
- } }, sel.verifyEvidence)) : muted("(缺失——契约未兑现,host 已记警告)", { color: T.warn }), sel.output ? h("div", null, h("div", { style: {
2075
+ } }, sel.verifyEvidence)) : muted(t("stage.evidenceMissingShort"), { color: T.warn }), sel.output ? h("div", null, h("div", { style: {
1369
2076
  fontSize: 11,
1370
2077
  color: T.text2,
1371
2078
  marginBottom: 3
1372
- } }, "阶段产出"), h(FoldableText, {
2079
+ } }, t("detail.output")), h(FoldableText, {
1373
2080
  text: sel.output,
1374
2081
  charLimit: 400,
1375
2082
  lineLimit: 8,
@@ -1381,7 +2088,7 @@ window.__ModuleLoader__.load({
1381
2088
  fontSize: 11,
1382
2089
  color: T.text2,
1383
2090
  marginBottom: 3
1384
- } }, `同任务尝试 · ${sel.attempts.length}`), h("div", { style: {
2091
+ } }, t("detail.attempts", { n: sel.attempts.length })), h("div", { style: {
1385
2092
  display: "flex",
1386
2093
  flexDirection: "column",
1387
2094
  gap: 3
@@ -1394,7 +2101,7 @@ window.__ModuleLoader__.load({
1394
2101
  fontFamily: MONO,
1395
2102
  color: T.text2
1396
2103
  }
1397
- }, h("span", null, `#${a.seq}`), chip(stText(a.status), stColor(a.status)), a.outcome ? h("span", null, a.outcome) : null, h("span", { style: { marginLeft: "auto" } }, `${fmtTime(a.startedAt)} · ${fmtDur(a.startedAt, a.endedAt)}`))))) : null) : null, logs.length ? h("div", null, sectionTitle(`日志 · 最近 ${logs.length} 条`), h("div", { style: {
2104
+ }, h("span", null, `#${a.seq}`), chip(stageStatusText(a.status), stColor(a.status)), a.outcome && a.outcome !== "cancelled" ? h("span", null, a.outcome) : null, h("span", { style: { marginLeft: "auto" } }, `${fmtTime(a.startedAt)} · ${fmtDur(a.startedAt, a.endedAt)}`))))) : null) : null, logs.length ? h("div", null, sectionTitle(t("detail.logs", { n: logs.length })), h("div", { style: {
1398
2105
  display: "flex",
1399
2106
  flexDirection: "column",
1400
2107
  gap: 2,
@@ -1447,12 +2154,12 @@ window.__ModuleLoader__.load({
1447
2154
  let alive = true;
1448
2155
  if (!runId) {
1449
2156
  setSnap(null);
1450
- setErr(address ? `无法解析 run 地址:${address}` : readTab ? "读取 tab 地址中…" : "宿主 tab 信息钩子不可用(useTabInfo 缺失)");
2157
+ setErr(address ? t("tab.resolveFailed", { address }) : readTab ? t("tab.readingAddress") : t("tab.noHook"));
1451
2158
  return;
1452
2159
  }
1453
2160
  if (!api) {
1454
2161
  setSnap(null);
1455
- setErr("remote 不可用(插件未挂载或版本过旧)");
2162
+ setErr(t("tab.remoteUnavailable"));
1456
2163
  return;
1457
2164
  }
1458
2165
  api.runDetail(runId).then((v) => {
@@ -1481,6 +2188,16 @@ window.__ModuleLoader__.load({
1481
2188
  nonce,
1482
2189
  readTab
1483
2190
  ]);
2191
+ /** 中断后重新拉快照(nonce 触发上面那个 effect 重跑;右栏 tab 是会话级地址,与产品线选择无关)。 */
2192
+ const onCancel = async (rid) => {
2193
+ if (!api) return;
2194
+ try {
2195
+ await api.cancel(rid);
2196
+ setNonce((n) => n + 1);
2197
+ } catch (e) {
2198
+ setErr(String(e && e.message || e));
2199
+ }
2200
+ };
1484
2201
  if (err && !snap) {
1485
2202
  const pending = !address && !!readTab;
1486
2203
  return h("div", { style: {
@@ -1488,19 +2205,20 @@ window.__ModuleLoader__.load({
1488
2205
  display: "flex",
1489
2206
  flexDirection: "column",
1490
2207
  gap: 8
1491
- } }, muted(pending ? "读取 tab 地址中…" : err, { color: pending ? T.text2 : T.error }), pending ? null : h("button", {
2208
+ } }, muted(pending ? t("tab.readingAddress") : err, { color: pending ? T.text2 : T.error }), pending ? null : h("button", {
1492
2209
  style: panelBtn,
1493
2210
  onClick: () => {
1494
2211
  setErr(null);
1495
2212
  setNonce((n) => n + 1);
1496
2213
  }
1497
- }, "重试"));
2214
+ }, t("common.retry")));
1498
2215
  }
1499
- if (!snap) return muted("读取 run 详情中…", { padding: 12 });
2216
+ if (!snap) return muted(t("tab.readingRun"), { padding: 12 });
1500
2217
  return h(RunDetailPane, {
1501
2218
  snap,
1502
2219
  product,
1503
- api
2220
+ api,
2221
+ onCancel
1504
2222
  });
1505
2223
  }
1506
2224
  function productApi(remote, product) {
@@ -1508,7 +2226,13 @@ window.__ModuleLoader__.load({
1508
2226
  view: async () => unwrap$1(await remote.productView(product), "productView"),
1509
2227
  runDetail: async (runId) => unwrap$1(await remote.productRunDetail(product, runId), "productRunDetail"),
1510
2228
  stageDetail: async (runId, seq) => unwrap$1(await remote.productStageDetail(product, runId, seq), "productStageDetail"),
1511
- itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail")
2229
+ itemDetail: async (kind, id, sessionId) => unwrap$1(await remote.productItemDetail(product, kind, id, sessionId), "productItemDetail"),
2230
+ /** 中断运行:runId 全局寻址(与产品线无关),放这里是为了让面板行/详情/右栏共用同一处解包与报错。 */
2231
+ cancel: async (runId) => {
2232
+ const r = unwrap$1(await remote.cancel(runId), "cancel");
2233
+ if (!r || r.ok !== true) throw new Error(t("cancel.failed"));
2234
+ return true;
2235
+ }
1512
2236
  };
1513
2237
  }
1514
2238
  /** 全局面板 props:root scope 标准 props(useSessions 取当前会话)+ 插件注入(remote/打开回调)。 */
@@ -1545,7 +2269,7 @@ window.__ModuleLoader__.load({
1545
2269
  const attempt = (quiet) => !!(props.openResource && address && props.openResource(address, label, quiet));
1546
2270
  if (attempt(false)) return;
1547
2271
  if (!address) {
1548
- setHint(`${label}:host 未生成可打开的地址(可能缺少会话上下文)`);
2272
+ setHint(t("panel.hintNoAddress", { label }));
1549
2273
  if (fallback) fallback();
1550
2274
  return;
1551
2275
  }
@@ -1560,7 +2284,7 @@ window.__ModuleLoader__.load({
1560
2284
  setTimeout(tick, 120);
1561
2285
  return;
1562
2286
  }
1563
- setHint(`右侧栏打开失败(宿主只在对话视图挂载它):${label}${fallback ? "——已在本面板内联显示" : ""}`);
2287
+ setHint(t("panel.hintRightbarFailed", { label }) + (fallback ? t("panel.hintInlineSuffix") : ""));
1564
2288
  if (fallback) fallback();
1565
2289
  };
1566
2290
  setTimeout(tick, 140);
@@ -1568,8 +2292,10 @@ window.__ModuleLoader__.load({
1568
2292
  /**
1569
2293
  * **跳到资源所属的会话,再在那个会话的右栏打开**(全局面板的正确语义)。
1570
2294
  * 右侧栏是会话级的:从全局面板看 tetris 的 run 却把 tab 挂到"用户当前所在会话"上没有意义
1571
- * (用户 2026-09-11 提出)。所以先 `sessions.open(ownerSession)`,等当前会话真的切过去、
1572
- * 且对话 seat 挂载 bind 之后再 openResource(两者都要等,故小步重试 + 就绪判据)。
2295
+ * (用户 2026-09-11 提出)。所以先 `uiWorkspace.openSession(ownerSession)`,再小步重试等右栏 seat 就绪后 openResource。
2296
+ * **2026-09-23 迁移(宿主 0.1.7-alpha.1)**:`sessions.open` `sessions.openSubagent` 同批被移除,
2297
+ * 跳会话唯一入口改为 `uiWorkspace.openSession(target)`;同时 `SessionListState` 已无 `current` 字段,
2298
+ * 故原先"等当前会话真的切过去"的判据删除(契约变更后它恒为 undefined,等于死代码),只留时间维度的重试。
1573
2299
  * @param target.ownerSession - 资源所属会话(run 的发起会话 / 产物地址里的会话)
1574
2300
  * @param target.address - host 生成的 dsh-resource 地址
1575
2301
  * @param target.label - 提示用的名字
@@ -1577,15 +2303,15 @@ window.__ModuleLoader__.load({
1577
2303
  */
1578
2304
  const goOwnerSessionAndOpen = (target) => {
1579
2305
  const { ownerSession, address, label, fallback } = target || {};
1580
- const sessions = props.sessions;
1581
- if (!ownerSession || !sessions || typeof sessions.open !== "function") {
2306
+ const ws = props.uiWorkspace;
2307
+ if (!ownerSession || !ws || typeof ws.openSession !== "function") {
1582
2308
  openInConversationRightbar(address, label, fallback);
1583
2309
  return;
1584
2310
  }
1585
2311
  try {
1586
- sessions.open(ownerSession);
2312
+ ws.openSession(ownerSession);
1587
2313
  } catch (e) {
1588
- setHint(`发起会话 ${String(ownerSession).slice(0, 8)}… 不在会话列表里(可能已被清理)——已在本面板展示详情`);
2314
+ setHint(t("panel.hintSessionGone", { sid: String(ownerSession).slice(0, 8) }));
1589
2315
  if (fallback) fallback();
1590
2316
  return;
1591
2317
  }
@@ -1595,18 +2321,12 @@ window.__ModuleLoader__.load({
1595
2321
  let tries = 0;
1596
2322
  const tick = () => {
1597
2323
  tries += 1;
1598
- let nowCurrent = null;
1599
- try {
1600
- nowCurrent = sessions.list && sessions.list.getSnapshot ? sessions.list.getSnapshot().current : null;
1601
- } catch (e) {
1602
- nowCurrent = null;
1603
- }
1604
- if ((nowCurrent === ownerSession || tries >= 6) && props.openResource && address && props.openResource(address, label, tries < 6)) return;
2324
+ if (props.openResource && address && props.openResource(address, label, tries < 6)) return;
1605
2325
  if (tries < 14) {
1606
2326
  setTimeout(tick, 130);
1607
2327
  return;
1608
2328
  }
1609
- setHint(`已切到发起会话,但右栏没打开(${label})——可在该会话里用「⇥ 右栏打开」重试`);
2329
+ setHint(t("panel.hintSwitchedNoRightbar", { label }));
1610
2330
  };
1611
2331
  setTimeout(tick, 140);
1612
2332
  };
@@ -1614,7 +2334,7 @@ window.__ModuleLoader__.load({
1614
2334
  if (!remote || typeof remote.products !== "function") {
1615
2335
  setState((s) => ({
1616
2336
  ...s,
1617
- err: "remote.products 不可用(插件未挂载或版本过旧)"
2337
+ err: t("panel.remoteProductsUnavailable")
1618
2338
  }));
1619
2339
  return;
1620
2340
  }
@@ -1731,6 +2451,25 @@ window.__ModuleLoader__.load({
1731
2451
  }
1732
2452
  };
1733
2453
  const closeDetail = () => setDetail(null);
2454
+ /** 中断运行(面板内两个入口共用):成功后刷新产品线视图;详情浮层开着就顺手把快照也换新。 */
2455
+ const cancelRun = async (runId) => {
2456
+ if (!api) return;
2457
+ try {
2458
+ await api.cancel(runId);
2459
+ loadView(state.current, true);
2460
+ if (detail && detail.kind === "run" && detail.data && detail.data.id === runId) setDetail({
2461
+ kind: "run",
2462
+ data: await api.runDetail(runId),
2463
+ run: detail.run
2464
+ });
2465
+ setHint(t("cancel.sent"));
2466
+ } catch (e) {
2467
+ setState((s) => ({
2468
+ ...s,
2469
+ err: String(e && e.message || e)
2470
+ }));
2471
+ }
2472
+ };
1734
2473
  const openRun = (r) => {
1735
2474
  goOwnerSessionAndOpen({
1736
2475
  ownerSession: r.ownerSession,
@@ -1802,32 +2541,32 @@ window.__ModuleLoader__.load({
1802
2541
  } }, h("span", { style: {
1803
2542
  fontSize: 13,
1804
2543
  fontWeight: 700
1805
- } }, "🏭 团队工作台"), h("span", { style: {
2544
+ } }, t("panel.title")), h("span", { style: {
1806
2545
  fontSize: 11,
1807
2546
  color: T.text2
1808
- } }, "全局面板 · 按产品线"), currentSessionId ? h("span", { style: {
2547
+ } }, t("panel.subtitle")), currentSessionId ? h("span", { style: {
1809
2548
  fontSize: 10,
1810
2549
  color: T.text2,
1811
2550
  fontFamily: MONO
1812
- } }, `当前会话 ${String(currentSessionId).slice(0, 8)}`) : h("span", { style: {
2551
+ } }, t("panel.currentSession", { sid: String(currentSessionId).slice(0, 8) })) : h("span", { style: {
1813
2552
  fontSize: 10,
1814
2553
  color: T.text2
1815
- } }, "无当前会话")), h("div", { style: {
2554
+ } }, t("panel.noSession"))), h("div", { style: {
1816
2555
  ...flexRow,
1817
2556
  gap: 6
1818
2557
  } }, product ? h("button", {
1819
2558
  style: panelBtn,
1820
2559
  onClick: () => loadView(state.current)
1821
- }, "刷新数据") : null, h("button", {
2560
+ }, t("panel.reload")) : null, h("button", {
1822
2561
  style: panelBtn,
1823
- title: "回到对话(再点侧边栏图标即可切回本面板)",
2562
+ title: t("panel.backToChatTip"),
1824
2563
  onClick: () => {
1825
2564
  try {
1826
2565
  const layout = props.layout;
1827
2566
  if (layout && layout.selectPanel) layout.selectPanel(null);
1828
2567
  } catch (e) {}
1829
2568
  }
1830
- }, "回到对话"))), state.err ? h("div", { style: {
2569
+ }, t("panel.backToChat")))), state.err ? h("div", { style: {
1831
2570
  padding: "6px 14px",
1832
2571
  fontSize: 11,
1833
2572
  color: T.error,
@@ -1844,7 +2583,7 @@ window.__ModuleLoader__.load({
1844
2583
  } }, h("span", null, hint), h("button", {
1845
2584
  style: panelBtn,
1846
2585
  onClick: () => setHint(null)
1847
- }, "知道了")) : null, h("div", { style: {
2586
+ }, t("common.gotIt"))) : null, h("div", { style: {
1848
2587
  flex: 1,
1849
2588
  minHeight: 0,
1850
2589
  display: "flex",
@@ -1862,7 +2601,7 @@ window.__ModuleLoader__.load({
1862
2601
  minHeight: 0,
1863
2602
  display: "flex",
1864
2603
  flexDirection: "column"
1865
- } }, !state.current ? h("div", { style: { padding: "12px 14px" } }, muted("选择左侧产品线查看 backlog 与 run(首次进入默认选最近更新的产品线)。")) : !view ? h("div", { style: { padding: "12px 14px" } }, muted("读取产品线数据中…")) : h(react.default.Fragment, null, h("div", { style: {
2604
+ } }, !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: {
1866
2605
  ...flexRow,
1867
2606
  justifyContent: "space-between",
1868
2607
  gap: 10,
@@ -1883,21 +2622,21 @@ window.__ModuleLoader__.load({
1883
2622
  ...flexRow,
1884
2623
  gap: 6,
1885
2624
  flex: "0 0 auto"
1886
- } }, chip(`run ${product.totalRuns}`, T.text2), product.activeRuns > 0 ? chip(`活跃 ${product.activeRuns}`, T.brand, { dot: true }) : null, product.lastVerdict ? chip(`验收 ${product.lastVerdict}`, stColor("accepted")) : null)), h("div", { style: {
2625
+ } }, 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: {
1887
2626
  display: "flex",
1888
2627
  alignItems: "flex-end",
1889
2628
  gap: 2,
1890
2629
  padding: "0 14px",
1891
2630
  borderBottom: `1px solid ${T.border}`
1892
- } }, panelTabBtn("run", `🚀 流水线 run · ${runs.length}`), panelTabBtn("backlog", `📋 Backlog · ${backlogCount}`), h("div", { style: {
2631
+ } }, panelTabBtn("run", t("panel.tabRuns", { n: runs.length })), panelTabBtn("backlog", t("panel.tabBacklog", { n: backlogCount })), h("div", { style: {
1893
2632
  marginLeft: "auto",
1894
2633
  ...flexRow,
1895
2634
  gap: 6,
1896
2635
  paddingBottom: 7
1897
- } }, panelTab === "run" && !runsExpanded && pinnedActive.length > 0 ? chip(`已置顶进行中 ${pinnedActive.length}`, T.brand, { dot: true }) : null, panelTab === "run" && runsMatched.length > RUN_PREVIEW ? h("button", {
2636
+ } }, 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", {
1898
2637
  style: panelBtn,
1899
2638
  onClick: () => setRunsExpanded((v) => !v)
1900
- }, runsExpanded ? `只看最近 ${RUN_PREVIEW} 条` : `展开全部 ${runsMatched.length} 条`) : null)), h("div", { style: {
2639
+ }, runsExpanded ? t("panel.showRecent", { n: RUN_PREVIEW }) : t("panel.showAll", { n: runsMatched.length })) : null)), h("div", { style: {
1901
2640
  flex: 1,
1902
2641
  minHeight: 0,
1903
2642
  overflowY: "auto",
@@ -1907,19 +2646,24 @@ window.__ModuleLoader__.load({
1907
2646
  ...flexRow,
1908
2647
  gap: 6,
1909
2648
  marginBottom: 8
1910
- } }, ...RUN_STATUS_ORDER.filter((st) => runStatusCounts[st]).map((st) => filterChip(`${RUN_STATUS_TEXT[st] || st} ${runStatusCounts[st]}`, stColor(st), runSelSet.has(st), () => toggleRunStatus(st))), runFiltering ? h("button", {
2649
+ } }, ...RUN_STATUS_ORDER.filter((st) => runStatusCounts[st]).map((st) => filterChip(`${runStatusText(st)} ${runStatusCounts[st]}`, stColor(st), runSelSet.has(st), () => toggleRunStatus(st))), runFiltering ? h("button", {
1911
2650
  style: panelBtn,
1912
- title: "清除 run 状态筛选",
2651
+ title: t("panel.clearRunFilterTip"),
1913
2652
  onClick: () => setRunFilter([])
1914
- }, `筛选中 ${runSel.length} 项 · 显示 ${runsMatched.length}/${runs.length} × 清除`) : null), visibleRuns.length ? h(RunList, {
2653
+ }, t("panel.filtered", {
2654
+ sel: runSel.length,
2655
+ shown: runsMatched.length,
2656
+ total: runs.length
2657
+ })) : null), visibleRuns.length ? h(RunList, {
1915
2658
  runs: visibleRuns,
1916
2659
  activeRunId: detail && detail.kind === "run" && detail.run ? detail.run.id : null,
1917
2660
  onOpenRun: openRun,
1918
- onInlineRun: showInline
1919
- }) : muted("该筛选下没有 run。", { fontSize: 10.5 }), muted("点一行看详情浮层;「去会话右栏」= 跳到该 run 的发起会话并在其右侧栏打开(与任务夹产物并排)", {
2661
+ onInlineRun: showInline,
2662
+ onCancel: cancelRun
2663
+ }) : muted(t("panel.emptyRunFilter"), { fontSize: 10.5 }), muted(t("panel.runHint"), {
1920
2664
  fontSize: 10,
1921
2665
  marginTop: 8
1922
- })) : h(react.default.Fragment, null, muted("点状态徽章可筛选(多选);终态卡片默认收起,筛选时自动显示", {
2666
+ })) : h(react.default.Fragment, null, muted(t("panel.boardHint"), {
1923
2667
  fontSize: 10,
1924
2668
  marginBottom: 8
1925
2669
  }), h(BacklogGroups, {
@@ -1952,24 +2696,25 @@ window.__ModuleLoader__.load({
1952
2696
  fontSize: 11.5,
1953
2697
  fontWeight: 700,
1954
2698
  color: T.text
1955
- } }, "run 详情"), h("div", { style: {
2699
+ } }, t("panel.runDetail")), h("div", { style: {
1956
2700
  ...flexRow,
1957
2701
  gap: 6
1958
2702
  } }, detail && detail.data && detail.data.address ? h("button", {
1959
2703
  style: brandBtn,
1960
- title: "跳到该 run 的发起会话,并在那个会话的右侧栏打开(与任务夹产物并排看)",
2704
+ title: t("panel.goOwnerSessionTip"),
1961
2705
  onClick: () => goOwnerSessionAndOpen({
1962
2706
  ownerSession: detail.data.ownerSession,
1963
2707
  address: detail.data.address,
1964
2708
  label: detail.data.id
1965
2709
  })
1966
- }, "去发起会话") : null, h("button", {
2710
+ }, t("panel.goOwnerSession")) : null, h("button", {
1967
2711
  style: panelBtn,
1968
2712
  onClick: closeDetail
1969
- }, "关闭"))), h(RunDetailPane, {
2713
+ }, t("common.close")))), h(RunDetailPane, {
1970
2714
  snap: detail && detail.data,
1971
2715
  product: state.current,
1972
- api
2716
+ api,
2717
+ onCancel: cancelRun
1973
2718
  }))) : null));
1974
2719
  }
1975
2720
  //#endregion
@@ -1991,7 +2736,7 @@ window.__ModuleLoader__.load({
1991
2736
  const inject = [
1992
2737
  "remote",
1993
2738
  "slots",
1994
- "sessions",
2739
+ "uiWorkspace",
1995
2740
  "locale"
1996
2741
  ];
1997
2742
  const NODE_W = 300;
@@ -2012,7 +2757,7 @@ window.__ModuleLoader__.load({
2012
2757
  let maxH = 0;
2013
2758
  groups.forEach((g, i) => {
2014
2759
  const anyRun = g.stages.some((s) => s.status === "running");
2015
- const anyFail = g.stages.some((s) => s.status === "failed" || s.status === "needs-human" || s.status === "cancelled");
2760
+ const anyFail = g.stages.some((s) => s.status === "failed" || s.status === "needs-human");
2016
2761
  const allDone = g.stages.length > 0 && g.stages.every((s) => s.status === "done");
2017
2762
  const headColor = anyRun ? T.brand : anyFail ? T.error : allDone ? T.success : T.text2;
2018
2763
  const h = 48 + g.stages.reduce((a, s) => a + cardH(s), 0) + Math.max(0, g.stages.length - 1) * 7;
@@ -2059,7 +2804,7 @@ window.__ModuleLoader__.load({
2059
2804
  const usage = stageUsageLine(s);
2060
2805
  return h("div", {
2061
2806
  key,
2062
- title: `${s.label} —— 点击查看阶段详情`,
2807
+ title: t("stage.cardTip", { label: stageLabelOf(s) }),
2063
2808
  onMouseDown: (e) => e.stopPropagation(),
2064
2809
  onClick: () => onOpen && onOpen(s),
2065
2810
  style: {
@@ -2108,8 +2853,11 @@ window.__ModuleLoader__.load({
2108
2853
  textOverflow: "ellipsis",
2109
2854
  whiteSpace: "nowrap"
2110
2855
  }
2111
- }, s.__taskKey || s.label), s.attempts && s.attempts.length > 1 ? h("span", {
2112
- title: `重试 ${s.attempts.length - 1} 次(共 ${s.attempts.length} 次尝试)`,
2856
+ }, stageLabelOf(s)), s.attempts && s.attempts.length > 1 ? h("span", {
2857
+ title: t("stage.retryTip", {
2858
+ n: s.attempts.length - 1,
2859
+ m: s.attempts.length
2860
+ }),
2113
2861
  style: {
2114
2862
  fontFamily: MONO,
2115
2863
  fontSize: 10,
@@ -2121,7 +2869,7 @@ window.__ModuleLoader__.load({
2121
2869
  lineHeight: "15px",
2122
2870
  flex: "0 0 auto"
2123
2871
  }
2124
- }, `↻${s.attempts.length - 1}`) : null, chip(stText(s.status), color, { dot: true }), h("span", { style: {
2872
+ }, `↻${s.attempts.length - 1}`) : null, chip(stageStatusText(s.status), color, { dot: true }), h("span", { style: {
2125
2873
  color: T.text2,
2126
2874
  fontSize: 11,
2127
2875
  opacity: .5
@@ -2234,7 +2982,7 @@ window.__ModuleLoader__.load({
2234
2982
  /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。
2235
2983
  * 2026-09-06 状态机化:同任务多次尝试 → 顶部尝试时间线 + 选中展开(默认最新);
2236
2984
  * 单次尝试保持现状(不渲染时间线)。 */
2237
- function StageDetailDrawer({ det, onClose, sessionId, sessions }) {
2985
+ function StageDetailDrawer({ det, onClose, sessionId, uiWorkspace }) {
2238
2986
  const [sel, setSel] = react.default.useState(null);
2239
2987
  const st = det.stage;
2240
2988
  const d = det.data;
@@ -2245,18 +2993,18 @@ window.__ModuleLoader__.load({
2245
2993
  const ownerSession = d && d.ownerSession ? String(d.ownerSession) : null;
2246
2994
  const mySession = sessionId ? String(sessionId) : null;
2247
2995
  const crossSession = !!ownerSession && !!mySession && ownerSession !== mySession;
2248
- const hasChild = !!(!crossSession && cur && cur.childId && sessions && typeof sessions.openSubagent === "function");
2996
+ const hasChild = !!(!crossSession && cur && cur.childId && uiWorkspace && typeof uiWorkspace.openSession === "function");
2249
2997
  const openChild = () => {
2250
2998
  if (crossSession || !hasChild) return;
2251
2999
  try {
2252
- sessions.openSubagent({
3000
+ uiWorkspace.openSession({
2253
3001
  parentSessionId: ownerSession || sessionId,
2254
3002
  childSessionId: cur.childId,
2255
3003
  mode: "one-shot"
2256
3004
  });
2257
3005
  } catch (e) {}
2258
3006
  };
2259
- const outText = cur && cur.output ? cur.output : cur && cur.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${cur.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
3007
+ 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");
2260
3008
  const closeBtn = {
2261
3009
  font: "inherit",
2262
3010
  width: 26,
@@ -2318,16 +3066,16 @@ window.__ModuleLoader__.load({
2318
3066
  textOverflow: "ellipsis",
2319
3067
  whiteSpace: "nowrap"
2320
3068
  }
2321
- }, st ? st.label : "阶段详情"), h("div", { style: {
3069
+ }, st ? stageLabelOf(st) : t("stage.detailTitle")), h("div", { style: {
2322
3070
  fontSize: 10.5,
2323
3071
  color: T.text2,
2324
3072
  marginTop: 1,
2325
3073
  fontFamily: MONO,
2326
3074
  fontVariantNumeric: "tabular-nums"
2327
- } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stText(st.status), color, { dot: true }) : null, h("button", {
3075
+ } }, `${st ? `#${st.seq} · ${st.phase}` : ""}${st && (st.startedAt || st.endedAt) ? ` · ${fmtDur(st.startedAt, st.endedAt)}` : ""}`)), st ? chip(stageStatusText(st.status), color, { dot: true }) : null, h("button", {
2328
3076
  onClick: onClose,
2329
3077
  style: closeBtn,
2330
- title: "关闭"
3078
+ title: t("common.close")
2331
3079
  }, "✕")), h("div", { style: {
2332
3080
  flex: 1,
2333
3081
  overflowY: "auto",
@@ -2344,7 +3092,7 @@ window.__ModuleLoader__.load({
2344
3092
  fontWeight: 700,
2345
3093
  color: T.text2,
2346
3094
  letterSpacing: .3
2347
- } }, "TOKEN · 官方口径"), h("span", { style: {
3095
+ } }, t("token.officialTitle")), h("span", { style: {
2348
3096
  fontSize: 11.5,
2349
3097
  fontFamily: MONO,
2350
3098
  color: T.text,
@@ -2358,7 +3106,7 @@ window.__ModuleLoader__.load({
2358
3106
  fontWeight: 700,
2359
3107
  color: T.text2,
2360
3108
  letterSpacing: .3
2361
- } }, `↻ 尝试历史(${attempts.length} 次 · 点击查看该次详情)`), h("div", { style: {
3109
+ } }, t("stage.attemptHistory", { n: attempts.length })), h("div", { style: {
2362
3110
  display: "flex",
2363
3111
  flexDirection: "column",
2364
3112
  gap: 4
@@ -2389,7 +3137,7 @@ window.__ModuleLoader__.load({
2389
3137
  color: aColor,
2390
3138
  flex: "0 0 64px",
2391
3139
  fontWeight: 700
2392
- } }, a.status === "done" ? "✅ 成功" : a.status === "failed" ? `❌ ${a.outcome || "失败"}` : " 进行中"), h("span", { style: {
3140
+ } }, a.status === "done" ? t("stage.attemptDone") : a.status === "failed" ? t("stage.attemptFailed", { outcome: a.outcome || t("common.failed") }) : a.status === "cancelled" ? t("stageStatus.cancelled") : t("stage.attemptRunning")), h("span", { style: {
2393
3141
  flex: 1,
2394
3142
  minWidth: 0,
2395
3143
  fontSize: 10.5,
@@ -2397,7 +3145,7 @@ window.__ModuleLoader__.load({
2397
3145
  overflow: "hidden",
2398
3146
  textOverflow: "ellipsis",
2399
3147
  whiteSpace: "nowrap"
2400
- } }, (a.summary || a.outcome || "(无摘要)").slice(0, 80)), h("span", { style: {
3148
+ } }, (a.summary || a.outcome || t("common.noSummary")).slice(0, 80)), h("span", { style: {
2401
3149
  fontFamily: MONO,
2402
3150
  fontSize: 10,
2403
3151
  color: T.text2,
@@ -2415,7 +3163,7 @@ window.__ModuleLoader__.load({
2415
3163
  } }, h("button", {
2416
3164
  onClick: openChild,
2417
3165
  disabled: !hasChild,
2418
- title: crossSession ? `该子代理由会话 ${ownerSession.slice(-6)} 发起,跨会话跳转暂不支持——请打开其发起会话的团队工作台查看` : hasChild ? "跳转到该阶段子代理会话(完整推理与工具调用轨迹);跳转后请切换「对话」tab 查看" : "该阶段无可用子代理会话",
3166
+ title: crossSession ? t("stage.childCrossSessionTip", { sid: ownerSession.slice(-6) }) : hasChild ? t("stage.childJumpTip") : t("stage.childNoneTip"),
2419
3167
  style: {
2420
3168
  font: "inherit",
2421
3169
  fontSize: 12,
@@ -2432,17 +3180,17 @@ window.__ModuleLoader__.load({
2432
3180
  color: T.brand,
2433
3181
  opacity: hasChild ? 1 : .45
2434
3182
  }
2435
- }, "🎬 跳转子代理会话"), crossSession ? h("div", { style: {
3183
+ }, t("stage.childJumpBtn")), crossSession ? h("div", { style: {
2436
3184
  fontSize: 10.5,
2437
3185
  color: T.text2,
2438
3186
  textAlign: "center",
2439
3187
  lineHeight: 1.55
2440
- } }, `跨会话暂不支持:该子代理由会话 ${ownerSession ? ownerSession.slice(-6) : ""} 发起。如需查看轨迹,请打开其发起会话的团队工作台。`) : hasChild ? h("div", { style: {
3188
+ } }, t("stage.childCrossSessionNote", { sid: ownerSession ? ownerSession.slice(-6) : "" })) : hasChild ? h("div", { style: {
2441
3189
  fontSize: 10.5,
2442
3190
  color: T.text2,
2443
3191
  textAlign: "center",
2444
3192
  lineHeight: 1.55
2445
- } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
3193
+ } }, t("stage.childJumpNote")) : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
2446
3194
  display: "flex",
2447
3195
  flexDirection: "column",
2448
3196
  gap: 5
@@ -2451,7 +3199,7 @@ window.__ModuleLoader__.load({
2451
3199
  fontWeight: 700,
2452
3200
  color: T.text2,
2453
3201
  letterSpacing: .3
2454
- } }, "🔬 验证证据"), cur && cur.verifyEvidence ? h("div", { style: {
3202
+ } }, t("stage.evidenceTitle")), cur && cur.verifyEvidence ? h("div", { style: {
2455
3203
  whiteSpace: "pre-wrap",
2456
3204
  wordBreak: "break-word",
2457
3205
  fontSize: 11.5,
@@ -2472,7 +3220,7 @@ window.__ModuleLoader__.load({
2472
3220
  borderRadius: 10,
2473
3221
  padding: "8px 12px",
2474
3222
  lineHeight: 1.55
2475
- } }, "(缺失——契约未兑现,host 已记警告;可与 logs/teamflow/<runId>/ 命令输出日志对照)")) : null, h("div", { style: {
3223
+ } }, t("stage.evidenceMissing"))) : null, h("div", { style: {
2476
3224
  display: "flex",
2477
3225
  flexDirection: "column",
2478
3226
  gap: 5
@@ -2481,7 +3229,7 @@ window.__ModuleLoader__.load({
2481
3229
  fontWeight: 700,
2482
3230
  color: T.text2,
2483
3231
  letterSpacing: .3
2484
- } }, "📄 阶段性产物"), h("div", { style: {
3232
+ } }, t("stage.artifactsTitle")), h("div", { style: {
2485
3233
  whiteSpace: "pre-wrap",
2486
3234
  wordBreak: "break-word",
2487
3235
  fontSize: 12,
@@ -2495,7 +3243,7 @@ window.__ModuleLoader__.load({
2495
3243
  overflowY: "auto"
2496
3244
  } }, outText))));
2497
3245
  }
2498
- function PipelinePanel({ active, api, runId, sessionId, sessions }) {
3246
+ function PipelinePanel({ active, api, runId, sessionId, uiWorkspace }) {
2499
3247
  if (!active) return h("div", { style: {
2500
3248
  color: T.text2,
2501
3249
  fontSize: 13,
@@ -2504,7 +3252,7 @@ window.__ModuleLoader__.load({
2504
3252
  } }, h("div", { style: {
2505
3253
  fontSize: 28,
2506
3254
  marginBottom: 8
2507
- } }, "🏭"), "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求");
3255
+ } }, "🏭"), t("pipeline.empty"));
2508
3256
  const groups = [];
2509
3257
  const taskKeyOf = (s) => String(s.taskKey || String(s.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
2510
3258
  for (const st of active.stages || []) {
@@ -2779,7 +3527,7 @@ window.__ModuleLoader__.load({
2779
3527
  justifyContent: "center",
2780
3528
  color: T.text2,
2781
3529
  fontSize: 13
2782
- } }, "流水线还没有开始执行节点") : null, h("div", {
3530
+ } }, t("pipeline.noNodes")) : null, h("div", {
2783
3531
  onMouseDown: (e) => e.stopPropagation(),
2784
3532
  style: {
2785
3533
  position: "absolute",
@@ -2797,7 +3545,7 @@ window.__ModuleLoader__.load({
2797
3545
  boxShadow: "0 6px 20px rgba(0,0,0,.16)"
2798
3546
  }
2799
3547
  }, h("button", {
2800
- title: "缩小",
3548
+ title: t("pipeline.zoomOut"),
2801
3549
  onClick: () => zoomBy(.86),
2802
3550
  style: zoomStyle
2803
3551
  }, "−"), h("span", { style: {
@@ -2807,7 +3555,7 @@ window.__ModuleLoader__.load({
2807
3555
  minWidth: 34,
2808
3556
  textAlign: "center"
2809
3557
  } }, `${Math.round(view.s * 100)}%`), h("button", {
2810
- title: "放大",
3558
+ title: t("pipeline.zoomIn"),
2811
3559
  onClick: () => zoomBy(1.16),
2812
3560
  style: zoomStyle
2813
3561
  }, "+"), h("span", { style: {
@@ -2815,7 +3563,7 @@ window.__ModuleLoader__.load({
2815
3563
  height: 14,
2816
3564
  background: T.border
2817
3565
  } }), h("button", {
2818
- title: "适应画布",
3566
+ title: t("pipeline.fitCanvas"),
2819
3567
  onClick: fitNow,
2820
3568
  style: {
2821
3569
  ...zoomStyle,
@@ -2830,11 +3578,11 @@ window.__ModuleLoader__.load({
2830
3578
  color: T.text2,
2831
3579
  paddingRight: 4,
2832
3580
  opacity: .85
2833
- } }, "✥ 拖动画布 · 滚轮缩放")), det ? h(StageDetailDrawer, {
3581
+ } }, t("pipeline.canvasHint"))), det ? h(StageDetailDrawer, {
2834
3582
  det,
2835
3583
  onClose: closeDet,
2836
3584
  sessionId,
2837
- sessions
3585
+ uiWorkspace
2838
3586
  }) : null);
2839
3587
  }
2840
3588
  function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun, openArtifact }) {
@@ -2876,12 +3624,12 @@ window.__ModuleLoader__.load({
2876
3624
  } }, h("div", { style: {
2877
3625
  fontSize: 28,
2878
3626
  marginBottom: 8
2879
- } }, "📋"), "backlog 为空(还没有流水线运行过)");
3627
+ } }, "📋"), t("board.empty"));
2880
3628
  const subtaskMap = {};
2881
3629
  for (const t of backlog.tasks || []) if (t.type === "subtask" && t.id) subtaskMap[t.id] = t;
2882
3630
  const move = async (kind, id, to) => {
2883
3631
  try {
2884
- await api.backlogUpdate(kind, id, to, sessionId, "看板拖拽流转");
3632
+ await api.backlogUpdate(kind, id, to, sessionId, t("board.dragReason"));
2885
3633
  } catch (e) {}
2886
3634
  onRefresh();
2887
3635
  };
@@ -2911,7 +3659,11 @@ window.__ModuleLoader__.load({
2911
3659
  transition: "opacity .1s ease, transform .12s ease",
2912
3660
  boxShadow: "0 1px 2px rgba(0,0,0,.05)"
2913
3661
  },
2914
- title: `${item.id} · ${item.status}${item.summary ? "\n" + item.summary : ""}(点击查看详情)`
3662
+ title: t("board.cardTip", {
3663
+ id: item.id,
3664
+ status: item.status,
3665
+ summary: item.summary ? "\n" + item.summary : ""
3666
+ })
2915
3667
  }, h("div", { style: {
2916
3668
  display: "flex",
2917
3669
  alignItems: "center",
@@ -2961,7 +3713,7 @@ window.__ModuleLoader__.load({
2961
3713
  fontFamily: MONO,
2962
3714
  minWidth: 0
2963
3715
  } }, item.devAssign ? h("span", {
2964
- title: `dev 分配\n${item.devAssign}`,
3716
+ title: t("board.assignDevTip", { who: item.devAssign }),
2965
3717
  style: {
2966
3718
  display: "inline-flex",
2967
3719
  alignItems: "center",
@@ -2973,7 +3725,7 @@ window.__ModuleLoader__.load({
2973
3725
  whiteSpace: "nowrap"
2974
3726
  }
2975
3727
  }, `👨‍💻${item.devAssign}`) : null, item.qaAssign ? h("span", {
2976
- title: `qa 分配\n${item.qaAssign}`,
3728
+ title: t("board.assignQaTip", { who: item.qaAssign }),
2977
3729
  style: {
2978
3730
  display: "inline-flex",
2979
3731
  alignItems: "center",
@@ -2985,7 +3737,7 @@ window.__ModuleLoader__.load({
2985
3737
  whiteSpace: "nowrap"
2986
3738
  }
2987
3739
  }, `🧪${item.qaAssign}`) : null, item.acceptBy ? h("span", {
2988
- title: `验收/汇报人\n${item.acceptBy}`,
3740
+ title: t("board.acceptTip", { who: item.acceptBy }),
2989
3741
  style: {
2990
3742
  display: "inline-flex",
2991
3743
  alignItems: "center",
@@ -3027,7 +3779,7 @@ window.__ModuleLoader__.load({
3027
3779
  color: T.text2,
3028
3780
  fontFamily: MONO,
3029
3781
  marginBottom: 3
3030
- } }, 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", {
3782
+ } }, 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", {
3031
3783
  key: sub.id,
3032
3784
  style: {
3033
3785
  display: "flex",
@@ -3057,7 +3809,7 @@ window.__ModuleLoader__.load({
3057
3809
  whiteSpace: "nowrap"
3058
3810
  }
3059
3811
  }, (sub.title || "").replace(/^开发 · /, "")), sub.devAssign ? h("span", {
3060
- title: `dev 分配\n${sub.devAssign}`,
3812
+ title: t("board.assignDevTip", { who: sub.devAssign }),
3061
3813
  style: {
3062
3814
  flex: "0 1 auto",
3063
3815
  color: T.text2,
@@ -3112,7 +3864,7 @@ window.__ModuleLoader__.load({
3112
3864
  fontWeight: 700,
3113
3865
  fontSize: 13,
3114
3866
  padding: "6px 0 8px"
3115
- } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), KIND_TITLE[kind], h("span", { style: {
3867
+ } }, h("span", { style: { fontSize: 14 } }, kind === "req" ? "📌" : kind === "task" ? "🔧" : "🐞"), kindTitle(kind), h("span", { style: {
3116
3868
  fontSize: 11,
3117
3869
  fontWeight: 600,
3118
3870
  color: T.text2,
@@ -3188,7 +3940,7 @@ window.__ModuleLoader__.load({
3188
3940
  }) : null);
3189
3941
  }
3190
3942
  function fmtAt(ts) {
3191
- return ts ? new Date(ts).toLocaleTimeString("zh-CN", {
3943
+ return ts ? new Date(ts).toLocaleTimeString(localeTag(), {
3192
3944
  hour: "2-digit",
3193
3945
  minute: "2-digit"
3194
3946
  }) : "—";
@@ -3266,7 +4018,7 @@ window.__ModuleLoader__.load({
3266
4018
  color: T.warn,
3267
4019
  flex: "0 0 auto"
3268
4020
  } }, `⛽${fmtTokens((extra.usage.input || 0) + (extra.usage.cacheRead || 0) + (extra.usage.cacheWrite || 0) + (extra.usage.output || 0))}`) : null, extra && extra.assignee ? h("span", {
3269
- title: `dev 分配\n${extra.assignee}`,
4021
+ title: t("board.assignDevTip", { who: extra.assignee }),
3270
4022
  style: {
3271
4023
  display: "inline-flex",
3272
4024
  alignItems: "center",
@@ -3345,7 +4097,7 @@ window.__ModuleLoader__.load({
3345
4097
  } }, `${d.id} · ${d.kind}${d.severity ? " · " + d.severity : ""}`) : null), d ? chip(stText(d.status), color, { dot: true }) : null, h("button", {
3346
4098
  onClick: onClose,
3347
4099
  style: closeBtn,
3348
- title: "关闭"
4100
+ title: t("common.close")
3349
4101
  }, "✕")), h("div", { style: {
3350
4102
  flex: 1,
3351
4103
  overflowY: "auto",
@@ -3357,7 +4109,7 @@ window.__ModuleLoader__.load({
3357
4109
  color: T.text2,
3358
4110
  fontSize: 12,
3359
4111
  padding: 12
3360
- } }, "加载中…") : err ? h("div", { style: {
4112
+ } }, t("common.loading")) : err ? h("div", { style: {
3361
4113
  color: T.error,
3362
4114
  fontSize: 12,
3363
4115
  padding: 12
@@ -3366,7 +4118,7 @@ window.__ModuleLoader__.load({
3366
4118
  display: "flex",
3367
4119
  flexDirection: "column",
3368
4120
  gap: 4
3369
- } }, secTitle("概览"), d.spec ? h(FoldableText, {
4121
+ } }, secTitle(t("item.overview")), d.spec ? h(FoldableText, {
3370
4122
  text: d.spec,
3371
4123
  charLimit: 300,
3372
4124
  lineLimit: 4,
@@ -3384,7 +4136,28 @@ window.__ModuleLoader__.load({
3384
4136
  flexDirection: "column",
3385
4137
  gap: 2,
3386
4138
  marginTop: 2
3387
- } }, 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) || "—"))),
4139
+ } }, 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) || "—"))),
4140
+ kind === "bug" ? h("div", { style: {
4141
+ display: "flex",
4142
+ flexDirection: "column",
4143
+ gap: 4,
4144
+ padding: "8px 10px",
4145
+ borderRadius: 8,
4146
+ background: T.layer2,
4147
+ border: `1px solid ${T.border}`
4148
+ } }, secTitle(t("panelItem.defectSection")), d.spec ? h(FoldableText, {
4149
+ text: d.spec,
4150
+ charLimit: 300,
4151
+ lineLimit: 4
4152
+ }) : null, h("div", { style: {
4153
+ display: "flex",
4154
+ flexDirection: "column",
4155
+ gap: 2
4156
+ } }, 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: {
4157
+ fontSize: 10.5,
4158
+ color: T.warn,
4159
+ lineHeight: 1.5
4160
+ } }, t("panelItem.noDefectDetail"))) : null,
3388
4161
  d.runInfo ? (() => {
3389
4162
  const ri = d.runInfo;
3390
4163
  return h("div", { style: {
@@ -3399,14 +4172,14 @@ window.__ModuleLoader__.load({
3399
4172
  display: "flex",
3400
4173
  alignItems: "center",
3401
4174
  gap: 8
3402
- } }, secTitle("运行"), h("span", { style: {
4175
+ } }, secTitle(t("item.runSection")), h("span", { style: {
3403
4176
  fontFamily: MONO,
3404
4177
  fontSize: 10.5,
3405
4178
  color: T.text2,
3406
4179
  whiteSpace: "nowrap"
3407
4180
  } }, ri.runId), chip(stText(ri.status), stColor(ri.status)), onShowRun ? h("button", {
3408
4181
  onClick: () => onShowRun(ri.runId),
3409
- title: `跳转到该需求的流水线视图 #${String(ri.runId).slice(-6)}`,
4182
+ title: t("item.jumpRunTip", { id: String(ri.runId).slice(-6) }),
3410
4183
  style: {
3411
4184
  marginLeft: "auto",
3412
4185
  flex: "0 0 auto",
@@ -3421,7 +4194,7 @@ window.__ModuleLoader__.load({
3421
4194
  color: T.brand,
3422
4195
  lineHeight: "16px"
3423
4196
  }
3424
- }, "▶ 流水线") : null), ri.startedAt || ri.endedAt ? h("span", { style: {
4197
+ }, t("item.runBtn")) : null), ri.startedAt || ri.endedAt ? h("span", { style: {
3425
4198
  fontSize: 10.5,
3426
4199
  color: T.text2,
3427
4200
  fontFamily: MONO
@@ -3429,8 +4202,8 @@ window.__ModuleLoader__.load({
3429
4202
  display: "flex",
3430
4203
  flexDirection: "column",
3431
4204
  gap: 4
3432
- } }, secTitle("需求原文"), h(FoldableText, {
3433
- text: ri.requirement || "(无原文)",
4205
+ } }, secTitle(t("item.requirement")), h(FoldableText, {
4206
+ text: ri.requirement || t("item.noRequirement"),
3434
4207
  charLimit: 300,
3435
4208
  lineLimit: 4,
3436
4209
  style: {
@@ -3444,7 +4217,7 @@ window.__ModuleLoader__.load({
3444
4217
  display: "flex",
3445
4218
  flexDirection: "column",
3446
4219
  gap: 4
3447
- } }, secTitle("任务夹"), h("div", { style: {
4220
+ } }, secTitle(t("item.runDocs")), h("div", { style: {
3448
4221
  fontSize: 11.5,
3449
4222
  fontFamily: MONO,
3450
4223
  color: T.brand,
@@ -3462,7 +4235,7 @@ window.__ModuleLoader__.load({
3462
4235
  onClick: () => {
3463
4236
  if (openArtifact) openArtifact(a.address, a.name);
3464
4237
  },
3465
- title: `在右侧栏预览 ${d.runDocs}/${a.name}`,
4238
+ title: t("item.previewTip", { path: `${d.runDocs}/${a.name}` }),
3466
4239
  style: {
3467
4240
  fontSize: 10.5,
3468
4241
  fontWeight: 600,
@@ -3480,7 +4253,7 @@ window.__ModuleLoader__.load({
3480
4253
  display: "flex",
3481
4254
  flexDirection: "column",
3482
4255
  gap: 4
3483
- } }, secTitle("TOKEN · 官方口径"), h("span", { style: {
4256
+ } }, secTitle(t("token.officialTitle")), h("span", { style: {
3484
4257
  fontSize: 11.5,
3485
4258
  fontFamily: MONO,
3486
4259
  color: T.text,
@@ -3492,7 +4265,7 @@ window.__ModuleLoader__.load({
3492
4265
  marginTop: 2
3493
4266
  } }, Object.entries(d.byRole).sort((a, b) => totalTokens(b[1]) - totalTokens(a[1])).map(([role, uRaw]) => {
3494
4267
  const u = uRaw;
3495
- const label = role === "dev" ? "👨‍💻 开发" : role === "qa" ? "🧪 QA" : role === "acceptance" ? "✅ 验收" : role === "pm" ? "📌 产品" : role === "design" ? "🎨 设计" : role === "arch" ? "🏗 架构" : "⚙️ " + role;
4268
+ const label = roleChip(role);
3496
4269
  return h("div", {
3497
4270
  key: role,
3498
4271
  style: {
@@ -3503,13 +4276,13 @@ window.__ModuleLoader__.load({
3503
4276
  fontFamily: MONO,
3504
4277
  color: T.text2
3505
4278
  }
3506
- }, 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} 次`));
4279
+ }, 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 })}`));
3507
4280
  })) : null) : null,
3508
4281
  d.subtasks && d.subtasks.length > 0 ? h("div", { style: {
3509
4282
  display: "flex",
3510
4283
  flexDirection: "column",
3511
4284
  gap: 4
3512
- } }, secTitle(`关联子卡(${d.subtasks.length})`), h("div", { style: {
4285
+ } }, secTitle(t("item.subtasks", { n: d.subtasks.length })), h("div", { style: {
3513
4286
  display: "flex",
3514
4287
  flexDirection: "column",
3515
4288
  gap: 5
@@ -3521,7 +4294,7 @@ window.__ModuleLoader__.load({
3521
4294
  display: "flex",
3522
4295
  flexDirection: "column",
3523
4296
  gap: 4
3524
- } }, secTitle(`关联缺陷(${d.bugs.length})`), h("div", { style: {
4297
+ } }, secTitle(t("item.bugs", { n: d.bugs.length })), h("div", { style: {
3525
4298
  display: "flex",
3526
4299
  flexDirection: "column",
3527
4300
  gap: 5
@@ -3530,7 +4303,7 @@ window.__ModuleLoader__.load({
3530
4303
  display: "flex",
3531
4304
  flexDirection: "column",
3532
4305
  gap: 4
3533
- } }, secTitle(`流转时间线(${d.events.length})`), h("div", { style: {
4306
+ } }, secTitle(t("item.timeline", { n: d.events.length })), h("div", { style: {
3534
4307
  display: "flex",
3535
4308
  flexDirection: "column",
3536
4309
  gap: 0
@@ -3566,7 +4339,7 @@ window.__ModuleLoader__.load({
3566
4339
  }, ev.reason || ""))))) : null
3567
4340
  ]));
3568
4341
  }
3569
- function TeamSelector({ sessionId, remote }) {
4342
+ function TeamSelector({ sessionId, remote, locale }) {
3570
4343
  const [teams, setTeams] = react.default.useState([]);
3571
4344
  const [active, setActive] = react.default.useState(null);
3572
4345
  const [open, setOpen] = react.default.useState(false);
@@ -3579,7 +4352,11 @@ window.__ModuleLoader__.load({
3579
4352
  const at = unwrap(await remote.getActiveTeam(sessionId), "getActiveTeam");
3580
4353
  setActive(at && at.team ? at.team : null);
3581
4354
  } catch (e) {}
3582
- }, [remote, sessionId]);
4355
+ }, [
4356
+ remote,
4357
+ sessionId,
4358
+ locale
4359
+ ]);
3583
4360
  react.default.useEffect(() => {
3584
4361
  load();
3585
4362
  }, [load]);
@@ -3610,7 +4387,7 @@ window.__ModuleLoader__.load({
3610
4387
  style: { position: "relative" }
3611
4388
  }, h("button", {
3612
4389
  onClick: () => setOpen(!open),
3613
- title: active ? `当前团队:${active.name}(点击切换)` : "选择团队",
4390
+ title: active ? t("team.currentTip", { name: active.name }) : t("team.pick"),
3614
4391
  style: {
3615
4392
  display: "inline-flex",
3616
4393
  alignItems: "center",
@@ -3627,14 +4404,14 @@ window.__ModuleLoader__.load({
3627
4404
  transition: "all .12s ease"
3628
4405
  }
3629
4406
  }, h("span", { style: { fontSize: 12 } }, active ? active.icon : "🏭"), h("span", {
3630
- title: active && active.name ? String(active.name) : "团队",
4407
+ title: active && active.name ? String(active.name) : t("team.label"),
3631
4408
  style: {
3632
4409
  maxWidth: 80,
3633
4410
  overflow: "hidden",
3634
4411
  textOverflow: "ellipsis",
3635
4412
  whiteSpace: "nowrap"
3636
4413
  }
3637
- }, active ? active.name : "团队"), h("span", { style: {
4414
+ }, active ? active.name : t("team.label")), h("span", { style: {
3638
4415
  fontSize: 8,
3639
4416
  opacity: .6
3640
4417
  } }, open ? "▲" : "▼")), open ? h("div", { style: {
@@ -3654,7 +4431,7 @@ window.__ModuleLoader__.load({
3654
4431
  fontSize: 10,
3655
4432
  color: T.text2,
3656
4433
  borderBottom: `1px solid ${T.border}`
3657
- } }, "选择团队"), h("button", {
4434
+ } }, t("team.pick")), h("button", {
3658
4435
  onClick: () => select(null),
3659
4436
  style: {
3660
4437
  display: "flex",
@@ -3683,14 +4460,14 @@ window.__ModuleLoader__.load({
3683
4460
  } }, h("div", { style: {
3684
4461
  fontWeight: 600,
3685
4462
  lineHeight: 1.35
3686
- } }, "无团队(直接对话)"), h("div", { style: {
4463
+ } }, t("team.none")), h("div", { style: {
3687
4464
  fontSize: 10.5,
3688
4465
  color: T.text2,
3689
4466
  marginTop: 3,
3690
4467
  lineHeight: 1.45,
3691
4468
  whiteSpace: "normal",
3692
4469
  wordBreak: "break-word"
3693
- } }, "不走 teamflow,模型直接工作")), !active ? h("span", { style: {
4470
+ } }, t("team.noneNote"))), !active ? h("span", { style: {
3694
4471
  marginLeft: "auto",
3695
4472
  color: T.text2,
3696
4473
  fontSize: 12,
@@ -3739,7 +4516,10 @@ window.__ModuleLoader__.load({
3739
4516
  }
3740
4517
  /** 解包 remote 信封:失败抛错;成功返回 value。 */
3741
4518
  function unwrap(res, what) {
3742
- if (!res || !res.ok) throw new Error(`${what || "remote"} 调用失败:${res && res.error && (res.error.message || res.error.code) || "未知错误"}`);
4519
+ if (!res || !res.ok) throw new Error(t("common.remoteCallFailed", {
4520
+ what: what || "remote",
4521
+ detail: res && res.error && (res.error.message || res.error.code) || t("common.unknownError")
4522
+ }));
3743
4523
  return res.value;
3744
4524
  }
3745
4525
  function TeamFlowView(props) {
@@ -3758,7 +4538,7 @@ window.__ModuleLoader__.load({
3758
4538
  if (!api) {
3759
4539
  setState((s) => ({
3760
4540
  ...s,
3761
- err: "remote 未就绪"
4541
+ err: t("common.remoteNotReady")
3762
4542
  }));
3763
4543
  return;
3764
4544
  }
@@ -3820,6 +4600,26 @@ window.__ModuleLoader__.load({
3820
4600
  setBusy(false);
3821
4601
  };
3822
4602
  const anyRunning = runs.some((r) => r.status === "running" || r.status === "pending");
4603
+ const runningRun = !!(activeRun && activeRun.status === "running");
4604
+ /** 中断请求已发出:按钮先隐藏,等 2s 轮询把状态刷成 cancelled(避免重复点出「未生效」提示)。 */
4605
+ const [cancelSentFor, setCancelSentFor] = react.default.useState(null);
4606
+ react.default.useEffect(() => {
4607
+ setCancelSentFor(null);
4608
+ }, [activeRun && activeRun.id, activeRun && activeRun.status]);
4609
+ const onCancel = async (id) => {
4610
+ if (!api) return;
4611
+ try {
4612
+ const r = unwrap(await api.cancel(id), "cancel");
4613
+ if (!r || r.ok !== true) throw new Error(t("cancel.failed"));
4614
+ setCancelSentFor(id);
4615
+ refresh();
4616
+ } catch (e) {
4617
+ setState((s) => ({
4618
+ ...s,
4619
+ err: String(e && e.message || e)
4620
+ }));
4621
+ }
4622
+ };
3823
4623
  const btn = {
3824
4624
  font: "inherit",
3825
4625
  fontSize: 12,
@@ -3887,7 +4687,7 @@ window.__ModuleLoader__.load({
3887
4687
  fontWeight: 700,
3888
4688
  fontSize: 14,
3889
4689
  lineHeight: "18px"
3890
- } }, "团队工作台"), h("span", { style: {
4690
+ } }, t("workbench.title")), h("span", { style: {
3891
4691
  fontSize: 11,
3892
4692
  color: T.text2,
3893
4693
  display: "flex",
@@ -3900,7 +4700,7 @@ window.__ModuleLoader__.load({
3900
4700
  display: "inline-block",
3901
4701
  background: anyRunning ? T.success : T.text2,
3902
4702
  animation: anyRunning ? "tf-pulse 1.6s ease-in-out infinite" : "none"
3903
- } }), anyRunning ? "流水线运行中" : "空闲")), h("button", {
4703
+ } }), anyRunning ? t("workbench.running") : t("workbench.idle"))), h("button", {
3904
4704
  onClick: refresh,
3905
4705
  style: {
3906
4706
  ...btn,
@@ -3909,10 +4709,18 @@ window.__ModuleLoader__.load({
3909
4709
  alignItems: "center",
3910
4710
  gap: 5
3911
4711
  }
3912
- }, "🔄 刷新"), canResume ? h("button", {
4712
+ }, t("workbench.refresh")), runningRun && cancelSentFor !== activeRun.id ? h(CancelButton, {
4713
+ runId: activeRun.id,
4714
+ label: t("cancel.btnWithId", { id: String(activeRun.id).slice(-6) }),
4715
+ title: t("cancel.tip", { id: activeRun.id }),
4716
+ onConfirm: onCancel
4717
+ }) : null, canResume ? h("button", {
3913
4718
  onClick: onResume,
3914
4719
  disabled: busy,
3915
- title: `断点续跑 ${activeRun.id}\n当前状态:${RUN_STATUS_TEXT[activeRun.status] || activeRun.status};跳过已完成阶段,从第一个未完成阶段重跑`,
4720
+ title: t("workbench.resumeTip", {
4721
+ id: activeRun.id,
4722
+ status: runStatusText(activeRun.status)
4723
+ }),
3916
4724
  style: {
3917
4725
  ...btn,
3918
4726
  background: T.error,
@@ -3920,14 +4728,14 @@ window.__ModuleLoader__.load({
3920
4728
  border: "none",
3921
4729
  fontWeight: 600
3922
4730
  }
3923
- }, busy ? "续跑中…" : `↻ 从断点重跑 #${String(activeRun.id).slice(-6)}`) : null), err ? h("div", { style: {
4731
+ }, busy ? t("workbench.resuming") : t("workbench.resumeBtn", { id: String(activeRun.id).slice(-6) })) : null), err ? h("div", { style: {
3924
4732
  color: T.error,
3925
4733
  fontSize: 12,
3926
4734
  background: `color-mix(in srgb, ${T.error} 8%, transparent)`,
3927
4735
  border: `1px solid color-mix(in srgb, ${T.error} 30%, transparent)`,
3928
4736
  borderRadius: 8,
3929
4737
  padding: "7px 11px"
3930
- } }, `⚠ ${err}(确认已安装 dsh-plugin-teamflow 且 web 已重启)`) : null, needHuman.length > 0 ? h("div", { style: {
4738
+ } }, t("workbench.loadFailed", { err })) : null, needHuman.length > 0 ? h("div", { style: {
3931
4739
  display: "flex",
3932
4740
  alignItems: "center",
3933
4741
  gap: 10,
@@ -3940,13 +4748,13 @@ window.__ModuleLoader__.load({
3940
4748
  fontWeight: 700,
3941
4749
  color: T.warn,
3942
4750
  fontSize: 12.5
3943
- } }, `⚠ ${needHuman.length} 项需人工介入`), needHuman.slice(0, 5).map((item) => {
4751
+ } }, t("workbench.needsHumanBanner", { n: needHuman.length })), needHuman.slice(0, 5).map((item) => {
3944
4752
  const kind = (backlog.requirements || []).some((r) => r.id === item.id) ? "req" : (backlog.tasks || []).some((t) => t.id === item.id) ? "task" : "bug";
3945
4753
  const fin = kind === "bug" ? "verified" : "accepted";
3946
4754
  return h("button", {
3947
4755
  key: item.id,
3948
4756
  onClick: async () => {
3949
- await api.backlogUpdate(kind, item.id, fin, props.sessionId, "人工处理");
4757
+ await api.backlogUpdate(kind, item.id, fin, props.sessionId, t("workbench.manualReason"));
3950
4758
  refresh();
3951
4759
  },
3952
4760
  style: {
@@ -3956,7 +4764,7 @@ window.__ModuleLoader__.load({
3956
4764
  border: "none",
3957
4765
  fontWeight: 600
3958
4766
  }
3959
- }, `处理 ${item.id}`);
4767
+ }, t("workbench.handle", { id: item.id }));
3960
4768
  })) : null, h("div", { style: {
3961
4769
  display: "flex",
3962
4770
  alignItems: "center",
@@ -3965,10 +4773,10 @@ window.__ModuleLoader__.load({
3965
4773
  } }, h("button", {
3966
4774
  onClick: () => setTab("pipeline"),
3967
4775
  style: tabBtn(tab === "pipeline")
3968
- }, "🔄 流水线"), h("button", {
4776
+ }, t("workbench.tabPipeline")), h("button", {
3969
4777
  onClick: () => setTab("board"),
3970
4778
  style: tabBtn(tab === "board")
3971
- }, "📋 Backlog 看板"), h("div", { style: {
4779
+ }, t("workbench.tabBoard")), h("div", { style: {
3972
4780
  marginLeft: "auto",
3973
4781
  display: "flex",
3974
4782
  alignItems: "center",
@@ -3984,14 +4792,14 @@ window.__ModuleLoader__.load({
3984
4792
  color: activeRun.status === "interrupted" ? T.warn : T.text2,
3985
4793
  border: `1px solid ${T.border}`
3986
4794
  }
3987
- }, `#${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", {
4795
+ }, `#${String(activeRun.id).slice(-8)} · ${runStatusText(activeRun.status)}`) : null, total && total.input + total.cacheRead + total.cacheWrite + total.output > 0 ? h("span", {
3988
4796
  style: {
3989
4797
  fontSize: 11.5,
3990
4798
  fontFamily: MONO,
3991
4799
  color: T.text2,
3992
4800
  cursor: "help"
3993
4801
  },
3994
- title: "输入(未命中)/输入(命中)/输出 全部阶段合计"
4802
+ title: t("token.allStagesTip")
3995
4803
  }, `∑ ⇅${fmtTokens(total.input)}/⇅${fmtTokens(total.cacheRead)}·⬆${fmtTokens(total.output)}`) : null)), h("div", { style: {
3996
4804
  display: "flex",
3997
4805
  alignItems: "center",
@@ -3999,7 +4807,7 @@ window.__ModuleLoader__.load({
3999
4807
  fontSize: 12,
4000
4808
  flexWrap: "wrap"
4001
4809
  } }, h("span", {
4002
- title: `当前工作区(workspace 级隔离):${workspace && workspace.path || "未连接工作区"}`,
4810
+ title: t("workbench.workspaceTip", { path: workspace && workspace.path || t("workbench.noWorkspace") }),
4003
4811
  style: {
4004
4812
  display: "inline-flex",
4005
4813
  alignItems: "center",
@@ -4024,7 +4832,7 @@ window.__ModuleLoader__.load({
4024
4832
  display: "flex",
4025
4833
  alignItems: "center",
4026
4834
  gap: 5
4027
- } }, h("span", { style: { color: T.text2 } }, "历史"), runs.length ? runs.map((r) => {
4835
+ } }, h("span", { style: { color: T.text2 } }, t("workbench.history")), runs.length ? runs.map((r) => {
4028
4836
  const sel = r.id === (runId || runs[0] && runs[0].id);
4029
4837
  return h("button", {
4030
4838
  key: r.id,
@@ -4035,13 +4843,13 @@ window.__ModuleLoader__.load({
4035
4843
  }) : h("span", { style: {
4036
4844
  color: T.text2,
4037
4845
  fontSize: 11.5
4038
- } }, "(暂无)")) : null, tab === "pipeline" && activeRun && activeRun.address ? h("button", {
4846
+ } }, t("common.noneDash"))) : null, tab === "pipeline" && activeRun && activeRun.address ? h("button", {
4039
4847
  style: chipBtn(false),
4040
- title: "在右侧栏打开该 run 详情(与任务夹产物并排看)",
4848
+ title: t("workbench.openRightBarTip"),
4041
4849
  onClick: () => {
4042
4850
  if (!(props.openResource && props.openResource(activeRun.address, activeRun.id))) console.warn("[teamflow] 右侧栏不可用,run 详情请在画布节点里查看");
4043
4851
  }
4044
- }, "⇥ 右栏打开") : null), h("div", { style: {
4852
+ }, t("workbench.openRightBarBtn")) : null), h("div", { style: {
4045
4853
  flex: 1,
4046
4854
  minHeight: 0,
4047
4855
  display: "flex",
@@ -4051,7 +4859,7 @@ window.__ModuleLoader__.load({
4051
4859
  api,
4052
4860
  runId: activeRun ? activeRun.id : null,
4053
4861
  sessionId: props.sessionId,
4054
- sessions: props.sessions
4862
+ uiWorkspace: props.uiWorkspace
4055
4863
  }) : h(BoardPanel, {
4056
4864
  backlog,
4057
4865
  api,
@@ -4082,6 +4890,24 @@ window.__ModuleLoader__.load({
4082
4890
  async function apply(ctx) {
4083
4891
  await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
4084
4892
  const teamflow = ctx.get("remote.teamflow");
4893
+ const t = ctx.locale.bind(NS);
4894
+ ctx.effect(() => ctx.locale.register(NS, {
4895
+ zh,
4896
+ en
4897
+ }), "teamflow: dictionaries");
4898
+ ctx.effect(() => ctx.locale.subscribe(() => setTranslator(t, () => ctx.locale.getSnapshot().active)), "teamflow: translator sync");
4899
+ setTranslator(t, () => ctx.locale.getSnapshot().active);
4900
+ const pushLocaleToHost = (active) => {
4901
+ try {
4902
+ if (typeof active !== "string" || !active) return;
4903
+ const p = teamflow.setLocale(active);
4904
+ if (p && typeof p.catch === "function") p.catch(() => {});
4905
+ } catch (e) {}
4906
+ };
4907
+ ctx.effect(() => {
4908
+ pushLocaleToHost(ctx.locale.getSnapshot().active);
4909
+ return ctx.locale.subscribe(() => pushLocaleToHost(ctx.locale.getSnapshot().active));
4910
+ }, "teamflow: host locale push");
4085
4911
  const openResourceSafe = (address, label, quiet) => {
4086
4912
  try {
4087
4913
  const sidebarRight = ctx.get("sidebarRight");
@@ -4103,14 +4929,16 @@ window.__ModuleLoader__.load({
4103
4929
  name: "sidebar.panellist",
4104
4930
  id: "teamflow",
4105
4931
  order: 60,
4106
- label: "团队工作台"
4932
+ locale: NS,
4933
+ label: () => t("workbench.title")
4107
4934
  }, TeamflowPanelIcon));
4108
4935
  ctx.slots.inject("main", () => ctx.slots.register({
4109
4936
  name: "main",
4110
4937
  key: "teamflow",
4938
+ locale: NS,
4111
4939
  inject: () => ({
4112
4940
  remote: teamflow,
4113
- sessions: ctx.get("sessions"),
4941
+ uiWorkspace: ctx.get("uiWorkspace"),
4114
4942
  layout: ctx.get("layout"),
4115
4943
  openResource: openResourceSafe,
4116
4944
  openArtifact
@@ -4126,6 +4954,7 @@ window.__ModuleLoader__.load({
4126
4954
  ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
4127
4955
  name: "sidebar.right.pane.tab",
4128
4956
  key: RUN_TAB_ID,
4957
+ locale: NS,
4129
4958
  inject: () => ({
4130
4959
  remote: teamflow,
4131
4960
  openArtifact
@@ -4135,11 +4964,12 @@ window.__ModuleLoader__.load({
4135
4964
  name: "conversation.view",
4136
4965
  id: "teamflow",
4137
4966
  order: 20,
4138
- label: "🏭 团队工作台",
4967
+ locale: NS,
4968
+ label: () => `🏭 ${t("workbench.title")}`,
4139
4969
  inject: (sessionId) => ({
4140
4970
  sessionId,
4141
4971
  remote: teamflow,
4142
- sessions: ctx.get("sessions"),
4972
+ uiWorkspace: ctx.get("uiWorkspace"),
4143
4973
  openArtifact,
4144
4974
  openResource: openResourceSafe
4145
4975
  })
@@ -4148,9 +4978,11 @@ window.__ModuleLoader__.load({
4148
4978
  name: "conversation.input.right",
4149
4979
  id: "teamflow-team-select",
4150
4980
  order: 5,
4981
+ locale: NS,
4151
4982
  inject: (sessionId) => ({
4152
4983
  sessionId,
4153
- remote: teamflow
4984
+ remote: teamflow,
4985
+ locale: ctx.locale.getSnapshot().active
4154
4986
  })
4155
4987
  }, TeamSelector));
4156
4988
  }