@yangdcm/dsh-expert-team 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +141 -0
  3. package/README.md +135 -0
  4. package/client.js +2473 -0
  5. package/cordis.patch.yml +24 -0
  6. package/lib/artifact-writer.js +379 -0
  7. package/lib/command-parse.js +181 -0
  8. package/lib/command.js +5100 -0
  9. package/lib/dispatch-ledger.js +229 -0
  10. package/lib/index.js +15 -0
  11. package/lib/interception.js +266 -0
  12. package/lib/lead-toolface.js +179 -0
  13. package/lib/log-parse.js +181 -0
  14. package/lib/loop-guard.js +165 -0
  15. package/lib/metrics/collect.js +70 -0
  16. package/lib/metrics/render.js +100 -0
  17. package/lib/metrics/session-usage.js +319 -0
  18. package/lib/metrics/timing.js +188 -0
  19. package/lib/metrics/token-usage.js +352 -0
  20. package/lib/metrics/tokens.js +271 -0
  21. package/lib/routes/shared.js +83 -0
  22. package/lib/settings.js +289 -0
  23. package/lib/tier.js +190 -0
  24. package/lib/validate.js +681 -0
  25. package/lib/vocab.js +121 -0
  26. package/lib/write-tracer.js +58 -0
  27. package/package.json +119 -0
  28. package/presets/expert-team/agent.cordis.yml +542 -0
  29. package/presets/expert-team/preset.yml +3 -0
  30. package/skills/expert-team/SKILL.md +328 -0
  31. package/skills/expert-team/assets/templates/AUTHORITY.md +32 -0
  32. package/skills/expert-team/assets/templates/PLAN.md +27 -0
  33. package/skills/expert-team/assets/templates/RESEARCH.md +13 -0
  34. package/skills/expert-team/assets/templates/RETRO.md +24 -0
  35. package/skills/expert-team/assets/templates/REVIEW.md +10 -0
  36. package/skills/expert-team/assets/templates/ROSTER.json +6 -0
  37. package/skills/expert-team/assets/templates/SPEC.md +62 -0
  38. package/skills/expert-team/assets/templates/STATE.json +10 -0
  39. package/skills/expert-team/assets/templates/SUMMARY.md +25 -0
  40. package/skills/expert-team/assets/templates/TASK.md +23 -0
  41. package/skills/expert-team/assets/templates/TASKS.json +3 -0
  42. package/skills/expert-team/assets/templates/TEST.md +9 -0
  43. package/skills/expert-team/assets/templates//344/273/273/345/212/241/347/234/213/346/235/277.md +23 -0
  44. package/skills/expert-team/references/EFFICIENCY.md +79 -0
  45. package/skills/expert-team/references/LOGGING.md +82 -0
  46. package/skills/expert-team/references/PERSIST.md +57 -0
  47. package/skills/expert-team/references/PIPELINE.md +58 -0
  48. package/skills/expert-team/references/ROLES.md +297 -0
  49. package/skills/expert-team/references/WORKSPACE.md +123 -0
  50. package/skills/expert-team/references/workflow.team.js +97 -0
  51. package/skills/expert-team/scripts/scan-authority.mjs +114 -0
  52. package/skills/expert-team/scripts/scan-single-source.mjs +292 -0
package/client.js ADDED
@@ -0,0 +1,2473 @@
1
+ // @yangdcm/dsh-expert-team — client half: a live team overlay panel + header button.
2
+ //
3
+ // Mirrors dsh-md-preview's proven wiring: a header button in
4
+ // `conversation.session.header.actions` captures the session id into a module
5
+ // store; the overlay registered into `shell.overlay` reads it via
6
+ // useCurrentSession(). It fetches `/plugins/dsh-expert-team/state` every 3s and
7
+ // renders: overall progress, phase stepper, roster (member model/status),
8
+ // an interactive SVG task DAG, coverage matrix, and artifact preview.
9
+ // Follows --dsw-alias-* theme vars.
10
+
11
+ window.__ModuleLoader__.load({
12
+ id: '@yangdcm/dsh-expert-team',
13
+ factory: function (require) {
14
+ var module = { exports: {} }
15
+ var exports = module.exports
16
+ var react = require('react')
17
+ var useState = react.useState
18
+ var useEffect = react.useEffect
19
+ var useRef = react.useRef
20
+ var createElement = react.createElement
21
+ var Fragment = react.Fragment
22
+ var h = createElement
23
+
24
+ var CSS =
25
+ '.exp-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:100vw;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,var(--bg,#fff));border-left:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));box-shadow:-10px 0 32px rgba(0,0,0,.16);z-index:120;font-size:13px;color:var(--dsw-alias-label-primary,var(--text,#1f2328));font-family:-apple-system,"Segoe UI","PingFang SC",sans-serif}' +
26
+ '.exp-head{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,var(--border,#d0d7de));background:linear-gradient(180deg,var(--dsw-alias-bg-module-platform,var(--bg-subtle,#f6f8fa)),var(--dsw-alias-bg-base,var(--bg,#fff)));display:flex;align-items:center;justify-content:space-between;flex:none}' +
27
+ '.exp-head.exp-grab{cursor:move;user-select:none}' +
28
+ '.exp-dock-handle{position:absolute;left:-2px;top:0;bottom:0;width:9px;cursor:col-resize;z-index:3;transition:background .12s}' +
29
+ '.exp-dock-handle::after{content:"";position:absolute;left:3px;top:0;bottom:0;width:2px;background:var(--dsw-alias-border-l2,var(--border,#d0d7de));opacity:.7}' +
30
+ '.exp-dock-handle:hover::after{background:var(--dsw-alias-state-business-primary,var(--accent,#0969da));opacity:.9;width:3px}' +
31
+ '.exp-panel.exp-float{left:auto;right:auto;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));box-shadow:0 18px 48px rgba(0,0,0,.24)}' +
32
+ '.exp-title{font-weight:700;font-size:14px;color:var(--dsw-alias-label-primary,var(--text,#1f2328))}' +
33
+ '.exp-run{font-size:11px;color:var(--dsw-alias-label-secondary,var(--text,#57606a));font-family:ui-monospace,monospace;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
34
+ '.exp-close{padding:3px 9px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:7px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));cursor:pointer;font-size:12px}' +
35
+ '.exp-open{padding:5px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));color:var(--dsw-alias-label-primary,var(--text,#1f2328));cursor:pointer;font-size:12px}' +
36
+ '.exp-open:hover{background:var(--dsw-alias-bg-layer-2,var(--bg-subtle,#f6f8fa))}' +
37
+ // 头部状态徽章(与画布标签职责区分:徽章=状态+并排面板)
38
+ '.exp-live-badge{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}' +
39
+ '.exp-live-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l2,#c5ccd6);flex:none}' +
40
+ '.exp-live-dot.on{background:#22b07d;animation:exp-pulse 1.4s ease-in-out infinite}' +
41
+ '.exp-live-badge.run{border-color:#9db4ff;color:var(--dsw-alias-state-business-primary,#0969da)}' +
42
+ '.exp-live-badge.run .exp-live-dot{background:#3b6ef5}' +
43
+ '.exp-live-badge.dec{border-color:#e0a83c;color:#8a6100;background:rgba(224,168,60,.10)}' +
44
+ '.exp-live-badge.dec .exp-live-dot{background:#e0a83c;animation:exp-pulse 1.4s ease-in-out infinite}' +
45
+ '.exp-live-badge.warn{border-color:#f0bcb6;color:#b3291e;background:rgba(179,41,30,.08)}' +
46
+ '.exp-live-badge.warn .exp-live-dot{background:#c62828}' +
47
+ '.exp-live-badge.done{border-color:#bfe6d5;color:#1a7f5a;background:rgba(34,176,125,.10)}' +
48
+ '.exp-live-badge.done .exp-live-dot{background:#22b07d}' +
49
+ '.exp-modehint{padding:7px 14px;font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary,var(--text,#57606a));background:var(--dsw-alias-bg-layer-2,var(--bg-subtle,#f6f8fa));border-bottom:1px solid var(--dsw-alias-border-l1,var(--border,#eef1f5))}' +
50
+ // ── F1 计划草稿编辑器 ──
51
+ '.exp-plan{margin:0 0 10px;padding:10px 12px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:10px;background:linear-gradient(180deg,#f6f9ff,var(--dsw-alias-bg-layer-1,var(--bg,#fff)))}' +
52
+ '.exp-plan-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:2px}' +
53
+ '.exp-plan-t{font-weight:700;font-size:12.5px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
54
+ '.exp-plan-msg{margin-left:auto;font-size:11px;font-weight:700;color:#1a7f5a}' +
55
+ '.exp-plan-roles{display:flex;flex-wrap:wrap;gap:6px;align-items:center}' +
56
+ '.exp-plan-chip{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));background:var(--dsw-alias-bg-layer-3,var(--bg,#fff))}' +
57
+ '.exp-plan-add{font-size:11px;padding:2px 6px;border-radius:7px;border:1px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de));background:transparent;color:var(--dsw-alias-label-secondary,#57606a)}' +
58
+ '.exp-plan-row{display:flex;gap:4px;align-items:center;margin:4px 0}' +
59
+ '.exp-plan-id{font-family:ui-monospace,Menlo,monospace;font-size:10px;color:var(--dsw-alias-label-secondary,#57606a);flex:none;width:42px;overflow:hidden;text-overflow:ellipsis}' +
60
+ '.exp-plan-in{flex:1;min-width:0;font-size:11.5px;padding:2px 6px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));color:inherit}' +
61
+ '.exp-plan-sel{flex:none;width:74px;font-size:11px;padding:2px 4px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:6px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));color:inherit}' +
62
+ '.exp-plan-dep{flex:none;width:62px;font-size:10.5px;padding:2px 5px;border:1px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:6px;background:transparent;color:inherit}' +
63
+ '.exp-plan-btns{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}' +
64
+ '.exp-plan-b{font-size:11.5px;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));color:inherit;cursor:pointer}' +
65
+ '.exp-plan-b.go{border-color:#22b07d;color:#1a7f5a;font-weight:700;background:rgba(34,176,125,.10)}' +
66
+ '.exp-plan-b.bad{border-color:#f0bcb6;color:#b3291e}' +
67
+ '.exp-plan-discarded{margin:0 0 10px;padding:8px 12px;border:1px dashed #e0a83c;border-radius:10px;font-size:11.5px;color:#8a6100;background:rgba(224,168,60,.10)}' +
68
+ '.exp-body{flex:1;overflow:auto;padding:12px 14px 20px}' +
69
+ '.exp-prog{height:8px;border-radius:5px;background:var(--dsw-alias-bg-layer-2,var(--bg-subtle,#eef1f5));overflow:hidden;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de))}' +
70
+ '.exp-prog-fill{height:100%;border-radius:5px;background:linear-gradient(90deg,#3b6ef5,#22b07d);transition:width .4s}' +
71
+ '.exp-meta{display:flex;justify-content:space-between;align-items:center;font-size:11.5px;color:var(--dsw-alias-label-secondary,var(--text,#57606a));margin:8px 0 4px}' +
72
+ '.exp-sec{margin:16px 0 7px;font-size:11.5px;font-weight:700;letter-spacing:.02em;color:var(--dsw-alias-label-secondary,var(--text,#57606a));text-transform:uppercase;display:flex;align-items:center;gap:8px}' +
73
+ '.exp-sec::after{content:"";flex:1;height:1px;background:var(--dsw-alias-border-l1,var(--border,#e6e8ec))}' +
74
+ '.exp-sec-col{cursor:pointer;user-select:none}' +
75
+ '.exp-sec-col:hover{color:var(--dsw-alias-state-business-primary,#0969da)}' +
76
+ '.exp-chev{flex:none;font-size:10px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
77
+ '.exp-path{display:flex;flex-wrap:wrap;gap:6px;list-style:none;padding:0;margin:8px 0 0}' +
78
+ '.exp-path li{font-size:11px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:14px;color:var(--dsw-alias-label-secondary,var(--text,#57606a));background:var(--dsw-alias-bg-layer-1,var(--bg,#fff))}' +
79
+ '.exp-path li.done{background:#eef4ff;border-color:#c8daff;color:var(--dsw-alias-state-business-primary,#0969da)}' +
80
+ '.exp-path li.cur{background:linear-gradient(90deg,#3b6ef5,#2f5ad8);border-color:transparent;color:#fff;font-weight:700;box-shadow:0 2px 8px rgba(59,110,245,.35)}' +
81
+ '.exp-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 0;border-bottom:1px solid var(--dsw-alias-border-l1,var(--border,#eef1f5))}' +
82
+ '.exp-row.mem{flex-wrap:wrap;align-items:flex-start}' +
83
+ '.exp-mem-main{display:flex;align-items:center;gap:7px;flex:1;min-width:0}' +
84
+ '.exp-mem-name{font-weight:600}' +
85
+ '.exp-mem-sub{font-family:ui-monospace,monospace;font-size:10.5px;color:var(--dsw-alias-label-secondary,#57606a);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
86
+ '.exp-mem-cur{width:100%;font-size:11px;color:var(--dsw-alias-state-business-primary,#0969da);margin-left:14px;margin-top:2px}' +
87
+ '.exp-legend{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 2px;font-size:10.5px;color:var(--dsw-alias-label-secondary,#57606a)}' +
88
+ '.exp-legend i{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:4px}' +
89
+ '.exp-dot.running{animation:exp-pulse 1.4s ease-in-out infinite}' +
90
+ '@keyframes exp-pulse{0%,100%{opacity:1}50%{opacity:.4}}' +
91
+ '.exp-prog-fill{position:relative;overflow:hidden}' +
92
+ '.exp-prog-fill::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.55),transparent);animation:exp-shimmer 1.8s infinite}' +
93
+ '@keyframes exp-shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}' +
94
+ '.exp-edge-run{stroke-dasharray:6 4;animation:exp-dash 1s linear infinite}' +
95
+ '@keyframes exp-dash{to{stroke-dashoffset:-20}}' +
96
+ '.exp-node-run{animation:exp-nodepulse 1.6s ease-in-out infinite}' +
97
+ '@keyframes exp-nodepulse{0%,100%{stroke-opacity:1}50%{stroke-opacity:.3}}' +
98
+ '.exp-live{width:7px;height:7px;border-radius:50%;background:#22b07d;animation:exp-pulse 1.4s infinite;display:inline-block;margin-right:6px}' +
99
+ '.exp-mem-run{position:relative;overflow:hidden}' +
100
+ '.exp-mem-run::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(59,110,245,.14),transparent);animation:exp-shimmer 1.8s infinite}' +
101
+ '.exp-row.task{cursor:pointer;border-radius:8px;padding:7px 6px}' +
102
+ '.exp-row.task:hover{background:var(--dsw-alias-interactive-bg-hover,var(--bg-subtle,#f6f8fa))}' +
103
+ '.exp-row.task.cur{background:linear-gradient(90deg,rgba(59,110,245,.09),transparent)}' +
104
+ '.exp-ava{width:26px;height:26px;border-radius:50%;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex:none;margin-right:7px}' +
105
+ '.exp-lead{display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:10px;background:linear-gradient(90deg,#eef4ff,var(--dsw-alias-bg-layer-1,#fff));margin-bottom:8px}' +
106
+ '.exp-lead b{font-weight:700}' +
107
+ '.exp-panorama{display:grid;grid-template-columns:repeat(auto-fill,minmax(156px,1fr));gap:8px;margin:6px 0}' +
108
+ '.exp-agent{border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:10px;padding:8px 10px;background:var(--dsw-alias-bg-layer-1,#fff)}' +
109
+ '.exp-agent-a{display:flex;align-items:center;gap:7px}' +
110
+ '.exp-agent-n{font-weight:600;font-size:12.5px}' +
111
+ '.exp-agent-r{font-size:11px;color:var(--dsw-alias-label-secondary,#57606a);margin-top:4px}' +
112
+ '.exp-agent-t{font-size:11px;color:var(--dsw-alias-label-primary,#1f2328);margin-top:4px}' +
113
+ '.exp-agent-off{opacity:.5}' +
114
+ '.exp-flow-note{font-size:11px;color:var(--dsw-alias-label-secondary,#57606a);margin:2px 0 6px}' +
115
+ '.exp-flow-row{display:flex;gap:6px;align-items:stretch;overflow-x:auto;padding-bottom:4px;scrollbar-width:thin}' +
116
+ '.exp-flow{flex:0 0 auto;min-width:168px;max-width:224px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:10px;padding:7px 9px;background:var(--dsw-alias-bg-layer-1,#fff);cursor:pointer}' +
117
+ '.exp-flow.cur{border-color:var(--dsw-alias-state-business-primary,#0969da);box-shadow:0 0 0 1px rgba(9,105,218,.25)}' +
118
+ '.exp-flow-a{display:flex;align-items:center;gap:6px;font-size:12px}' +
119
+ '.exp-flow-a b{font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:84px}' +
120
+ '.exp-flow-t{font-size:11.5px;margin:4px 0 5px;color:var(--dsw-alias-label-primary,#1f2328);line-height:1.4}' +
121
+ '.exp-flow-a2{align-self:center;color:var(--dsw-alias-label-secondary,#57606a);font-size:13px;flex:none;padding:0 1px}' +
122
+ '.exp-flow-sep{height:8px;margin:2px 0;border-left:2px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de))}' +
123
+ '.exp-roster-strip{display:flex;gap:6px;flex-wrap:wrap;margin:4px 0 6px}' +
124
+ '.exp-roster-chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;padding:3px 7px;background:var(--dsw-alias-bg-layer-1,#fff)}' +
125
+ '.exp-roster-chip.exp-mem-run{border-color:#22b07d;box-shadow:0 0 0 1px rgba(34,176,125,.3)}' +
126
+ '.exp-codeidx{margin:2px 0 6px}' +
127
+ '.exp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);font-size:12px;color:var(--dsw-alias-label-primary,#1f2328);outline:none}' +
128
+ '.exp-search:focus{border-color:var(--dsw-alias-state-business-primary,#0969da)}' +
129
+ '.exp-qhits{margin-top:5px}' +
130
+ '.exp-qhit{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px;padding:2px 0;color:var(--dsw-alias-label-primary,#1f2328);border-bottom:1px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de))}' +
131
+ '.exp-feed{margin:6px 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:9px;background:var(--dsw-alias-bg-layer-2,#f6f8fa)}' +
132
+ '.exp-feed-h{display:flex;justify-content:space-between;align-items:center;font-weight:600;font-size:12px;margin-bottom:4px}' +
133
+ '.exp-feed-x{border:none;background:none;cursor:pointer;color:var(--dsw-alias-label-secondary,#57606a);font-size:12px}' +
134
+ '.exp-feed-i{display:flex;gap:6px;padding:2px 0;align-items:baseline;flex-wrap:wrap}' +
135
+ '.exp-feed-t{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--dsw-alias-label-secondary,#57606a);flex:none}' +
136
+ '.exp-feed-l{font-size:11.5px;color:var(--dsw-alias-label-primary,#1f2328)}' +
137
+ '.exp-feed-p{font-size:10.5px;color:var(--dsw-alias-label-secondary,#57606a);width:100%;margin-left:66px;white-space:pre-wrap;word-break:break-all}' +
138
+ '.exp-toggle{font-size:11px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:7px;cursor:pointer;background:var(--dsw-alias-bg-layer-3,#fff)}' +
139
+ '.exp-role{font-size:10px;padding:1px 6px;border-radius:8px;background:rgba(90,130,240,.12);color:var(--dsw-alias-state-business-primary,#0969da);border:1px solid var(--dsw-alias-border-l2,#d0d7de);white-space:nowrap}' +
140
+ '.exp-muted{color:var(--dsw-alias-label-secondary,var(--text,#57606a));font-size:11.5px}' +
141
+ '.exp-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}' +
142
+ '.exp-badge{font-size:10.5px;padding:1px 7px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--bg-subtle,#f0f2f4));border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));color:var(--dsw-alias-label-secondary,var(--text,#57606a));white-space:nowrap}' +
143
+ '.exp-badge.v-pass{color:#19713a;border-color:#b7e0c2;background:#ecf7ef}' +
144
+ '.exp-badge.v-fail{color:#b3291e;border-color:#f0bcb6;background:#fdeeed}' +
145
+ '.exp-badge.v-rev{color:#8a5a00;border-color:#f0d79a;background:#fff6e3}' +
146
+ '.exp-badge.st-pending{color:#57606a;border-color:#d0d7de;background:#f6f8fa}' +
147
+ '.exp-badge.st-claimed{color:#8a5a00;border-color:#f0d79a;background:#fff6e3}' +
148
+ '.exp-badge.st-progress{color:#0969da;border-color:#a8c7f5;background:#e9f2fe}' +
149
+ '.exp-badge.st-done{color:#19713a;border-color:#b7e0c2;background:#ecf7ef}' +
150
+ '.exp-badge.st-rework{color:#a34a00;border-color:#f3c9a0;background:#fff1e0}' +
151
+ '.exp-badge.st-failed{color:#b3291e;border-color:#f0bcb6;background:#fdeeed}' +
152
+ '.exp-badge.st-blocked{color:#6f5c00;border-color:#e3d488;background:#faf6e0}' +
153
+ '.exp-badge.st-cancelled{color:#57606a;border-color:#d0d7de;background:#eceff1}' +
154
+ '.exp-dag{overflow:auto;margin:4px 0;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:10px;background:var(--dsw-alias-bg-layer-1,var(--bg,#fff))}' +
155
+ '.exp-node{cursor:pointer}' +
156
+ '.exp-node:hover .exp-node-bg{stroke:var(--dsw-alias-state-business-primary,#0969da);stroke-width:1.5}' +
157
+ '.exp-node-bg{transition:stroke .12s}' +
158
+ // 底部状态带:运行中流光("动态的");状态变更时整卡闪两下
159
+ '.exp-node-bar{transition:fill .2s}' +
160
+ '.exp-node-bar-run{animation:exp-bar-shimmer 1.6s linear infinite}' +
161
+ '@keyframes exp-bar-shimmer{0%{fill-opacity:1}50%{fill-opacity:.6}100%{fill-opacity:1}}' +
162
+ '.exp-node-flash{animation:exp-node-flash 1.1s ease-out 2}' +
163
+ '@keyframes exp-node-flash{0%{stroke-width:4;stroke-opacity:1}60%{stroke-width:4;stroke-opacity:.45}100%{stroke-width:1.6;stroke-opacity:1}}' +
164
+ '.exp-node-status{pointer-events:none}' +
165
+ '.exp-legend-item{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));background:var(--dsw-alias-bg-layer-1,var(--bg,#fff))}' +
166
+ '.exp-legend-item b{font-variant-numeric:tabular-nums}' +
167
+ '.exp-legend-item.zero{opacity:.55}' +
168
+ '.exp-legend-item.live{border-color:#9db4ff;box-shadow:0 0 0 2px rgba(59,110,245,.10);animation:exp-pulse 1.6s ease-in-out infinite}' +
169
+ '.exp-sub{font-size:9.5px;font-family:ui-monospace,monospace;fill:var(--dsw-alias-label-secondary,#57606a)}' +
170
+ '.exp-art{display:inline-block;padding:3px 9px;margin:3px 4px 0 0;font-size:11px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:7px;cursor:pointer;background:var(--dsw-alias-bg-layer-1,var(--bg,#fff));transition:border-color .12s}' +
171
+ '.exp-art:hover{background:var(--dsw-alias-interactive-bg-hover,var(--bg-subtle,#f6f8fa));border-color:var(--dsw-alias-state-business-primary,#0969da)}' +
172
+ '.exp-detail{margin-top:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:9px;background:var(--dsw-alias-bg-layer-1,var(--bg,#fff));font-size:12px}' +
173
+ '.exp-detail h4{margin:0 0 6px;font-size:12.5px}' +
174
+ '.exp-kv{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11.5px;margin:4px 0}' +
175
+ '.exp-kv b{color:var(--dsw-alias-label-secondary,var(--text,#57606a));font-weight:500}' +
176
+ '.exp-detail-who{display:flex;align-items:center;gap:6px;font-size:12px;margin:2px 0 6px}' +
177
+ '.exp-task-op{display:flex;gap:6px;margin:2px 0 6px}' +
178
+ '.exp-op-btn{font-size:11px;padding:2px 10px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;background:var(--dsw-alias-bg-layer-3,#fff);cursor:pointer;color:var(--dsw-alias-label-primary,#1f2328)}' +
179
+ '.exp-op-btn.bad{color:#b3291e;border-color:#f0bcb6}' +
180
+ '.exp-fileview{margin-top:5px}' +
181
+ '.exp-fileview-h{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary,#57606a);margin-bottom:2px}' +
182
+ '.exp-tabs{display:flex;gap:6px;align-items:center;margin:6px 0 8px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));padding-bottom:4px}' +
183
+ '.exp-tab{font-size:12px;padding:3px 12px;border-radius:8px;cursor:pointer;color:var(--dsw-alias-label-primary,#1f2328);opacity:.72}' +
184
+ '.exp-tab.on{opacity:1;font-weight:600;background:var(--dsw-alias-state-business-primary,#0969da);color:#fff}' +
185
+ '.exp-viol-badge{margin-left:auto;font-size:11px;padding:2px 8px;border-radius:9px;background:#c62828;color:#fff;cursor:pointer;font-weight:600}' +
186
+ '.exp-viol-list{margin:2px 0 6px;font-size:11.5px}' +
187
+ '.exp-viol-list div{padding:2px 0;color:#b3291e}' +
188
+ '.exp-board-row{display:flex;gap:6px;align-items:baseline;padding:5px 6px;border-bottom:1px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de));cursor:pointer}' +
189
+ '.exp-board-t{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--dsw-alias-label-secondary,#57606a);flex:none}' +
190
+ '.exp-board-n{font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:150px}' +
191
+ '.exp-detail-sec{border-top:1px dashed var(--dsw-alias-border-l2,var(--border,#d0d7de));margin-top:8px;padding-top:6px}' +
192
+ '.exp-detail-sec b{font-size:11px;color:var(--dsw-alias-label-secondary,#57606a);font-weight:600}' +
193
+ '.exp-doing{font-size:11.5px;margin-top:3px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
194
+ '.exp-files{margin-top:3px}' +
195
+ '.exp-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px;color:var(--dsw-alias-label-primary,#1f2328);padding:1px 0}' +
196
+ '.exp-log{margin:4px 0 0;padding:6px 8px;background:var(--dsw-alias-bg-layer-2,#f6f8fa);border-radius:6px;font-size:10.5px;line-height:1.5;white-space:pre-wrap;word-break:break-all;max-height:140px;overflow:auto;color:var(--dsw-alias-label-primary,#1f2328)}' +
197
+ '.exp-findings{margin:6px 0 0;padding-left:16px;font-size:11.5px}' +
198
+ '.exp-findings li{margin:2px 0}' +
199
+ '.exp-preview{margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:9px;background:var(--dsw-alias-bg-layer-1,var(--bg,#fff));font-size:12px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;max-height:330px;overflow:auto}' +
200
+ '.exp-empty{color:var(--dsw-alias-label-secondary,var(--text,#57606a));font-size:12px;padding:8px 0}' +
201
+ '.exp-err{color:#b3291e;font-size:12px;padding:8px 0}' +
202
+ '.exp-viol{color:#fff;background:linear-gradient(90deg,#c62828,#e53935);font-size:12px;font-weight:600;padding:7px 10px;border-radius:8px;margin:0 0 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
203
+ '.exp-decision{margin:8px 0;padding:12px 14px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:10px;background:linear-gradient(180deg,#f6f9ff,var(--dsw-alias-bg-layer-1,#fff))}' +
204
+ '.exp-decision-title{font-weight:700;font-size:13px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
205
+ '.exp-opt{padding:5px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#1f2328)}' +
206
+ '.exp-opt:hover{border-color:var(--dsw-alias-state-business-primary,#0969da);color:var(--dsw-alias-state-business-primary,#0969da)}' +
207
+ // ── 对话流角色化工具卡(tool.call.toolview 注册)──
208
+ '.et-tc{margin:6px 0;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:10px;background:linear-gradient(180deg,var(--dsw-alias-bg-module-platform,var(--bg-subtle,#f6f8fa)),var(--dsw-alias-bg-layer-1,var(--bg,#fff)));font-size:12.5px;color:var(--dsw-alias-label-primary,#1f2328)}' +
209
+ '.et-tc.run{border-color:#9db4ff;box-shadow:0 0 0 3px rgba(59,110,245,.08);animation:et-tc-pulse 1.6s ease-in-out infinite}' +
210
+ '.et-tc.err{border-color:#f0bcb6;background:linear-gradient(180deg,#fff5f4,var(--dsw-alias-bg-layer-1,var(--bg,#fff)))}' +
211
+ '@keyframes et-tc-pulse{0%,100%{box-shadow:0 0 0 3px rgba(59,110,245,.08)}50%{box-shadow:0 0 0 6px rgba(59,110,245,.16)}}' +
212
+ '.et-tc-h{display:flex;align-items:center;gap:7px}' +
213
+ '.et-tc-ava{width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:12px;background:rgba(120,140,255,.14);border:1px solid rgba(140,160,255,.3);flex:none}' +
214
+ '.et-tc-nm{font-weight:700}' +
215
+ '.et-tc-duty{font-size:11px;color:var(--dsw-alias-label-secondary,#57606a)}' +
216
+ '.et-tc-st{margin-left:auto;font-size:10.5px;font-weight:700;padding:1px 8px;border-radius:8px;white-space:nowrap;color:#3b6ef5;background:rgba(59,110,245,.1)}' +
217
+ '.et-tc-st.done{color:#22b07d;background:rgba(34,176,125,.12)}' +
218
+ '.et-tc-st.e{color:#b3291e;background:rgba(179,41,30,.1)}' +
219
+ '.et-tc-t{margin:6px 0 3px;font-weight:600;line-height:1.45}' +
220
+ '.et-tc-f{font-size:11.5px;color:var(--dsw-alias-label-secondary,#57606a);line-height:1.5;word-break:break-all}' +
221
+ '.et-tc-b{display:flex;gap:6px;margin-top:7px}' +
222
+ '.et-tc-b button{font-size:11px;padding:2px 10px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:7px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));cursor:pointer;color:var(--dsw-alias-state-business-primary,#0969da)}' +
223
+ // ── N5 嵌入式画布视图(conversation.view)──
224
+ '.exp-canvas{position:static;width:100%;height:100%;border-left:none;box-shadow:none;border-radius:0}' +
225
+ '.exp-canvas .exp-head{border-radius:0}' +
226
+ // ── N1 对话流团队动态胶囊 ──
227
+ '.exp-cap{position:fixed;z-index:118;top:64px;left:50%;transform:translateX(-50%);max-width:76vw;font-size:12px}' +
228
+ '.exp-cap-btn{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:999px;background:linear-gradient(180deg,#1f2937,#111827);color:#e6edff;border:1px solid rgba(140,160,255,.45);box-shadow:0 6px 18px rgba(0,0,0,.35);cursor:pointer;max-width:100%}' +
229
+ '.exp-cap-dot{width:8px;height:8px;border-radius:50%;background:#22b07d;animation:exp-pulse 1.4s ease-in-out infinite;flex:none}' +
230
+ '.exp-cap-tx{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
231
+ '.exp-cap-panel{margin-top:6px;background:var(--dsw-alias-bg-base,var(--bg,#fff));border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.28);padding:6px 8px;max-height:320px;overflow:auto}' +
232
+ '.exp-cap-row{display:flex;gap:8px;align-items:baseline;padding:5px 6px;font-size:12px;color:var(--dsw-alias-label-primary,#1f2328);border-bottom:1px dashed var(--dsw-alias-border-l1,#eef1f5)}' +
233
+ '.exp-cap-row.cur{font-weight:700}' +
234
+ '.exp-cap-ic{flex:none}' +
235
+ '.exp-cap-t{margin-left:auto;font-family:ui-monospace,monospace;font-size:10px;color:var(--dsw-alias-label-secondary,#57606a)}' +
236
+ '.exp-cap-foot{padding:7px 6px 3px;text-align:right}' +
237
+ '.exp-cap-foot button{font-size:11.5px;padding:3px 12px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:8px;background:transparent;color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer}' +
238
+ // ── N2 待拍板横幅(composer 接管)──
239
+ '.exp-decbar{display:flex;align-items:center;gap:10px;padding:10px 14px;margin:6px 12px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:12px;background:linear-gradient(180deg,#f6f9ff,var(--dsw-alias-bg-layer-1,#fff));box-shadow:0 4px 14px rgba(59,110,245,.14)}' +
240
+ '.exp-decbar-ic{font-size:18px;flex:none}' +
241
+ '.exp-decbar-t{font-weight:700;font-size:13px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
242
+ '.exp-decbar-p{font-size:12px;color:var(--dsw-alias-label-secondary,#57606a);margin-top:2px}' +
243
+ // ── 任务人员流转(波次带)样式:来自 UI.md §4.2(.etv-*,主题变量 + 深色覆写,零依赖)──
244
+ '/* ── 任务人员流转:竖直时间轴 + 波次带 ── */\n.etv-root{display:block;position:relative;padding:2px 0 4px}\n.etv-note{font-size:11px;color:var(--dsw-alias-label-secondary);margin:2px 0 8px;display:flex;align-items:center;gap:6px}\n.etv-wide-btn{margin-left:auto;font-size:10.5px;padding:1px 8px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}\n.etv-wave{position:relative;margin:0 0 var(--etv-wave-gap);border-radius:0 8px 8px 0}\n.etv-wave.cur{background:rgba(65,118,230,.055);border-left:2px solid var(--dsw-alias-state-business-primary)}\n.etv-wave-h{display:flex;align-items:center;gap:6px;height:22px;padding-left:var(--etv-indent);font-size:11.5px;font-weight:600;cursor:default}\n.etv-wave.collapsed .etv-wave-h{cursor:pointer}\n.etv-wave-no{display:inline-flex;align-items:center;justify-content:center;height:16px;min-width:16px;padding:0 5px;border-radius:8px;font-size:10px;font-weight:700;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}\n.etv-wave.cur .etv-wave-no{background:var(--dsw-alias-state-business-primary);color:#fff}\n.etv-time{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}\n.etv-delta{font-size:10px;color:var(--dsw-alias-label-tertiary)}\n.etv-wave-n{font-size:10.5px;color:var(--dsw-alias-label-tertiary)}\n.etv-wave-sum{margin-left:auto;font-size:10.5px;color:var(--dsw-alias-label-tertiary);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.etv-cards{display:flex;flex-wrap:wrap;gap:var(--etv-gap);padding:6px 0 2px var(--etv-indent)}\n.etv-scroll{overflow-x:auto;padding-bottom:4px;scrollbar-width:thin}\n.etv-scroll .etv-cards{flex-wrap:nowrap}\n.etv-card{flex:1 1 var(--etv-card-min);min-width:0;max-width:100%;box-sizing:border-box;border:1.4px solid var(--etv-idle);border-radius:9px;background:var(--dsw-alias-bg-layer-1);padding:6px 8px 6px 7px;min-height:56px;cursor:pointer;position:relative;transition:border-color .12s,background .12s}\n.etv-card:hover{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-hover)}\n.etv-card.sel{background:var(--dsw-alias-state-business-tertiary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary)}\n.etv-card.run{border-color:var(--etv-run);border-width:2px;background:rgba(59,110,245,.08)}\n.etv-card.done{border-color:var(--etv-ok);background:rgba(34,176,125,.10)}\n.etv-card.rework{border-color:var(--etv-rework);border-style:dashed;background:rgba(217,119,6,.10)}\n.etv-card.failed{border-color:var(--etv-fail);background:rgba(192,57,43,.09)}\n.etv-card.more{align-items:center;justify-content:center;display:flex;border-style:dashed;color:var(--dsw-alias-label-secondary);font-size:11.5px}\n.etv-card.tiny{min-height:40px}\n.etv-card.tiny .etv-task{display:none}\n.etv-card-h{display:flex;align-items:center;gap:5px;min-width:0}\n.etv-card .exp-ava{width:18px;height:18px;font-size:10px;margin-right:0;flex:none}\n.etv-role{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 auto;min-width:0}\n.etv-who{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 auto;min-width:0}\n.etv-task{margin-top:3px;font-size:11px;line-height:1.35;color:var(--dsw-alias-label-primary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.etv-card-f{display:flex;align-items:center;gap:6px;margin-top:5px;height:16px}\n.etv-dot{width:8px;height:8px;border-radius:50%;flex:none;display:inline-block}\n.etv-dot.run{animation:exp-pulse 1.4s ease-in-out infinite}\n.etv-dot.hollow{background:transparent!important;border:1.5px solid var(--etv-idle)}\n.etv-dur{margin-left:auto;font-family:ui-monospace,Menlo,monospace;font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}\n.etv-rework-tag{position:absolute;top:-7px;left:6px;font-size:9.5px;line-height:14px;padding:0 5px;border-radius:7px;background:var(--dsw-alias-state-warn-tertiary,#fff7ea);border:1px solid #f3c9a0;color:#a34a00}\n.etv-rail{position:absolute;left:0;top:0;bottom:0;width:16px;pointer-events:none}\n.etv-rail-i{position:absolute;left:7px;top:0;bottom:0;width:2px;background:var(--etv-line)}\n.etv-rail-n{position:absolute;left:3px;width:10px;height:10px;border-radius:50%;border:2px solid var(--dsw-alias-bg-layer-1);box-sizing:content-box}\n.etv-handoff{stroke:#8aa8e8;stroke-width:1.4;fill:none}\n.etv-rework-link{stroke:var(--etv-rework);stroke-width:1.5;stroke-dasharray:4 3;fill:none}\n.etv-para{font-size:10px;color:var(--dsw-alias-label-tertiary);margin-left:2px}\n.etv-group{border:1px dashed var(--dsw-alias-border-l2);border-radius:9px;padding:6px 8px;margin:0 0 var(--etv-gap) var(--etv-indent);background:var(--dsw-alias-bg-module-platform)}\n/* ── 深色主题覆写(零依赖:prefers-color-scheme 兜底;若 shell 在根节点暴露主题标记,优先用它) ── */\n@media (prefers-color-scheme:dark){\n .exp-panel{--etv-run:#679efe;--etv-ok:#4ed17e;--etv-rework:#f7ad31;--etv-fail:#ff7b7b;--etv-idle:#adb2b8;\n --etv-text-ok:#4ed17e;--etv-text-run:#679efe;--etv-text-rework:#f7ad31}\n .etv-handoff{stroke:#679efe}\n .etv-rework-tag{background:#3a2c14;border-color:#7a5a22;color:#f7ad31}\n .etv-degrade.warn{background:rgba(247,173,49,.12);color:#f7ad31}\n .etv-degrade.info{color:#9dc0ff}\n .etv-wave.cur{background:rgba(103,158,254,.10)}\n}\n/* ── 无障碍:减少动效 ── */\n@media (prefers-reduced-motion:reduce){.etv-dot.run,.etv-skel{animation:none!important}}'
245
+
246
+ // lead 微调(UI.md 基线之上):360px 两列时 8px 间距偏挤 → 10px;卡更高一点;分组区更松
247
+ + '.etv-cards{gap:10px}.etv-card{min-height:58px}.etv-group{padding:8px 10px 4px}'
248
+ // SVG 树(照抄目标形态):曲线 + 节点卡,全部用主题变量,深色自动适配
249
+ + '.etv-graph{overflow-x:auto;margin:2px 0 6px;padding-bottom:2px}'
250
+ + '.etv-curve{fill:none;stroke:#8aa8e8;stroke-width:1.4;opacity:.85}'
251
+ // 依赖边是视觉主角(真实 DAG),委派边降权成虚线
252
+ + '.etv-dep-edge{fill:none;stroke:#5b8def;stroke-width:1.4;opacity:.95;stroke-linecap:round}'
253
+ + '.etv-g-node{fill:var(--dsw-alias-bg-layer-1);stroke:var(--dsw-alias-border-l2);stroke-width:1.4}'
254
+ + '.etv-g-node.run{stroke:var(--etv-run);stroke-width:2}'
255
+ + '.etv-g-node.failed{stroke:var(--etv-fail);stroke-width:2;stroke-dasharray:5 3}'
256
+ + '.etv-g-node.crit{stroke:var(--etv-ok);stroke-width:2}'
257
+ + '.etv-g-node.lead{stroke:var(--etv-lead);stroke-width:1.6}'
258
+ + '.etv-link{position:relative;height:16px;margin-left:var(--etv-indent)}.etv-link i{position:absolute;left:6px;top:0;bottom:0;width:2px;background:var(--etv-line)}.etv-link b{position:absolute;left:1.5px;bottom:-2px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}.etv-list{position:relative;margin:2px 0 6px var(--etv-indent);padding-left:16px}.etv-list-i{position:absolute;left:0;top:0;bottom:14px;width:2px;background:var(--etv-line)}.etv-row{position:relative;margin:0 0 10px}.etv-row::before{content:"";position:absolute;left:-16px;top:28px;width:14px;height:2px;background:var(--etv-line)}.etv-row::after{content:"";position:absolute;left:-19px;top:25px;width:6px;height:6px;border-radius:50%;background:var(--etv-line)}'
259
+ // ── session store (mirrors dsh-md-preview) ──
260
+ var currentSessionId = null
261
+ var open = false // default collapsed so the panel never auto-blocks the workspace; open via the「专家团」header button
262
+ var listeners = new Set()
263
+ function notify() { listeners.forEach(function (f) { try { f() } catch (e) {} }) }
264
+ function setCurrentSession(sid) { if (currentSessionId === sid) return; currentSessionId = sid; notify() }
265
+ function setOpen() { open = !open; notify() }
266
+ function subscribe(f) { listeners.add(f); return function () { listeners.delete(f) } }
267
+ function useExternal(getValue) { var s = useState(getValue()); useEffect(function () { return subscribe(function () { s[1](getValue()) }) }, []); return s[0] }
268
+ /** 对话流 workflow 卡是否已回退到 dsh 原生(localStorage 开关,刷新生效)。 */
269
+ function nativeWfOn() { try { return window.localStorage.getItem('et-native-workflow') === '1' } catch (e) { return false } }
270
+ function useCurrentSession() { return useExternal(function () { return currentSessionId }) }
271
+ function useOpen() { return useExternal(function () { return open }) }
272
+ // 切换开关必须**订阅**才可见:旧写法在渲染时直接调 nativeWfOn(),而 notify() 触发的一批
273
+ // setState 值全都没变 ⇒ React 直接 bail out ⇒ 标签不翻转,点击看起来「没反应」。
274
+ function useNativeWf() { return useExternal(nativeWfOn) }
275
+
276
+ // ── panel dock / drag state (memory like dsh-md-preview) ──
277
+ function loadLS(key, def) { try { var v = window.localStorage.getItem(key); return v ? JSON.parse(v) : def } catch (e) { return def } }
278
+ function saveLS(key, v) { try { window.localStorage.setItem(key, JSON.stringify(v)) } catch (e) {} }
279
+ var dockState = loadLS('et-dock', true)
280
+ var panelPos = loadLS('et-pos', { x: 0, y: 0 })
281
+ var panelWidth = loadLS('et-width', 360)
282
+ function clampW(w) { return Math.max(280, Math.min(640, w)) }
283
+ function setDock(v) { if (dockState === v) return; dockState = v; if (!v) { panelPos = { x: ((window && window.innerWidth) || 1280) - panelWidth, y: 80 }; saveLS('et-pos', panelPos) } saveLS('et-dock', dockState); notify() }
284
+ function setPanelPos(x, y) { panelPos = { x: x, y: y }; saveLS('et-pos', panelPos); notify() }
285
+ /**
286
+ * 任务点击 → 把团队面板**停靠到右侧**并展开,用右栏展示该任务详情,而不是停在浮动浮层里。
287
+ * 停靠态由 shell frame 让位(见 Panel 的 paddingRight 副作用),因此是真正的右侧并排栏。
288
+ * 直接置位 open(setOpen() 是 toggle 语义,会误关已打开的面板)。
289
+ */
290
+ function focusPanelRight() {
291
+ try {
292
+ var changed = false
293
+ if (dockState !== true) { dockState = true; saveLS('et-dock', true); changed = true }
294
+ if (!open) { open = true; changed = true }
295
+ if (changed) notify()
296
+ } catch (e) {}
297
+ }
298
+ function setPanelWidth(w) { panelWidth = clampW(w); saveLS('et-width', panelWidth); notify() }
299
+
300
+ /**
301
+ * ── 对话流卡片 → 右侧面板的「聚焦意图」通道 ──
302
+ * 对话里的角色卡/任务卡被点击时**不跳新页面、不切视图**:只写一条意图并把面板停靠到右侧,
303
+ * 由常驻的 Panel 消费该意图、选中对应角色的进行中任务 —— 详情就渲染在右栏。
304
+ * 用自增 seq 做去重,避免轮询重渲染时重复应用同一条意图。
305
+ */
306
+ var focusIntent = null
307
+ var focusSeq = 0
308
+ function requestFocus(v) { focusSeq += 1; focusIntent = Object.assign({ seq: focusSeq }, v || {}); notify() }
309
+ function useFocusIntent() { return useExternal(function () { return focusIntent }) }
310
+
311
+ // ── 波次聚类(SPEC §2.3 纯函数,可单测)──────────────────────────────
312
+ // 主轴 = 时间波次:同波内 createdAt 相差 ≤ WAVE_GAP_MS,视为"同一批派工"(实证同波 Δ≤1ms、
313
+ // 波间 ≥60s,故 1500ms 切分零歧义:13 人 → 7 波)。**不臆造**:createdAt 缺失的人进 untimed,
314
+ // 绝不按数组顺序/阶段/依赖去伪造批次。
315
+ var WAVE_GAP_MS = 1500
316
+ var MAX_WAVES = 12
317
+ function buildWaves(people, gapMs) {
318
+ var gap = typeof gapMs === 'number' && gapMs > 0 ? gapMs : WAVE_GAP_MS
319
+ var timed = [], untimed = []
320
+ ;(people || []).forEach(function (p) {
321
+ if (p && typeof p.createdAt === 'number' && p.createdAt > 0) timed.push(p)
322
+ else if (p) untimed.push(p)
323
+ })
324
+ timed.sort(function (a, b) { return (a.createdAt - b.createdAt) || String(a.id || '').localeCompare(String(b.id || '')) })
325
+ var waves = []
326
+ timed.forEach(function (p) {
327
+ var last = waves[waves.length - 1]
328
+ if (last && p.createdAt - last.startAt <= gap) last.members.push(p)
329
+ else waves.push({ index: waves.length + 1, startAt: p.createdAt, members: [p] })
330
+ })
331
+ // 精修(仅当有 parentId):派生 ≠ 并列 —— 父在同波时,把子挪到下一波(实证 047765b8)
332
+ if (waves.length && timed.some(function (p) { return p && p.parentId })) {
333
+ var moved = true, guard = 0
334
+ while (moved && guard++ < 8) {
335
+ moved = false
336
+ for (var wi = 0; wi < waves.length; wi++) {
337
+ var w = waves[wi]
338
+ var keep = [], spill = []
339
+ w.members.forEach(function (p) {
340
+ var sameWaveParent = p.parentId && w.members.some(function (q) { return q.id && String(q.id) === String(p.parentId) })
341
+ if (sameWaveParent) { spill.push(p); moved = true } else keep.push(p)
342
+ })
343
+ if (spill.length) {
344
+ w.members = keep
345
+ if (!waves[wi + 1]) waves.push({ index: wi + 2, startAt: spill[0].createdAt, members: [] })
346
+ waves[wi + 1].members = waves[wi + 1].members.concat(spill)
347
+ }
348
+ }
349
+ waves = waves.filter(function (w) { return w.members.length })
350
+ }
351
+ }
352
+ waves.forEach(function (w, i) { w.index = i + 1 })
353
+ // 上限:> MAX_WAVES 时把尾部并入最后一波
354
+ var merged = 0
355
+ if (waves.length > MAX_WAVES) {
356
+ var tail = waves.slice(MAX_WAVES - 1)
357
+ tail.forEach(function (w) { merged += w.members.length })
358
+ waves = waves.slice(0, MAX_WAVES - 1)
359
+ waves.push({ index: MAX_WAVES, startAt: tail.length ? tail[0].startAt : 0, members: tail.reduce(function (a, w) { return a.concat(w.members) }, []), merged: merged })
360
+ }
361
+ var base = waves.length ? waves[0].startAt : 0
362
+ return { waves: waves, untimed: untimed, base: base, gap: gap }
363
+ }
364
+
365
+ /**
366
+ * 补全成员的 `createdAt` —— **全景图的分层依据**(纯函数,便于单测)。
367
+ *
368
+ * 为什么需要:分层唯一依据是 `createdAt`,而 **workflow 派生的子代理没有会话 header**,
369
+ * host 只能拿到 0。实测本会话 30 个子代理**全部**为 0 ⇒ `buildWaves` 判为
370
+ * 「无创建时间记录,无法分批」⇒ 全景图退化成一条竖直列表(用户看到的就是这个)。
371
+ *
372
+ * 两源,按可信度降级,并**回报用了哪一源**(UI 必须如实标注,不得把推断当事实):
373
+ * ① `header` —— host 从子会话 header 读到的真实时刻(最准)
374
+ * ② `event` —— 父会话事件流 `tool-workflow/agent-start` 的**真实墙钟时间**
375
+ * (实测同一次扇出内 Δ≤1ms、扇出之间 ~25s,正好是 buildWaves 的切分粒度)
376
+ * 两源都拿不到 ⇒ 保持 `createdAt: 0` 走既有的「无时间戳」降级路径,
377
+ * **刻意不按数组下标合成时间戳**:那会把"不知道"伪装成"知道"。
378
+ *
379
+ * 顺带把 workflow 归属与**逐子代理结算态**挂上,供画布画"同一次扇出"的边、
380
+ * 并把"被中断、从未结算"的节点标红(而不是让它永远转圈)。
381
+ */
382
+ function enrichPeopleTiming(people, wfChildren) {
383
+ var meta = (wfChildren && typeof wfChildren === 'object' && !Array.isArray(wfChildren)) ? wfChildren : {}
384
+ var out = [], used = { header: 0, event: 0, none: 0 }
385
+ ;(people || []).forEach(function (p) {
386
+ if (!p) return
387
+ var q = { id: p.id, role: p.role, label: p.label, activity: p.activity, model: p.model,
388
+ lastEventAt: p.lastEventAt, parentId: p.parentId, depth: p.depth, projected: p.projected }
389
+ var m = meta[String(p.id || '')] || null
390
+ var t = (typeof p.createdAt === 'number' && p.createdAt > 0) ? p.createdAt : 0
391
+ if (t) { q.createdAt = t; q.timeSource = 'header'; used.header += 1 }
392
+ else if (m && typeof m.startedAt === 'number' && m.startedAt > 0) { q.createdAt = m.startedAt; q.timeSource = 'event'; used.event += 1 }
393
+ else { q.createdAt = 0; q.timeSource = 'none'; used.none += 1 }
394
+ if (m) {
395
+ q.runId = String(m.runId || '')
396
+ q.wfPhase = String(m.phase || '')
397
+ q.wfLabel = String(m.label || '')
398
+ q.settled = m.settled === true
399
+ q.outcome = String(m.outcome || '')
400
+ }
401
+ out.push(q)
402
+ })
403
+ return { people: out, used: used }
404
+ }
405
+
406
+ /**
407
+ * 对话流 workflow 卡把「阶段 → 成员」结构发到这里,面板据此画**任务人员流转图**:
408
+ * 列 = 阶段(真实阶段顺序,来自 dsh 的 workflow-run 节点),卡 = 人(按角色聚合)。
409
+ * 只有 workflow 卡渲染过才会有数据;没有时面板退回"依赖层"分列。
410
+ */
411
+ var wfPhaseStore = { name: '', phases: null, sig: '' }
412
+ function publishWfPhases(name, phases) {
413
+ var sig = ''
414
+ try {
415
+ sig = JSON.stringify([name, (phases || []).map(function (p) { return [p.key, (p.members || []).map(function (m) { return [m.childId, m.label, m.status] })] })])
416
+ } catch (e) { sig = 'x' + Date.now() }
417
+ if (sig === wfPhaseStore.sig) return
418
+ wfPhaseStore = { name: name, phases: phases, sig: sig }
419
+ notify()
420
+ }
421
+ function useWfPhases() { return useExternal(function () { return wfPhaseStore }) }
422
+ function useDocked() { return useExternal(function () { return dockState }) }
423
+ function usePanelPos() { return useExternal(function () { return panelPos }) }
424
+ function usePanelWidth() { return useExternal(function () { return panelWidth }) }
425
+
426
+ // ── i18n: follow the shell's document lang (zh/en) ──
427
+ function detectLang() { try { var l = (document.documentElement.lang || '').toLowerCase(); return l.indexOf('zh') === 0 ? 'zh' : 'en' } catch (e) { return 'zh' } }
428
+ var langListeners = new Set()
429
+ var langNow = detectLang()
430
+ function notifyLang() { langNow = detectLang(); langListeners.forEach(function (f) { try { f() } catch (e) {} }) }
431
+ if (typeof MutationObserver !== 'undefined') { try { new MutationObserver(notifyLang).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] }) } catch (e) {} }
432
+ function useLang() { var s = useState(detectLang()); useEffect(function () { function on() { s[1](detectLang()) } langListeners.add(on); return function () { langListeners.delete(on) } }, []); return s[0] }
433
+ function t(zh, en) { return langNow === 'en' ? en : zh }
434
+
435
+ function startResize(e, curW) {
436
+ var startX = e.clientX, baseW = curW
437
+ function onMove(ev) { setPanelWidth(baseW - (ev.clientX - startX)) }
438
+ function onUp() { if (window.removeEventListener) { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) } }
439
+ if (window.addEventListener) { window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp) }
440
+ if (e.preventDefault) e.preventDefault()
441
+ }
442
+ function startDrag(e, curDock, curPos) {
443
+ if (curDock) return
444
+ var startX = e.clientX, startY = e.clientY, baseX = curPos.x, baseY = curPos.y
445
+ function onMove(ev) { setPanelPos(baseX + (ev.clientX - startX), baseY + (ev.clientY - startY)) }
446
+ function onUp() { if (window.removeEventListener) { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) } }
447
+ if (window.addEventListener) { window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp) }
448
+ if (e.preventDefault) e.preventDefault()
449
+ }
450
+
451
+ // Text is rendered by React as a text node, so React already escapes it.
452
+ // Escaping here as well produced DOUBLE-escaped text on screen: a live-feed
453
+ // line such as `cat x.yml; echo "=== dirs ===";` showed literal `&quot;`.
454
+ // Kept as the identity (all call sites document "this is text, not markup";
455
+ // there is no innerHTML/dangerouslySetInnerHTML anywhere in this client).
456
+ function esc(s) { return String(s == null ? '' : s) }
457
+ function basename(p) { var i = String(p).lastIndexOf('/'); return i >= 0 ? String(p).slice(i + 1) : String(p) }
458
+
459
+ var PHASES = ['clarify', 'research', 'design', 'spec-review', '方案确认', 'implement', 'review', 'test', 'deliver']
460
+ var PHASE_ZH = { clarify: '澄清', research: '调研', design: '设计', 'spec-review': '规格评审', '方案确认': '方案确认', implement: '实现', review: '审查', test: '测试', deliver: '交付' }
461
+ function phaseLabel(p) { var zh = PHASE_ZH[p] || p; var en = { clarify: 'Clarify', research: 'Research', design: 'Design', 'spec-review': 'Spec Review', implement: 'Implement', review: 'Review', test: 'Test', deliver: 'Deliver' }[p] || p; return (p === '方案确认') ? (t('方案确认', 'Approve')) : (langNow === 'en' ? en : zh) }
462
+ var ARTIFACT = ['SPEC', 'PLAN', 'RESEARCH', 'REVIEW-SPEC', 'REVIEW', 'TEST', 'SUMMARY', 'RETRO', 'TASK', '任务看板']
463
+ var FINAL = ['completed', 'done', 'cancelled', 'failed']
464
+ var ST_COLOR = { pending: '#9aa4b2', claimed: '#e0a83c', in_progress: '#3b6ef5', done: '#22b07d', rework: '#e05f45', blocked: '#8a6d00', failed: '#c0392b', cancelled: '#8a94a3', completed: '#22b07d' }
465
+ function stColor(s) { return ST_COLOR[s] || '#9aa4b2' }
466
+ function stBadgeCls(s) { return { pending: 'st-pending', claimed: 'st-claimed', in_progress: 'st-progress', done: 'st-done', completed: 'st-done', rework: 'st-rework', failed: 'st-failed', blocked: 'st-blocked', cancelled: 'st-cancelled' }[s] || '' }
467
+ /**
468
+ * 状态元数据(纯函数):节点状态带 / 图例 / 描边都从这里取,保证"同一状态一个口径"。
469
+ * 用户 2026-09-11 报障「状态给一个明显的展示,动态的」—— 此前节点上只有一个 4px 圆点,
470
+ * 状态文字只存在于顶部图例里,看图得靠颜色猜。
471
+ * @returns {key,label,color,tint,icon,active}
472
+ */
473
+ var DAG_STATUS_ORDER = ['pending', 'claimed', 'in_progress', 'done', 'rework', 'failed', 'blocked']
474
+ // color = 描边/色条/图例点;band = 底部状态带底色(浅色状态单独加深,白字才够对比度)
475
+ var DAG_STATUS_META = {
476
+ pending: { color: '#9aa4b2', band: '#6f7b87', tint: '#ffffff', icon: '●' },
477
+ claimed: { color: '#d9a13a', band: '#b5811f', tint: '#fffaf0', icon: '◆' },
478
+ in_progress: { color: '#3b6ef5', band: '#2f5fd8', tint: '#f4f8ff', icon: '▶' },
479
+ done: { color: '#22b07d', band: '#1c9268', tint: '#f2fbf7', icon: '✔' }, // completed 见下方归一
480
+ rework: { color: '#e05f45', band: '#c74a30', tint: '#fff8f1', icon: '↻' },
481
+ failed: { color: '#c0392b', band: '#a52c20', tint: '#fdf3f2', icon: '✕' },
482
+ blocked: { color: '#8a6d00', band: '#6f5700', tint: '#fffdf2', icon: '■' },
483
+ cancelled: { color: '#8a94a3', band: '#6d7885', tint: '#f7f8f9', icon: '⊘' },
484
+ }
485
+ function dagStatusMeta(status) {
486
+ // completed 是 done 的别名:归一,避免同一个状态出现"完成/已完成"两种标签
487
+ var raw = (status === 'completed') ? 'done' : (DAG_STATUS_META[status] ? String(status) : 'pending')
488
+ var key = raw
489
+ var m = DAG_STATUS_META[key]
490
+ return { key: key, label: stLabel(key), color: m.color, band: m.band || m.color, tint: m.tint, icon: m.icon, active: (key === 'in_progress' || key === 'claimed') }
491
+ }
492
+ /** 图例顺序(pending 在前、blocked 在后;claimed 与 in_progress 分开,因为"已领取"是真实中间态)。 */
493
+ function dagStatusOrder() { return DAG_STATUS_ORDER.slice() }
494
+ /** 各状态任务数(图例上的实时计数);completed 合并到 done。 */
495
+ function dagStatusCounts(tasks) {
496
+ var out = {}
497
+ ;(Array.isArray(tasks) ? tasks : []).forEach(function (t) {
498
+ var k = dagStatusMeta(t && t.status).key
499
+ if (k === 'completed') k = 'done'
500
+ out[k] = (out[k] || 0) + 1
501
+ })
502
+ return out
503
+ }
504
+ /**
505
+ * 状态**变更**检测(纯函数):返回"刚刚变化、值得闪一下"的任务 id。
506
+ * 为什么要它:面板每 2.5s 轮询,状态是**跳变**的;不闪的话用户盯着图也看不出刚才是谁
507
+ * 从"待开始"变成"进行中"、谁完成了(用户要的"动态的")。
508
+ * 首次见到某任务只登记、不闪烁(否则打开面板时满屏乱闪)。
509
+ * @param tasks - 当前任务数组
510
+ * @param now - 当前时刻(ms;显式传入便于测试)
511
+ * @param windowMs - 变更后保持闪烁的时长(默认 1500ms)
512
+ * @param prev - 上一次的 snapshot(不传则视为首次)
513
+ * @returns {flash:{id:true}, snapshot:{id:{status,at}}}
514
+ */
515
+ function dagStatusFlash(tasks, now, windowMs, prev) {
516
+ var t0 = Number(now) || 0
517
+ var win = Number(windowMs) > 0 ? Number(windowMs) : 1500
518
+ var before = (prev && typeof prev === 'object') ? prev : {}
519
+ var snap = {}, flash = {}
520
+ ;(Array.isArray(tasks) ? tasks : []).forEach(function (t) {
521
+ var id = String((t && t.id) || ''); if (!id) return
522
+ var st = String((t && t.status) || '')
523
+ var old = before[id]
524
+ if (old === undefined) { snap[id] = { status: st, at: 0 }; return }
525
+ if (old.status !== st) { snap[id] = { status: st, at: t0 }; flash[id] = true; return }
526
+ var at = Number(old.at) || 0
527
+ snap[id] = { status: st, at: at }
528
+ if (at && t0 - at <= win) flash[id] = true
529
+ })
530
+ return { flash: flash, snapshot: snap }
531
+ }
532
+ /** 本轮 dagSvg 渲染要闪烁的 id 与状态快照(由 dagSvg 内部刷新)。 */
533
+ var DAG_FLASH = {}
534
+ var DAG_STATUS_SNAP = {}
535
+
536
+ function stLabel(s) { var m = { pending: ['待开始', 'Pending'], claimed: ['已领取', 'Claimed'], in_progress: ['进行中', 'In progress'], done: ['已完成', 'Done'], rework: ['待返工', 'Rework'], blocked: ['受阻', 'Blocked'], failed: ['失败', 'Failed'], cancelled: ['取消', 'Cancelled'], completed: ['完成', 'Complete'] }[s]; return m ? (langNow === 'en' ? m[1] : m[0]) : s }
537
+ // run 级状态(STATE.json 的 status)——与任务状态分开,running/complete/…
538
+ var STATUS_ZH = { running: ['进行中', 'running'], complete: ['已完成', 'complete'], completed: ['已完成', 'completed'], done: ['已完成', 'done'], failed: ['失败', 'failed'], cancelled: ['已取消', 'cancelled'], canceled: ['已取消', 'canceled'], idle: ['空闲', 'idle'], paused: ['已暂停', 'paused'], blocked: ['受阻', 'blocked'], draft: ['草稿', 'draft'] }
539
+ function statusLabel(s) { if (s == null || s === '') return ''; var m = STATUS_ZH[s]; return m ? (langNow === 'en' ? m[1] : m[0]) : String(s) }
540
+ // 任务类型(TASKS.json 的 kind)——必须覆盖 host 的 KIND_ZH 全部键,否则面板会漏出英文 id
541
+ var KIND_ZH = { work: ['任务', 'work'], requirements: ['需求', 'requirements'], research: ['调研', 'research'], design: ['设计', 'design'], implementation: ['编码实现', 'implementation'], verification: ['验证测试', 'verification'], review: ['代码审查', 'review'], repair: ['返工修复', 'repair'], integration: ['集成', 'integration'], quality: ['质量自检', 'quality'], docs: ['文档', 'docs'], security: ['安全审计', 'security'], data: ['数据', 'data'], test: ['测试', 'test'] }
542
+ function kindLabel(k) { var m = KIND_ZH[k]; return m ? (langNow === 'en' ? m[1] : m[0]) : (k || t('任务', 'work')) }
543
+ // 裁决(verdict)
544
+ var VERDICT_ZH = { pass: ['通过', 'pass'], approved: ['已通过', 'approved'], needs_revision: ['需修订', 'needs-revision'], reject: ['驳回', 'reject'], rejected: ['驳回', 'rejected'], fail: ['未通过', 'fail'], failed: ['未通过', 'failed'], pending: ['待判定', 'pending'], none: ['—', '—'] }
545
+ function verdictLabel(v) { var m = VERDICT_ZH[v]; return m ? (langNow === 'en' ? m[1] : m[0]) : String(v == null ? '' : v) }
546
+ var ROLE_LABEL = { pm: ['产品', 'PM'], architect: ['架构', 'Arch'], researcher: ['调研员', 'Research'], ui: ['UI 设计师', 'UI Design'], backend: ['后端', 'Backend'], frontend: ['前端', 'Frontend'], dba: ['数据工程师', 'Data'], sec: ['安全审计员', 'Security'], reviewer: ['评审员', 'Review'], qa: ['测试员', 'QA'], devops: ['运维', 'DevOps'], docs: ['文档工程师', 'Docs'] }
547
+ function roleLabel(r) { var m = ROLE_LABEL[r]; return m ? (langNow === 'en' ? m[1] : m[0]) : r }
548
+ /**
549
+ * 文本 → 角色(纯函数,便于单测)。镜像 host 侧 SUB_ROLE_WORDS 的关键词序:
550
+ * 动态角色在前(「产品分析」与「产品经理」会重叠)。
551
+ *
552
+ * 为什么要它:workflow 派生的子代理在 host 侧常解析不出角色(descriptor label 为空、
553
+ * 子会话日志经 seam 读不到),于是面板如实把它们列为「未匹配到角色」(用户实测看到 22 个)。
554
+ * 但对话流 workflow 卡上每个成员是有 label 的(publishWfPhases 收了 m.label),
555
+ * 那里通常带着角色线索 —— 这就是补解析的来源。
556
+ */
557
+ var WF_ROLE_WORDS = [
558
+ ['competitive-analyst', ['competitive', '竞品', '对标']],
559
+ ['product-analyst', ['product-analyst', '产品分析', '收益成本', '差距分析']],
560
+ ['pm', ['pm', '产品经理', '澄清', '起草', 'ultra spec', '需求', '规格']],
561
+ ['architect', ['architect', '架构', 'chief', '接口契约']],
562
+ ['researcher', ['research', '调研', '研究员', 'researcher', 'investigate', '现状']],
563
+ ['backend', ['backend', '后端']],
564
+ ['dba', ['dba', '数据', 'sql', 'ddl', '迁移', 'database', '表结构']],
565
+ ['frontend', ['frontend', '前端', 'uni-app', 'uniapp']],
566
+ ['ui', ['ui', '视觉', '界面', '走查', '设计', 'designer', 'design']],
567
+ ['reviewer', ['review', '评审', '审查']],
568
+ // '重测'/'test': workflow legs are labelled 「[重测] test-2 …」「[终局重测] test-3 …」;
569
+ // neither contains 测试, so without these every verification leg stayed role-less.
570
+ // Keep in sync with SUB_ROLE_WORDS in lib/command.js.
571
+ ['qa', ['qa', '测试', '重测', 'test', '验证']],
572
+ ['sec', ['sec', '安全', '越权', '注入', '渗透', 'security']],
573
+ ['devops', ['devops', '部署', '构建', 'cicd', '环境', '运维', '发布']],
574
+ ['docs', ['docs', '文档', 'readme', '手册']],
575
+ ]
576
+ /**
577
+ * 规范中文角色标签 → 角色 id(2026-09-11 起 prompt 与派工 label 用中文标签)。
578
+ * **必须与 host 侧 `lib/command.js` 的 `ROLE_LABELS_ZH` 保持同步**:两边解析同一批
579
+ * `【产品经理】…` / `[测试员] 任务` 文本,任一侧漏掉就会让浮层把成员列成「未匹配到角色」。
580
+ * 取值 = 浮层显示名(本文件 `ROLE_TOOL` 的 nm),避免同一角色两套叫法。
581
+ */
582
+ var ROLE_LABELS_ZH = { '产品经理': 'pm', '架构师': 'architect', '研究员': 'researcher', 'ui设计师': 'ui', '后端工程师': 'backend', '前端工程师': 'frontend', '数据工程师': 'dba', '安全审计员': 'sec', '审查官': 'reviewer', '测试员': 'qa', '运维': 'devops', '文档工程师': 'docs', '竞品分析师': 'competitive-analyst', '产品分析员': 'product-analyst', 'UI 验证专家': 'ui-verifier', '故障诊断工程师': 'debugger' }
583
+ /** 标签归一:去空白(含全角空格)与大小写 ⇒ `UI 设计师` / `ui设计师` 等价。 */
584
+ function roleLabelKey(s) { return String(s == null ? '' : s).replace(/[\s\u3000]+/g, '').toLowerCase() }
585
+ /**
586
+ * 档位中文标签(G 线档位徽标用)。
587
+ * **必须与 host 侧 `lib/tier.js` 的 `TIER_LABELS_ZH` 逐字一致** —— client 是
588
+ * `__ModuleLoader__` 工厂、不能 import,所以又是一份"搬不动的副本";
589
+ * `tier-badge.test.mjs` 会拿两侧的值逐字比对(缺一项就红)。
590
+ */
591
+ var TIER_LABELS_ZH = { quick: '快速档', standard: '标准档', strict: '严格档' }
592
+ /**
593
+ * 档位徽标:认得的档位返回 `🎚快速档`,**认不出/缺省一律返回空串**。
594
+ * 为什么认不出要空而不是退回原值:把 `涡轮档`、`undefined`、`1` 直接画到面板上
595
+ * 比"不显示"更糟 —— 那是把脏数据当事实展示(与 host 侧 `normalizeTier` 拒绝非法值同一纪律)。
596
+ */
597
+ function tierBadge(tier) {
598
+ var key = String(tier == null ? '' : tier).trim().toLowerCase()
599
+ return Object.prototype.hasOwnProperty.call(TIER_LABELS_ZH, key) ? ('🎚' + TIER_LABELS_ZH[key]) : ''
600
+ }
601
+ function roleFromText(text) {
602
+ var hay = String(text || '')
603
+ if (!hay) return ''
604
+ var low = hay.toLowerCase()
605
+ // ① 英文 id 写在括号/【】里(历史写法):`(researcher,一次性)` / `【researcher】…`
606
+ var m = low.match(/[((【[]\s*([a-z][a-z0-9-]{1,24})\s*[,,、))】\]]/)
607
+ if (m && m[1]) return m[1]
608
+ // ② 中文标签(2026-09-11 起主用写法):`【产品经理】…`。**只有精确命中标签表才算角色**
609
+ // —— 自由散文里的括号(「(非实验性)」)仍然被拒绝。
610
+ var zh = hay.match(/[((【[]\s*([^,,、()()【】\[\]]{1,32}?)\s*[,,、))】\]]/)
611
+ if (zh) { var hit = ROLE_LABELS_ZH[roleLabelKey(zh[1])]; if (hit) return hit }
612
+ // ③ 关键词兜底(表序先命中先赢;动态角色在前)
613
+ for (var i = 0; i < WF_ROLE_WORDS.length; i++) {
614
+ var role = WF_ROLE_WORDS[i][0], words = WF_ROLE_WORDS[i][1]
615
+ for (var j = 0; j < words.length; j++) if (low.indexOf(words[j]) >= 0) return role
616
+ }
617
+ return ''
618
+ }
619
+
620
+ /**
621
+ * 活子代理 → 角色 的三源合并(纯函数,便于单测):
622
+ * ① members[role].id —— host enrichMembers 的权威绑定(lead 回写 STATE.members 时最准)
623
+ * ② agents[].role —— host 按 label / 括号角色 id / 首条 prompt 三级推断的结果
624
+ * ③ workflow 卡成员 label —— ② 拿不到时的兜底(卡上有 label 而子代理 descriptor 没有)
625
+ * @returns {byRole, byId, matched, unmatched, dispatched, wfRole}
626
+ */
627
+ function resolveAgentRoles(members, roles, agentsLive, wfPhases) {
628
+ // 加固:本函数在**面板渲染路径**上,任何字段形状不符都**不得抛错**(否则整个浮层白屏)。
629
+ // 全部入口先做 Array.isArray / typeof 归一化。
630
+ var agentList = Array.isArray(agentsLive) ? agentsLive : []
631
+ var roleList = Array.isArray(roles) ? roles : []
632
+ var memMap = (members && typeof members === 'object' && !Array.isArray(members)) ? members : {}
633
+ var phaseList = (wfPhases && Array.isArray(wfPhases.phases)) ? wfPhases.phases : []
634
+ var byId = {}
635
+ agentList.forEach(function (a) { if (a && a.id) byId[String(a.id)] = a })
636
+ var wfRole = {}
637
+ phaseList.forEach(function (p) {
638
+ var mem = (p && Array.isArray(p.members)) ? p.members : []
639
+ mem.forEach(function (mm) {
640
+ var cid = mm && mm.childId
641
+ if (!cid) return
642
+ var r = roleFromText(mm.label)
643
+ if (r && !wfRole[String(cid)]) wfRole[String(cid)] = r
644
+ })
645
+ })
646
+ var byRole = {}, matched = {}, dispatched = 0
647
+ roleList.forEach(function (r) {
648
+ var mm2 = memMap[r]
649
+ if (mm2 && typeof mm2 === 'object' && mm2.id) { byRole[r] = String(mm2.id); matched[String(mm2.id)] = 1; dispatched += 1; return }
650
+ // 排除**已被认领**的 agent,避免同一 agent 被两个角色同时选中
651
+ var a2 = agentList.filter(function (x) { return x && x.role && String(x.role) === String(r) && !matched[String(x.id)] })[0]
652
+ if (a2) { byRole[r] = String(a2.id); matched[String(a2.id)] = 1; dispatched += 1; return }
653
+ var cid2 = Object.keys(wfRole).filter(function (c) { return wfRole[c] === String(r) && !matched[c] && byId[c] })[0]
654
+ if (cid2) { byRole[r] = cid2; matched[cid2] = 1; dispatched += 1 }
655
+ })
656
+ var unmatched = agentList.filter(function (a) { return a && a.id && !matched[String(a.id)] })
657
+ // 「没被匹配」≠「解析不出角色」:一个角色在面板上只展示 1 个成员,而同一角色常被派工
658
+ // 多次(实测本会话 30 个活子代理 / 14 个角色 ⇒ 必然有一大批"多余的同角色 leg")。
659
+ // 把它们说成"未能解析出角色"是错的,分开统计。
660
+ var roleless = unmatched.filter(function (a) { return !(a && a.role) })
661
+ var extraByRole = {}
662
+ unmatched.forEach(function (a) { if (a && a.role) extraByRole[a.role] = (extraByRole[a.role] || 0) + 1 })
663
+ return { byRole: byRole, byId: byId, matched: matched, unmatched: unmatched, roleless: roleless, extraByRole: extraByRole, dispatched: dispatched, wfRole: wfRole }
664
+ }
665
+
666
+ // ── 对话流角色化工具卡:为每个 subagent_<role> 工具注册 tool.call.toolview ──
667
+ // 用户在主对话流直接看到「派 XX 调研」的编排动作(运行中/完成状态卡片)。
668
+ var ROLE_TOOL = {
669
+ subagent_pm: ['👨‍💼', '产品经理', '需求澄清 · Ultra Spec'],
670
+ subagent_architect: ['🏗️', '架构师', '接口契约 · 技术选型'],
671
+ subagent_researcher: ['🔎', '研究员', '项目调研 · 竞品分析'],
672
+ subagent_ui: ['🎨', 'UI 设计师', '设计规范 · 交互稿'],
673
+ subagent_backend: ['🖥️', '后端工程师', '后端实现'],
674
+ subagent_frontend: ['💻', '前端工程师', '前端实现'],
675
+ subagent_dba: ['🗄️', '数据工程师', '数据契约 · 迁移'],
676
+ subagent_sec: ['🛡️', '安全审计员', '权限 · 越权 · 注入'],
677
+ subagent_reviewer: ['🕵️', '审查官', '代码审查 · 验收质量'],
678
+ subagent_qa: ['🧪', '测试员', '用例 · 回归验证'],
679
+ subagent_devops: ['🚀', '运维', '构建 · 部署 · CI'],
680
+ subagent_docs: ['📖', '文档工程师', '手册 · README · API']
681
+ }
682
+ /**
683
+ * 角色卡状态判定(纯函数;`tool-card-status.test.mjs` 抽源码直测 + 变异验证)。
684
+ *
685
+ * 为什么单独抽出来:`settled`(工具调用已返回)**不等于**任务完成。可继续委派
686
+ * (`backgroundMode: continuable`)下 `subagent_<role>` 几乎立刻返回
687
+ * `started subagent <id>`(dsh-tool-subagent 实测文本),而子代理随后仍在跑 ——
688
+ * 旧实现拿 `settled` 当完成标记,于是**正在运行的任务卡显示 ✅完成**
689
+ * (用户 2026-09-11 报障:卡上 ✅完成,同一角色在子代理列表里 3分39秒 · 正在运行)。
690
+ *
691
+ * 判据:① 工具调用在飞(有 `callId` 且无 `kind`)⇒ 进行中;② 调用已返回、结果里带
692
+ * 子代理 id、且该 id 在 `/state` 的 `data.agents` 里仍 `activity === 'running'` ⇒ 进行中。
693
+ * 拿不到 id 或 id 已不在活子代理里 ⇒ 按调用结果结算(✅完成 / ❌失败),与旧行为一致。
694
+ *
695
+ * @param block - 工具调用块(`{kind, callId, isError}`)
696
+ * @param out - 结算文本(可继续委派为 `started subagent <id>`)
697
+ * @param liveAgents - `/state` 的 `data.agents`(`[{id, activity, ...}]`),可为 null
698
+ * @returns `{settled, running, isErr, isRunning, childId, liveAgent, statusText}`
699
+ */
700
+ function roleCardState(block, out, liveAgents) {
701
+ var b = block || {}
702
+ var settled = b.kind === 'tool-result'
703
+ var inFlight = !!b.callId && !b.kind
704
+ var isErr = settled && !!b.isError
705
+ var text = String(out == null ? '' : out)
706
+ var childId = ''
707
+ if (settled) {
708
+ var m = /started (?:background )?subagent (?:job )?([0-9a-z-]{8,})/i.exec(text)
709
+ if (m) childId = m[1]
710
+ }
711
+ var liveAgent = null
712
+ if (childId && Array.isArray(liveAgents)) {
713
+ for (var i = 0; i < liveAgents.length; i++) {
714
+ var a = liveAgents[i]
715
+ if (a && String(a.id) === childId) { liveAgent = a; break }
716
+ }
717
+ }
718
+ var liveRunning = !!liveAgent && String(liveAgent.activity || '') === 'running'
719
+ var isRunning = inFlight || liveRunning
720
+ return {
721
+ settled: settled, running: inFlight, isErr: isErr, isRunning: isRunning,
722
+ childId: childId, liveAgent: liveAgent,
723
+ statusText: isRunning ? '⏳ 进行中' : (isErr ? '❌ 失败' : '✅ 完成')
724
+ }
725
+ }
726
+
727
+ function RoleToolView(props) {
728
+ var meta = ROLE_TOOL[(props && props.toolName) || ''] || [null, (props && props.toolName) || '成员', '']
729
+ var em = meta[0], nm = meta[1], duty = meta[2]
730
+ // 活子代理状态来自共享轮询(HeaderButton/LiveCapsule 同一份,单例;无订阅者即停)。
731
+ // 必须有会话 id 才认这份数据:否则宁可按旧口径结算,也不拿别的会话的状态下判断。
732
+ var sid = useCurrentSession()
733
+ var live = useLiveState(sid)
734
+ var block = (props && props.block) || {}
735
+ var argsRaw = block.argsRaw || (block.call && block.call.argsRaw) || ''
736
+ var label = '', prompt = ''
737
+ try {
738
+ var a = JSON.parse(argsRaw)
739
+ if (a && typeof a === 'object') { label = a.label || ''; prompt = String(a.prompt || a.task || a.message || '') }
740
+ else if (a) prompt = String(a)
741
+ } catch (e) { prompt = String(argsRaw).slice(0, 160) }
742
+ var out = ''
743
+ if (block.kind === 'tool-result' && Array.isArray(block.content)) {
744
+ out = block.content.map(function (c) { var t2 = (c && typeof c === 'object') ? (c.text || c.content || '') : String(c || ''); return t2 }).join(' ').replace(/\s+/g, ' ').trim()
745
+ }
746
+ // 状态判定:工具调用「已返回」≠「任务完成」(可继续委派下调用秒返、子代理仍在跑)。
747
+ var st = roleCardState(block, out, (sid && live && live.agents) ? live.agents : null)
748
+ // 子代理 id 一出现就立刻刷一次共享轮询:避免最长 2.5s 的「先 ✅完成、后跳 ⏳进行中」抖动。
749
+ useEffect(function () { if (st.childId) { try { liveTick() } catch (e) {} } }, [st.childId])
750
+ // 点卡片本体 → 不跳页面、不切视图:把该角色的任务详情渲染到**右侧面板**。
751
+ var roleId = String((props && props.toolName) || '').replace(/^subagent[_-]?/, '')
752
+ function focusRight(e) {
753
+ try {
754
+ if (e && e.stopPropagation) e.stopPropagation()
755
+ requestFocus({ kind: 'role', role: roleId })
756
+ focusPanelRight()
757
+ } catch (err) {}
758
+ }
759
+ return h('div', { className: 'et-tc' + (st.isRunning ? ' run' : '') + (st.isErr ? ' err' : ''), onClick: focusRight, title: t('点击在右侧面板查看该角色任务详情', 'Click to open this role\u2019s task detail in the right panel') },
760
+ h('div', { className: 'et-tc-h' },
761
+ h('span', { className: 'et-tc-ava' }, em || '🤖'),
762
+ h('span', { className: 'et-tc-nm' }, esc(nm)),
763
+ h('span', { className: 'et-tc-duty' }, esc(duty)),
764
+ h('span', { className: 'et-tc-st' + (st.isRunning ? '' : (st.isErr ? ' e' : ' done')) }, esc(st.statusText))),
765
+ // 名称(label)**不截断**:它是这张卡「谁在干什么」的身份,截了用户就认不出来
766
+ // (用户 2026-09-11 报障)。只有 prompt 预览按显示宽度截断,且截断处**补省略号**
767
+ // (clipText:CJK 记 2 单位)—— 旧实现 `String(prompt).slice(0, 90)` 是硬切且无省略号,
768
+ // 句子断在半截,看起来正是「名称没有显示完」。
769
+ (label || prompt) ? h('div', { className: 'et-tc-t' }, esc(label ? String(label) + (prompt ? ' — ' + clipText(prompt, 120) : '') : clipText(prompt, 200))) : null,
770
+ st.settled ? h('div', { className: 'et-tc-f' }, esc(
771
+ st.isRunning && st.childId ? t('⏳ 子代理运行中 · ', '⏳ subagent running · ') + st.childId + t('(点「调用详情」看实时状态)', ' (open call detail for live status)')
772
+ : st.childId ? t('✅ 子代理已结算 · ', '✅ subagent settled · ') + st.childId
773
+ : clipText(out || t('· 已完成(展开调用查看结果)', '· done (expand to view)'), 200))) : null,
774
+ h('div', { className: 'et-tc-b' },
775
+ h('button', { onClick: function (e) { try { if (e && e.stopPropagation) e.stopPropagation(); if (!open) { dockState = true; saveLS('et-dock', true); notify(); setOpen() } } catch (err) {} } }, esc('🧑‍🔬 团队浮层')),
776
+ props && props.inspect ? h('button', { onClick: function (e) { try { if (e && e.stopPropagation) e.stopPropagation(); props.inspect() } catch (err) {} } }, esc('🔍 调用详情')) : null))
777
+ }
778
+
779
+ // ── 对话流 workflow 运行卡(接管 dsh 原生 `conversation.chat.node` / key=workflow-run)──
780
+ // 为什么接管:dsh 原生卡把成员行硬接成 `ctx.sessions.open(childId)`(打开新标签页),
781
+ // 且它的 `openSession` 来自该条目自己的 inject —— 外部无法只替换点击行为;
782
+ // renderer 的规则是「每个 slot cell 取 order 最小的存活条目」,所以本插件用 order:-1
783
+ // 成为该 cell 的赢家,渲染同构的卡但把点击改为「保留当前对话页 + 右侧面板看实时细节」。
784
+ // 可逆:localStorage['et-native-workflow']='1' 时不注册(回退原生卡,刷新生效)。
785
+ var WF_STATUS_ZH = { running: ['运行中', 'running'], completed: ['已完成', 'completed'], failed: ['失败', 'failed'], cancelled: ['已取消', 'cancelled'], interrupted: ['已中断', 'interrupted'], pending: ['等待', 'pending'], queued: ['排队', 'queued'], paused: ['已暂停', 'paused'] }
786
+ function wfStatusLabel(s) { var m = WF_STATUS_ZH[s]; return m ? (langNow === 'en' ? m[1] : m[0]) : String(s || '') }
787
+ function wfDotColor(s) { return s === 'running' ? '#0969da' : s === 'completed' ? '#1a7f37' : (s === 'failed' || s === 'interrupted') ? '#cf222e' : '#9aa4b2' }
788
+ function wfPhases(node) {
789
+ var out = []
790
+ var phases = (node && node.data && Array.isArray(node.data.phases)) ? node.data.phases : []
791
+ for (var i = 0; i < phases.length; i++) {
792
+ var ph = phases[i] || {}
793
+ out.push({ key: String(ph.key || ph.name || ('#' + (i + 1))), members: Array.isArray(ph.members) ? ph.members : [] })
794
+ }
795
+ return out
796
+ }
797
+ function WorkflowRunCard(props) {
798
+ useLang()
799
+ var node = (props && props.node) || {}
800
+ var data = (node && node.data) || {}
801
+ var phases = wfPhases(node)
802
+ var name = String(data.name || node.name || t('工作流运行', 'Workflow run'))
803
+ var status = String(data.status || '')
804
+ var total = 0, runningN = 0
805
+ phases.forEach(function (p) { p.members.forEach(function (m) { total += 1; if (m && m.status === 'running') runningN += 1 }) })
806
+ var oS = useState(true); var open = oS[0], setOpen = oS[1]
807
+ // 把阶段结构发给面板(effect 内发布,避免渲染期 notify)
808
+ useEffect(function () { publishWfPhases(name, phases) }, [node])
809
+ function focusMember(m) { try { requestFocus({ kind: 'agent', agentId: String(m.childId || ''), label: String(m.label || '') }); focusPanelRight() } catch (e) {} }
810
+ var rows = []
811
+ phases.forEach(function (p) {
812
+ rows.push(h('div', { key: 'ph-' + p.key, style: { display: 'flex', alignItems: 'center', gap: 6, margin: '6px 0 2px', fontSize: 11.5, color: 'var(--dsw-alias-label-secondary,#57606a)' } },
813
+ h('span', null, '▸ ' + esc(p.key)), h('span', null, '· ' + p.members.length + t(' 个成员', ' members'))))
814
+ p.members.forEach(function (m, i) {
815
+ var lab = String((m && m.label) || '').trim() || (String((m && m.childId) || '').slice(0, 8) || t('(未命名)', '(unnamed)'))
816
+ rows.push(h('div', {
817
+ key: 'm-' + p.key + '-' + i,
818
+ onClick: function (e) { if (e && e.stopPropagation) e.stopPropagation(); focusMember(m) },
819
+ title: t('点击在右侧面板查看该成员实时细节(不跳转)', 'Click to inspect this member in the right panel (no navigation)'),
820
+ style: { display: 'flex', alignItems: 'center', gap: 8, padding: '5px 6px', borderTop: '1px dashed var(--dsw-alias-border-l1,#eef1f5)', cursor: 'pointer', fontSize: 12.5 }
821
+ },
822
+ h('span', { style: { width: 7, height: 7, borderRadius: '50%', background: wfDotColor(m && m.status), flex: 'none' } }),
823
+ h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, esc(lab)),
824
+ h('span', { style: { flex: 'none', color: 'var(--dsw-alias-label-secondary,#57606a)', fontSize: 11.5 } }, esc(wfStatusLabel(m && m.status)))))
825
+ })
826
+ })
827
+ return h('div', { style: { margin: '6px 0', border: '1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de))', borderRadius: 10, background: 'var(--dsw-alias-bg-layer-1,var(--bg,#fff))', fontSize: 12.5, color: 'var(--dsw-alias-label-primary,#1f2328)', overflow: 'hidden' } },
828
+ h('div', { onClick: function () { setOpen(!open) }, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', cursor: 'pointer' } },
829
+ h('span', null, open ? '▾' : '▸'),
830
+ h('b', { style: { fontWeight: 600 } }, esc(name)),
831
+ h('span', { style: { color: 'var(--dsw-alias-label-secondary,#57606a)' } }, '· ' + total + t(' 个成员', ' members')),
832
+ h('span', { style: { marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 5, color: 'var(--dsw-alias-label-secondary,#57606a)' } },
833
+ h('span', { style: { width: 7, height: 7, borderRadius: '50%', background: wfDotColor(status) } }),
834
+ esc(wfStatusLabel(status) + (runningN ? ' · ' + runningN + t(' 进行中', ' running') : '')))),
835
+ open ? h('div', { style: { padding: '0 10px 8px' } },
836
+ rows.length ? rows : h('div', { style: { color: 'var(--dsw-alias-label-secondary,#57606a)', padding: '4px 0' } }, t('(暂无成员)', '(no members)')),
837
+ h('div', { style: { marginTop: 6, fontSize: 11, color: 'var(--dsw-alias-label-secondary,#57606a)' } }, t('点击成员 → 右侧面板显示其实时细节(不跳页)', 'Click a member to inspect it in the right panel (no navigation)'))) : null)
838
+ }
839
+
840
+ // Normalize old/mixed-format task fields: verify/changedPaths/dependsOn/
841
+ // findings may be a STRING in legacy runs — always work with arrays.
842
+ function arrOf(v) { return Array.isArray(v) ? v : (v == null ? [] : [v]) }
843
+ function strOf(v) { return v == null ? '' : String(v) }
844
+
845
+ function verdictBadge(v) {
846
+ if (!v || v === 'pending' || v === 'none') return null
847
+ var cls = v === 'pass' ? 'v-pass' : (v === 'needs_revision' || v === 'reject' ? 'v-fail' : 'v-rev')
848
+ var label = verdictLabel(v)
849
+ return h('span', { className: 'exp-badge ' + cls }, esc(label))
850
+ }
851
+
852
+ // ── interactive SVG task DAG ──
853
+ var NODE_W = 168, NODE_H = 64, HGAP = 46, VGAP = 10
854
+
855
+ /**
856
+ * SVG 没有 CSS ellipsis:按显示宽度近似截断(CJK 记 2 个单位、其余记 1)。
857
+ * 用于把「任务标题」放进卡片可见行 —— 超宽就省略,绝不撑破节点。
858
+ */
859
+ function clipText(v, maxUnits) {
860
+ var str = String(v == null ? '' : v)
861
+ var used = 0, out = ''
862
+ for (var i = 0; i < str.length; i++) {
863
+ var ch = str.charAt(i)
864
+ var w = /[\u2e80-\u9fff\uff00-\uffef]/.test(ch) ? 2 : 1
865
+ if (used + w > maxUnits) return (out || str.charAt(0)) + '…'
866
+ used += w; out += ch
867
+ }
868
+ return out
869
+ }
870
+
871
+ /**
872
+ * 任务绑定推断(纯函数,便于单测与变异验证)。
873
+ *
874
+ * 背景:**真实 TASKS.json 不记录 agentId**,而早期实现只按 agentId 精确匹配
875
+ * ⇒ 面板里**每一个人都显示「未绑定任务」**(用户实测报障)。
876
+ *
877
+ * 两段式:
878
+ * ① 权威:task.agentId === person.id(tasksLive 投影 / lead 回写时存在)
879
+ * ② 推断:task.owner === person.role + **状态亲和**(在跑→in_progress/claimed;
880
+ * 已结束→completed/done),再按 round 取最近一轮。
881
+ * 推断命中的 id 写入 inferredOut,UI 据此用「~」前缀**明确标注是推断**,不冒充精确。
882
+ * @returns 绑定的任务;无候选时 null(此时 UI 才显示「未绑定任务」)
883
+ */
884
+ function bindTaskFor(person, tasks, inferredOut) {
885
+ var p = person || {}
886
+ var list = Array.isArray(tasks) ? tasks : []
887
+ var hit = null
888
+ list.forEach(function (tk) { if (!hit && tk && tk.agentId && String(tk.agentId) === String(p.id)) hit = tk })
889
+ if (hit) return hit
890
+ if (!p.role) return null
891
+ var mine = list.filter(function (tk) { return tk && String(tk.owner || '') === String(p.role) })
892
+ if (!mine.length) return null
893
+ var running = p.activity === 'running'
894
+ var want = running ? ['in_progress', 'claimed'] : ['completed', 'done']
895
+ var pref = mine.filter(function (tk) { return want.indexOf(String(tk.status || '')) >= 0 })
896
+ var pool = (pref.length ? pref : mine).slice().sort(function (a, b) { return (Number(b.round) || 1) - (Number(a.round) || 1) })
897
+ var pick = pool[0]
898
+ if (pick && inferredOut) inferredOut[String(pick.id)] = 1
899
+ return pick
900
+ }
901
+
902
+ function dagLayout(tasks) {
903
+ var byId = {}; tasks.forEach(function (t) { byId[t.id] = t })
904
+ var layer = {}
905
+ function compute(id) { if (id in layer) return layer[id]; var t = byId[id]; if (!t) return 0; var deps = arrOf(t.dependsOn).filter(function (d) { return byId[d] }); layer[id] = deps.length ? 1 + Math.max.apply(null, deps.map(compute)) : 0; return layer[id] }
906
+ tasks.forEach(function (t) { compute(t.id) })
907
+ var cols = []; tasks.forEach(function (t) { var L = layer[t.id] || 0; (cols[L] = cols[L] || []).push(t) })
908
+ var pos = {}; var width = 0, height = 0
909
+ cols.forEach(function (col, li) { col.forEach(function (t, i) { var x = li * (NODE_W + HGAP); var y = i * (NODE_H + VGAP); pos[t.id] = { x: x, y: y, L: li }; width = Math.max(width, x + NODE_W); height = Math.max(height, y + NODE_H) }) })
910
+ return { byId: byId, pos: pos, width: width || NODE_W, height: height || NODE_H }
911
+ }
912
+ var DAG_RENDER_SEQ = 0
913
+ function dagSvg(tasks, onPick, selId, hoverId, onHover) {
914
+ if (!tasks.length) return h('div', { className: 'exp-empty' }, t('(暂无任务)', '(no tasks)'))
915
+ var L = dagLayout(tasks)
916
+ var childOf = {}, parentOf = {}
917
+ tasks.forEach(function (t) { arrOf(t.dependsOn).forEach(function (d) { if (L.pos[d]) { (parentOf[t.id] = parentOf[t.id] || []).push(d); (childOf[d] = childOf[d] || []).push(t.id) } }) })
918
+ // highlight set = hoverId + upstream + downstream
919
+ var hot = {}
920
+ if (hoverId) {
921
+ hot[hoverId] = 1
922
+ ;(function walk(seed, next) { var q = [seed]; while (q.length) { var id = q.shift(); (next[id] || []).forEach(function (n) { if (!hot[n]) { hot[n] = 1; q.push(n) } }) } })(hoverId, parentOf)
923
+ ;(function walk(seed, next) { var q = [seed]; while (q.length) { var id = q.shift(); (next[id] || []).forEach(function (n) { if (!hot[n]) { hot[n] = 1; q.push(n) } }) } })(hoverId, childOf)
924
+ }
925
+ var edges = []
926
+ tasks.forEach(function (t) { arrOf(t.dependsOn).forEach(function (d) { if (L.pos[d]) edges.push([d, t.id]) }) })
927
+ var edgeEls = edges.map(function (e, i) {
928
+ var a = L.pos[e[0]], b = L.pos[e[1]]; var x1 = a.x + NODE_W, y1 = a.y + NODE_H / 2, x2 = b.x, y2 = b.y + NODE_H / 2; var mx = (x1 + x2) / 2
929
+ var on = hoverId && hot[e[0]] && hot[e[1]]
930
+ var run = (L.byId[e[0]] && (L.byId[e[0]].status === 'in_progress' || L.byId[e[0]].status === 'claimed')) || (L.byId[e[1]] && (L.byId[e[1]].status === 'in_progress' || L.byId[e[1]].status === 'claimed'))
931
+ return h('path', { key: i, className: on ? 'exp-edge-run' : (run ? 'exp-edge-run' : ''), d: 'M' + x1 + ' ' + y1 + ' C ' + mx + ' ' + y1 + ', ' + mx + ' ' + y2 + ', ' + x2 + ' ' + y2, stroke: on ? '#0969da' : '#8aa8e8', strokeWidth: on ? 2 : 1.4, opacity: (hoverId && !on) ? 0.35 : 1, fill: 'none' })
932
+ })
933
+ // 状态"变更即闪":面板每 2.5s 轮询,状态是跳变的,闪烁让用户看见"刚刚谁变了"。
934
+ // 首次见到只登记不闪(打开面板时不该满屏乱闪)。
935
+ var fl = dagStatusFlash(tasks, Date.now(), 1500, DAG_STATUS_SNAP)
936
+ DAG_FLASH = fl.flash; DAG_STATUS_SNAP = fl.snapshot
937
+ var dagSeq = (DAG_RENDER_SEQ += 1)
938
+ var BAR_H = 15
939
+ var nodeEls = tasks.map(function (t, ti) {
940
+ var p = L.pos[t.id]; if (!p) return null
941
+ var meta = dagStatusMeta(t.status)
942
+ var col = meta.color
943
+ var sel = selId === t.id
944
+ var hl = hoverId && hot[t.id]
945
+ var dim = hoverId && !hl
946
+ var run = meta.active
947
+ var flash = !!DAG_FLASH[t.id]
948
+ var clipId = 'dagclip-' + dagSeq + '-' + ti
949
+ var title = String(t.title || t.spec || (t.acceptance && t.acceptance[0]) || '(无标题)')
950
+ var metaLine = kindLabel(t.kind || 'work') + (t.owner ? ' · ' + roleLabel(t.owner) : '') + (t.verdict ? ' · ' + verdictLabel(t.verdict) : '')
951
+ var roundTag = (Number(t.round) > 1 || Number(t.attempt) > 1) ? ('r' + (Number(t.round) || 1) + '·a' + (Number(t.attempt) || 1)) : ''
952
+ return h('g', { key: t.id, className: 'exp-node', onClick: function () { onPick(t) }, onMouseEnter: function () { onHover && onHover(t.id) }, onMouseLeave: function () { onHover && onHover(null) }, transform: 'translate(' + p.x + ' ' + p.y + ')', opacity: dim ? 0.4 : 1 },
953
+ h('defs', null, h('clipPath', { id: clipId }, h('rect', { width: NODE_W, height: NODE_H, rx: 9 }))),
954
+ // 卡片主体(裁进圆角):状态色柔底 + 左侧状态色条 + 底部**状态带**(明显的状态展示)
955
+ h('g', { 'clip-path': 'url(#' + clipId + ')' },
956
+ h('rect', { width: NODE_W, height: NODE_H, fill: sel ? '#eef4ff' : (hl ? '#f2f7ff' : meta.tint) }),
957
+ h('rect', { x: 0, y: 0, width: 4, height: NODE_H, fill: col }),
958
+ h('rect', { className: 'exp-node-bar' + (run ? ' exp-node-bar-run' : ''), x: 4, y: NODE_H - BAR_H, width: NODE_W - 4, height: BAR_H, fill: meta.band })),
959
+ // 描边:状态色;选中=业务蓝;运行中脉冲;刚变更过闪一下
960
+ h('rect', { className: 'exp-node-bg' + (run ? ' exp-node-run' : '') + (flash ? ' exp-node-flash' : ''), width: NODE_W, height: NODE_H, rx: 9, fill: 'none', stroke: sel ? '#0969da' : col, strokeWidth: sel || hl ? 2 : 1.6 }),
961
+ h('text', { x: 12, y: 15, 'font-size': 11, 'font-weight': 700, 'fill': '#1f2328' }, esc(clipText(String(t.id || ''), 20))),
962
+ // 「具体干了什么」:任务标题提到**可见行**(此前只藏在 <title> tooltip 里,卡片上看不到内容)
963
+ h('text', { x: 12, y: 31, 'font-size': 11, 'font-weight': 600, 'fill': '#1f2328' }, esc(clipText(title, 24))),
964
+ h('text', { x: 12, y: 45, 'font-size': 9.5, 'fill': '#57606a' }, esc(clipText(metaLine, 30))),
965
+ // 底部状态带文字:图标 + 中文状态(白字打在状态色上,一眼可见)
966
+ h('text', { className: 'exp-node-status', x: 12, y: NODE_H - 4.5, 'font-size': 10, 'font-weight': 700, fill: '#ffffff' }, esc(meta.icon + ' ' + meta.label)),
967
+ roundTag ? h('text', { x: NODE_W - 8, y: NODE_H - 4.5, 'font-size': 9, 'text-anchor': 'end', fill: 'rgba(255,255,255,.88)' }, esc(roundTag)) : null,
968
+ h('title', null, esc(String(t.id || '') + ' · ' + title + ' · ' + meta.label + '(' + metaLine + ')')))
969
+ })
970
+ return h('div', { className: 'exp-dag' },
971
+ h('svg', { width: L.width + 6, height: L.height + 6, style: { display: 'block' } },
972
+ h('g', null, edgeEls), h('g', null, nodeEls)))
973
+ }
974
+
975
+ function TaskDetail(props) {
976
+ try {
977
+ var tk = props.task || {}
978
+ var mem = props.member
979
+ var nm = mem ? (mem.name || tk.owner) : tk.owner
980
+ var col = mem ? (mem.color || '#5b8def') : '#9aa4b2'
981
+ var ini = mem ? (mem.initial || nm.slice(0, 1).toUpperCase()) : nm.slice(0, 1).toUpperCase()
982
+ var act = mem ? (mem.activity || '') : ''
983
+ var st = act === 'running' ? t('运行中', 'running') : (act === 'idle' || act === 'ready' || act === 'inactive') ? t('空闲', 'idle') : ((mem && mem.active) ? t('已派', 'dispatched') : t('未启动', 'not started'))
984
+ var model = mem ? (mem.model || mem.planModel || '') : ''
985
+ var mt = props.current || null
986
+ var files = props.files || []
987
+ var onOp = props.onOp || null
988
+ var logTail = props.logTail || ''
989
+ var findings = arrOf(tk.findings).map(function (f, i) { return h('li', { key: i }, esc((f.severity || 'low') + ': ' + (f.title || f.detail || ''))) })
990
+ var kv = [['id', tk.id], ['kind', kindLabel(tk.kind)], ['status', stLabel(tk.status)], ['round', tk.round], ['attempt', tk.attempt], ['verdict', verdictLabel(tk.verdict)], ['dependsOn', arrOf(tk.dependsOn).join(', ')], ['changedPaths', arrOf(tk.changedPaths).join(', ') || '—'], ['verify', arrOf(tk.verify).join('; ') || '—'], ['acceptance', Array.isArray(tk.acceptance) ? tk.acceptance.join('; ') : (tk.acceptance == null ? '' : String(tk.acceptance))]]
991
+ return h('div', { className: 'exp-detail' },
992
+ h('h4', null, esc(tk.title || tk.spec || tk.id || '')),
993
+ h('div', { className: 'exp-detail-who' },
994
+ h('span', { className: 'exp-ava', style: { background: col } }, esc(ini)),
995
+ h('span', null, esc(nm) + ' · ' + esc(roleLabel(tk.owner)) + ' · ' + esc(st) + (model ? ' · ' + esc(String(model).slice(0, 20)) : '')),
996
+ act === 'running' ? h('span', { className: 'exp-live', title: t('后台运行中,完成会自动通知', 'running — you will be notified') }) : null),
997
+ onOp ? h('div', { className: 'exp-task-op' },
998
+ h('button', { className: 'exp-op-btn', onClick: function () { onOp('in_progress') } }, t('进行中', 'Start')),
999
+ h('button', { className: 'exp-op-btn', onClick: function () { onOp('completed') } }, t('完成', 'Done')),
1000
+ h('button', { className: 'exp-op-btn bad', onClick: function () { onOp('failed') } }, t('失败', 'Fail'))) : null,
1001
+ h('div', null, kv.map(function (k) { return h('div', { className: 'exp-kv', key: k[0] }, h('b', null, esc(k[0])), h('span', null, esc(String(k[1] == null ? '' : k[1])))) })),
1002
+ mt ? h('div', { className: 'exp-detail-sec' }, h('b', null, t('正在做', 'Doing')), h('div', { className: 'exp-doing' }, '▶ ' + esc(String(mt.title || mt.spec || mt.id || '').slice(0, 64)))) : null,
1003
+ files.length ? h('div', { className: 'exp-detail-sec' }, h('b', null, t('改动的文件(git 实时)', 'Changed files (live)')), h('div', { className: 'exp-files' }, files.map(function (f, i) { return h('div', { key: i, className: 'exp-file' }, esc(String(f).slice(0, 96))) }))) : null,
1004
+ findings.length ? h('ul', { className: 'exp-findings' }, findings) : null,
1005
+ logTail ? h('div', { className: 'exp-detail-sec' }, h('b', null, t('运行日志(最新)', 'Run log (latest)')), h('pre', { className: 'exp-log' }, esc(logTail))) : null)
1006
+ } catch (e) {
1007
+ return h('div', { className: 'exp-preview' }, 'TASK ERR: ' + esc(String(e && e.stack ? e.stack : e)).slice(0, 700))
1008
+ }
1009
+ }
1010
+
1011
+ /**
1012
+ * F 线第 3 项:把 host 的 `settingsSchema()` + 当前设置编译成**表单行**(纯函数 ⇒ 可单测)。
1013
+ * UI 结构来自 host 的 spec(不在客户端另写一份默认值/值域),认不出的类型**如实标出来**
1014
+ * 而不是静默渲染成一个空控件 —— 那会让"新增了设置项但界面没跟上"变得看不见。
1015
+ */
1016
+ function settingsFormModel(schema, settings) {
1017
+ var groups = Array.isArray(schema) ? schema : []
1018
+ var vals = settings && typeof settings === 'object' ? settings : {}
1019
+ return groups.map(function (g) {
1020
+ var rows = (g.items || []).map(function (it) {
1021
+ var cur = vals[g.group] ? vals[g.group][it.key] : undefined
1022
+ var value = typeof cur === 'undefined' ? it.default : cur
1023
+ return {
1024
+ path: it.path, key: it.key, type: it.type, label: it.label, hint: it.hint || '',
1025
+ values: it.values || null, min: typeof it.min === 'number' ? it.min : null, max: typeof it.max === 'number' ? it.max : null,
1026
+ value: it.type === 'roles' ? (Array.isArray(value) ? value.join(', ') : '') : value,
1027
+ known: ['bool', 'int', 'enum', 'roles'].indexOf(it.type) >= 0,
1028
+ }
1029
+ })
1030
+ return { group: g.group, label: g.label, hint: g.hint || '', rows: rows }
1031
+ })
1032
+ }
1033
+
1034
+ /**
1035
+ * 设置页签:**只做三件事** —— 读(GET /settings)、渲染(照 schema)、存(POST,自动保存)。
1036
+ * 失败一律照实显示(400 的错误列表 / 网络错误),**并把控件回滚到服务端的值** ——
1037
+ * "界面显示 A、服务端是 B"是最难排查的状态,宁可回滚也不装作成功。
1038
+ */
1039
+ function SettingsTab() {
1040
+ var sS = useState(null); var data = sS[0], setData = sS[1]
1041
+ var mS = useState(''); var msg = mS[0], setMsg = mS[1]
1042
+ var eS = useState(''); var err = eS[0], setErr = eS[1]
1043
+ useEffect(function () {
1044
+ var alive = true
1045
+ fetch('/plugins/dsh-expert-team/settings').then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
1046
+ if (alive && d && d.ok) setData(d)
1047
+ }).catch(function (e) { if (alive) setErr(String(e && e.message ? e.message : e)) })
1048
+ return function () { alive = false }
1049
+ }, [])
1050
+ function save(patch) {
1051
+ setErr(''); setMsg('保存中…')
1052
+ fetch('/plugins/dsh-expert-team/settings', {
1053
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(patch),
1054
+ }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d } }) }).then(function (res) {
1055
+ if (!res.ok || !res.d || !res.d.ok) {
1056
+ setErr(((res.d && res.d.errors) || ['保存失败']).join(';'))
1057
+ setMsg('')
1058
+ return
1059
+ }
1060
+ setData(function (prev) { return { schema: (prev && prev.schema) || [], settings: res.d.settings, ok: true } })
1061
+ setMsg(res.d.needsRestart ? '已保存 —— **重启 dsh web 后生效**' : '已保存')
1062
+ }).catch(function (e) { setErr(String(e && e.message ? e.message : e)); setMsg('') })
1063
+ }
1064
+ if (err) return h('div', { className: 'exp-settings' }, h('div', { style: { color: '#c0392b' } }, '✗ ' + esc(err)))
1065
+ if (!data || !data.schema) return h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)'))
1066
+ var model = settingsFormModel(data.schema, data.settings)
1067
+ var rows = []
1068
+ model.forEach(function (g) {
1069
+ rows.push(h('div', { key: 'h-' + g.group, className: 'exp-settings-group' }, esc(g.label) + (g.hint ? ' · ' + esc(g.hint) : '')))
1070
+ g.rows.forEach(function (r) {
1071
+ var ctl
1072
+ if (!r.known) {
1073
+ ctl = h('span', { style: { color: '#c0392b' } }, '未知类型 ' + esc(String(r.type)) + '(界面未跟上 spec)')
1074
+ } else if (r.type === 'bool') {
1075
+ ctl = h('input', { type: 'checkbox', checked: !!r.value, onChange: function (e) { save({ [r.path]: e.target.checked }) } })
1076
+ } else if (r.type === 'int') {
1077
+ ctl = h('input', { type: 'number', value: String(r.value), min: r.min === null ? undefined : r.min, max: r.max === null ? undefined : r.max, style: { width: 72 },
1078
+ onChange: function (e) { var v = e.target.value; if (v !== '' && /^-?\d+$/.test(v)) save({ [r.path]: Number(v) }) } })
1079
+ } else if (r.type === 'enum') {
1080
+ ctl = h('select', { value: String(r.value), onChange: function (e) { save({ [r.path]: e.target.value }) } },
1081
+ (r.values || []).map(function (v) { return h('option', { key: v, value: v }, esc(v)) }))
1082
+ } else {
1083
+ ctl = h('input', { type: 'text', value: String(r.value), placeholder: t('留空 = 按档位默认', 'empty = tier default'), style: { width: 150 },
1084
+ onBlur: function (e) { var v = e.target.value.trim(); save({ [r.path]: v === '' ? null : v.split(',').map(function (x) { return x.trim() }).filter(Boolean) }) } })
1085
+ }
1086
+ rows.push(h('div', { key: r.path, className: 'exp-settings-row' },
1087
+ h('span', { className: 'exp-settings-label', title: r.hint }, esc(r.label)),
1088
+ ctl,
1089
+ r.hint ? h('span', { className: 'exp-muted', style: { marginLeft: 6 } }, esc(r.hint)) : null))
1090
+ })
1091
+ })
1092
+ return h('div', { className: 'exp-settings' }, rows, h('div', { className: 'exp-muted', style: { marginTop: 6 } }, esc(msg || '')))
1093
+ }
1094
+
1095
+ function DecisionCard(props) {
1096
+ var d = props.decision || {}
1097
+ var options = (d.options || []).map(function (o) {
1098
+ var v = typeof o === 'string' ? o : o.label
1099
+ var id = typeof o === 'string' ? o : (o.id || o.label)
1100
+ return h('button', { key: id, className: 'exp-opt', onClick: function () { props.onChoose(id, v) } }, esc(v))
1101
+ })
1102
+ return h('div', { className: 'exp-decision' },
1103
+ h('div', { className: 'exp-decision-title' }, esc(d.title || t('等待你的决策', 'Your decision needed'))),
1104
+ d.prompt ? h('div', { className: 'exp-muted', style: { margin: '4px 0 8px' } }, esc(d.prompt)) : null,
1105
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } }, options))
1106
+ }
1107
+
1108
+ function Panel(props) {
1109
+ var sidProp = (props && props.sessionId) || ''
1110
+ var viewMode = (props && props.mode) === 'view'
1111
+ var sessionId = sidProp || useCurrentSession()
1112
+ var isOpen = useOpen()
1113
+ var nativeWf = useNativeWf()
1114
+ var docked = useDocked()
1115
+ var pos = usePanelPos()
1116
+ useLang() // re-render when the shell language changes
1117
+ var pw = usePanelWidth()
1118
+ var dataS = useState(null), data = dataS[0], setData = dataS[1]
1119
+ var runsMetaS = useState([]); var runsMeta = runsMetaS[0], setRunsMeta = runsMetaS[1]
1120
+ var selTask = useState(null); var selTaskV = selTask[0], setSelTask = selTask[1]
1121
+ var selArt = useState(null); var selArtV = selArt[0], setSelArt = selArt[1]
1122
+ var artText = useState(null); var artTextV = artText[0], setArtText = artText[1]
1123
+ var selRun = useState(null); var selRunV = selRun[0], setSelRun = selRun[1]
1124
+ var feedS = useState(null); var feedRole = feedS[0], setFeedRole = feedS[1]
1125
+ // 按 agentId 看实时细节流(对话流 workflow 卡的成员未必能反查到角色)
1126
+ var feedAS = useState(null); var feedAgent = feedAS[0], setFeedAgent = feedAS[1]
1127
+ var qState = useState(''); var qV = qState[0], setQ = qState[1]
1128
+ var qHitsState = useState(null); var qHitsV = qHitsState[0], setQHits = qHitsState[1]
1129
+ var qInfoS = useState(null); var qInfoV = qInfoS[0], setQInfo = qInfoS[1]
1130
+ var fileS = useState(null); var fileV = fileS[0], setFileV = fileS[1]
1131
+ var prevActRef = useRef(null)
1132
+ var prevDecRef = useRef(0)
1133
+ var hoverS = useState(null); var hoverId = hoverS[0], setHover = hoverS[1]
1134
+ var view = useState('dag'); var viewV = view[0], setView = view[1]
1135
+ var tabS = useState('team'); var tab = tabS[0], setTab = tabS[1]
1136
+ var listOpenS = useState(false); var listOpen = listOpenS[0], setListOpen = listOpenS[1]
1137
+ // 流转视图(波次带)自己的 UI 状态:宽视图 / 波折叠 / 未绑定任务展开
1138
+ var wideS = useState(false); var wideV = wideS[0], setWide = wideS[1]
1139
+ var cwS = useState({}); var collapsedWaves = cwS[0], setCollapsedWaves = cwS[1]
1140
+ var ubS = useState(false); var unboundOpen = ubS[0], setUnboundOpen = ubS[1]
1141
+ // 全景图的可画宽度:**实测容器宽度**,不能拿"侧栏宽度 360"当全屏画布的宽度。
1142
+ // 旧实现用 pw(停靠面板宽度偏好,默认 360)算 NODE_W ⇒ 在**全屏画布**里卡片被挤成
1143
+ // 一小撮、右侧留一大片空白;同时 `wideV`(宽视图按钮)当时压根没接进宽度计算,是个死按钮。
1144
+ var flowWS = useState(0); var flowW = flowWS[0], setFlowW = flowWS[1]
1145
+ useEffect(function () {
1146
+ function measure() {
1147
+ try {
1148
+ var be = document.querySelector('.exp-body') || document.querySelector('.exp-panel')
1149
+ var w = be && be.clientWidth ? be.clientWidth : 0
1150
+ if (w) setFlowW(w)
1151
+ } catch (e) { /* best-effort */ }
1152
+ }
1153
+ measure()
1154
+ try { window.addEventListener('resize', measure) } catch (e) {}
1155
+ return function () { try { window.removeEventListener('resize', measure) } catch (e) {} }
1156
+ }, [viewMode, docked])
1157
+ // 消费对话流卡片发来的「聚焦意图」:role → 该角色的进行中任务 + 实时操作流;task → 直接选中。
1158
+ // 只处理新 seq,避免轮询重渲染时重复应用。
1159
+ var focusIv = useFocusIntent()
1160
+ var wfPhaseIv = useWfPhases()
1161
+ var focusSeqRef = useRef(0)
1162
+ useEffect(function () {
1163
+ if (!focusIv || !focusIv.seq || focusIv.seq === focusSeqRef.current) return
1164
+ try {
1165
+ if (focusIv.kind === 'task' && focusIv.task) { focusSeqRef.current = focusIv.seq; setTab('team'); setSelTask(focusIv.task); return }
1166
+ if (focusIv.kind === 'role' && focusIv.role) {
1167
+ var arr = ((data && data.tasks) || [])
1168
+ var active = arr.filter(function (x) { return x && x.owner === focusIv.role && ['in_progress', 'claimed', 'rework'].indexOf(x.status) >= 0 })[0]
1169
+ if (!active) active = arr.filter(function (x) { return x && x.owner === focusIv.role })[0]
1170
+ // 任务列表可能还没拉到:此时不消费该意图,等 data 到达后由依赖变化再应用一次。
1171
+ if (!active && !arr.length) return
1172
+ focusSeqRef.current = focusIv.seq
1173
+ setTab('team')
1174
+ setFeedRole(focusIv.role)
1175
+ if (active) setSelTask(active)
1176
+ return
1177
+ }
1178
+ // kind:'agent' —— 对话流 workflow 卡的成员行(拿得到 childId,未必知道角色):
1179
+ // 先按 childId 反查角色(members[r].id);查不到就退化成"只看该 agent 的实时细节流"。
1180
+ if (focusIv.kind === 'agent' && focusIv.agentId) {
1181
+ var role = ''
1182
+ var mem = (data && data.members) || {}
1183
+ Object.keys(mem).forEach(function (r) { if (!role && mem[r] && String(mem[r].id || '') === String(focusIv.agentId)) role = r })
1184
+ var arr2 = ((data && data.tasks) || [])
1185
+ focusSeqRef.current = focusIv.seq
1186
+ setTab('team')
1187
+ if (role) {
1188
+ setFeedRole(role); setFeedAgent(null)
1189
+ var t2 = arr2.filter(function (x) { return x && x.owner === role && ['in_progress', 'claimed', 'rework'].indexOf(x.status) >= 0 })[0] || arr2.filter(function (x) { return x && x.owner === role })[0]
1190
+ if (t2) setSelTask(t2)
1191
+ } else {
1192
+ setFeedRole(null)
1193
+ setFeedAgent({ id: String(focusIv.agentId), label: String(focusIv.label || '') })
1194
+ }
1195
+ }
1196
+ } catch (e) {}
1197
+ }, [focusIv, data])
1198
+ // F1 计划门:草稿本地编辑态(dirty 时不被轮询覆盖)
1199
+ var draftS = useState(null); var draftV = draftS[0], setDraftV = draftS[1]
1200
+ var draftMeta = useRef({ loadedAt: '', dirty: false })
1201
+ var planMsgS = useState(''); var planMsg = planMsgS[0], setPlanMsg = planMsgS[1]
1202
+ var err = useState(''); var errV = err[0], setErr = err[1]
1203
+ var pollRef = useRef(0)
1204
+
1205
+ function stateUrl() {
1206
+ var url = '/plugins/dsh-expert-team/state', q = []
1207
+ if (sessionId) q.push('sessionId=' + encodeURIComponent(sessionId))
1208
+ if (selRunV && selRunV.workspace && selRunV.runId) { q.push('workspace=' + encodeURIComponent(selRunV.workspace)); q.push('run=' + encodeURIComponent(selRunV.runId)) }
1209
+ return url + (q.length ? ('?' + q.join('&')) : '')
1210
+ }
1211
+ function load() {
1212
+ fetch(stateUrl()).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
1213
+ if (d && d.ok) {
1214
+ setData(d); setErr(''); reschedule(d)
1215
+ // N2: keep the composer takeover in sync with pendingDecision
1216
+ // 批 0-2:把 /state 已经返回的 runId/workspace 一并传出,供横幅拍板时回传给 /decide
1217
+ try { if (exports._syncPending) exports._syncPending(d.pendingDecision, sessionId, d.runId, d.workspace) } catch (e) {}
1218
+ // A⑤ browser notification when a running member settles
1219
+ try {
1220
+ if (window.Notification && Notification.permission === 'granted') {
1221
+ var prev = prevActRef.current || {}
1222
+ var now = {}
1223
+ var names = {}
1224
+ Object.keys(d.members || {}).forEach(function (k) {
1225
+ var mm = d.members[k]
1226
+ if (mm && typeof mm === 'object') { now[mm.id] = mm.activity || ''; if (mm.id) names[mm.id] = (mm.name || '') + ' · ' + roleLabel(k); if (mm.id == null) names[mm.name] = (mm.name || '') + ' · ' + roleLabel(k); if (!mm.id) { now[k] = mm.activity || ''; names[k] = (mm.name || '') + ' · ' + roleLabel(k) } }
1227
+ })
1228
+ Object.keys(now).forEach(function (id) {
1229
+ if (prev[id] === 'running' && now[id] !== 'running') {
1230
+ try { new Notification('专家团:' + (names[id] || id) + ' 完成当前任务', { body: String(d.runId || '') }) } catch (e) {}
1231
+ }
1232
+ })
1233
+ prevActRef.current = now
1234
+ }
1235
+ } catch (e) {}
1236
+ // B1: notify the moment a decision gate opens (方案确认门等)
1237
+ try {
1238
+ var decNow = (d.pendingDecision && d.pendingDecision.options && d.pendingDecision.options.length) ? 1 : 0
1239
+ if (decNow && !prevDecRef.current && window.Notification && Notification.permission === 'granted') {
1240
+ try { new Notification('专家团等待你的决策', { body: String((d.pendingDecision && d.pendingDecision.title) || phaseLabel(d.phase || '') || '') }) } catch (e) {}
1241
+ }
1242
+ prevDecRef.current = decNow
1243
+ } catch (e) {}
1244
+ } else if (d && !d.ok && d.runsAvailable === 0) { setData(null); if (d.runs) setRunsMeta(d.runs); setErr(t('还没有专家团 run。先运行 /team <task> 开一个。', 'No team run yet — run /team <task> first.')) }
1245
+ else if (d && !d.ok) { if (d.runs) setRunsMeta(d.runs); setErr(d.error || ''); }
1246
+ }).catch(function () {})
1247
+ }
1248
+ // Adaptive refresh: while any member is running, poll fast (workers are
1249
+ // touching files right now); otherwise 3s. Re-schedules after each load.
1250
+ function reschedule(d) {
1251
+ var m = (d && d.members) || {}
1252
+ var busy = Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
1253
+ clearInterval(pollRef.current)
1254
+ pollRef.current = setInterval(load, busy ? 1200 : 3000)
1255
+ }
1256
+ useEffect(function () {
1257
+ if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
1258
+ load(); pollRef.current = setInterval(load, 3000)
1259
+ // catch up instantly when the user returns to the tab / window
1260
+ var onVis = function () { if (document.visibilityState === 'visible') load() }
1261
+ var onFocus = function () { load() }
1262
+ document.addEventListener('visibilitychange', onVis)
1263
+ window.addEventListener('focus', onFocus)
1264
+ return function () { clearInterval(pollRef.current); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('focus', onFocus) }
1265
+ }, [sessionId, selRunV, isOpen, viewMode])
1266
+
1267
+ // Make the docked panel reserve space (shift the shell frame) instead of
1268
+ // covering the workspace; clears when closed or floating (md-preview trick).
1269
+ useEffect(function () {
1270
+ try {
1271
+ var overlay = document.querySelector('[data-shell-overlay]')
1272
+ var frame = overlay && overlay.parentElement
1273
+ if (frame) frame.style.paddingRight = (isOpen && docked) ? pw + 'px' : ''
1274
+ } catch (e) {}
1275
+ }, [isOpen, docked, pw])
1276
+
1277
+ function openArtifact(name, runId, workspace) {
1278
+ setSelArt(name)
1279
+ var q = '?name=' + encodeURIComponent(name) + '&run=' + encodeURIComponent(runId)
1280
+ if (workspace) q += '&workspace=' + encodeURIComponent(workspace); else if (sessionId) q += '&sessionId=' + encodeURIComponent(sessionId)
1281
+ fetch('/plugins/dsh-expert-team/artifact' + q).then(function (r) { return r.json() }).then(function (d) { setArtText((d && d.ok) ? d.text : ('(无法读取 ' + name + ')')) }).catch(function () { setArtText('读取失败') })
1282
+ }
1283
+
1284
+ function decide(body) {
1285
+ var payload = Object.assign({ sessionId: sessionId, workspace: workspace, run: runId }, body)
1286
+ fetch('/plugins/dsh-expert-team/decide', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
1287
+ .then(function (r) { return r.json() }).then(function (d) { load() }).catch(function () { load() })
1288
+ }
1289
+
1290
+ // ── F1 计划门:草稿同步 / 编辑 / 保存 / 批准 / 丢弃(AgentTeams 借鉴)──
1291
+ // 轮询不覆盖正在编辑的草稿(dirty 优先),仅在 updatedAt 变化且未编辑时重载。
1292
+ useEffect(function () {
1293
+ var d = data && data.draft
1294
+ if (!d) { if (!draftMeta.current.dirty) { draftMeta.current.loadedAt = ''; setDraftV(null) } return }
1295
+ if (draftMeta.current.dirty) return
1296
+ var at = String(d.updatedAt || '')
1297
+ if (draftMeta.current.loadedAt === at) return
1298
+ draftMeta.current.loadedAt = at
1299
+ try { setDraftV(JSON.parse(JSON.stringify(d))) } catch (e) { setDraftV(d) }
1300
+ }, [data])
1301
+
1302
+ function planPost(path, extra) {
1303
+ var ws = (selRunV && selRunV.workspace) || workspace || (data && data.workspace) || ''
1304
+ var rn = (selRunV && selRunV.runId) || runId || (data && data.runId) || ''
1305
+ var body = Object.assign({ sessionId: sessionId, workspace: ws, run: rn }, extra || {})
1306
+ setPlanMsg(t('提交中…', 'sending…'))
1307
+ fetch('/plugins/dsh-expert-team/plan' + (path || ''), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
1308
+ .then(function (r) { return r.json() }).then(function (j) {
1309
+ if (j && j.ok) {
1310
+ draftMeta.current.dirty = false; draftMeta.current.loadedAt = ''
1311
+ if (path === '/approve') { setPlanMsg(t('✅ 已批准并派工', 'approved & dispatched')) }
1312
+ else if (path === '/discard') { setPlanMsg(t('🗑 已丢弃(禁止自动重建)', 'discarded')) }
1313
+ else { setPlanMsg(t('💾 草稿已保存', 'draft saved')) }
1314
+ load()
1315
+ } else { setPlanMsg((j && j.error) || t('操作失败', 'failed')) }
1316
+ }).catch(function () { setPlanMsg(t('请求失败', 'request failed')) })
1317
+ }
1318
+ function draftEdit(fn) {
1319
+ var d
1320
+ try { d = draftV ? JSON.parse(JSON.stringify(draftV)) : { roles: [], tasks: [] } } catch (e) { d = { roles: [], tasks: [] } }
1321
+ fn(d)
1322
+ draftMeta.current.dirty = true
1323
+ setDraftV(d)
1324
+ }
1325
+
1326
+ // Qoder-style code index: instant keyword hits from team/CODEINDEX.json.
1327
+ function codeSearch() {
1328
+ var ws = workspace || (data && data.workspace) || ''
1329
+ var kw = qV.trim()
1330
+ if (!ws || !kw) return
1331
+ setQHits(null); setQInfo(null)
1332
+ fetch('/plugins/dsh-expert-team/codeidx?workspace=' + encodeURIComponent(ws) + '&q=' + encodeURIComponent(kw))
1333
+ .then(function (r) { return r.json() }).then(function (d) { setQHits((d && d.ok) ? d.hits : []); setQInfo(d && d.ok ? d : null) }).catch(function () { setQHits([]) })
1334
+ }
1335
+ // A① click a code-index hit → preview the file inside the panel
1336
+ function openFileView(rel) {
1337
+ var ws = workspace || (data && data.workspace) || ''
1338
+ if (!ws || !rel) return
1339
+ setFileV({ path: rel, text: t('加载中…', 'Loading…'), loading: true })
1340
+ fetch('/plugins/dsh-expert-team/file?workspace=' + encodeURIComponent(ws) + '&path=' + encodeURIComponent(rel))
1341
+ .then(function (r) { return r.json() }).then(function (d) { setFileV(d && d.ok ? { path: rel, text: d.text, loading: false } : { path: rel, text: t('(无法读取)', '(unreadable)') + ' ' + String((d && d.error) || ''), loading: false }) }).catch(function () { setFileV({ path: rel, text: t('读取失败', 'read failed'), loading: false }) })
1342
+ }
1343
+ // B⑦ overlay task operation (writes TASKS.json + RUN.log)
1344
+ function taskOp(status) {
1345
+ if (!selTaskV || !workspace || !runId) return
1346
+ setErr(t('正在写状态…', 'updating…'))
1347
+ fetch('/plugins/dsh-expert-team/task', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId, workspace: workspace, run: runId, id: selTaskV.id, status: status, note: '浮层操控' }) })
1348
+ .then(function (r) { return r.json() }).then(function () { load() }).catch(function () { load() })
1349
+ }
1350
+
1351
+ var runId = (data && data.runId) || '', workspace = (data && data.workspace) || '', runs = (data && data.runs) || []
1352
+ var tasks0 = (data && data.tasks) || []
1353
+ var tasksLive = (data && data.tasksLive) || []
1354
+ // TASKS.json 为空但确有活子代理(lead 用 workflow 扇出却没回写任务)→ 用主机投影的
1355
+ // 实时任务兜底,让任务清单/依赖图/全景可用;行上带 live:true,UI 会明确标注来源。
1356
+ var usingLiveTasks = !tasks0.length && tasksLive.length > 0
1357
+ // 实时投影没有依赖数据;把"运行中"排前面、再按角色聚合,明细视图才不显得杂乱
1358
+ var tasks = usingLiveTasks
1359
+ ? tasksLive.slice().sort(function (a, b) {
1360
+ var ra = a.status === 'in_progress' ? 0 : 1, rb = b.status === 'in_progress' ? 0 : 1
1361
+ if (ra !== rb) return ra - rb
1362
+ return String(a.owner || '').localeCompare(String(b.owner || ''))
1363
+ })
1364
+ : tasks0
1365
+ var roles = (data && data.roles) || [], members = (data && data.members) || {}, coverage = (data && data.coverage) || []
1366
+ var violations = (data && data.violations) || []
1367
+ var phaseIdx = PHASES.indexOf((data && data.phase) || '')
1368
+ var total = tasks.length, done = tasks.filter(function (t) { return FINAL.indexOf(t.status) >= 0 }).length, pct = total ? Math.round((done / total) * 100) : 0
1369
+ var models = []
1370
+ roles.forEach(function (r) { var m = members[r]; if (m && (m.model || m.provider || m.activity)) { var v = m.model || m.provider || m.activity; var s = typeof v === 'string' ? v : (v.model || ''); if (s && models.indexOf(s) < 0) models.push(s) } })
1371
+ var modelsLine = models.length ? (t('在用模型:', 'Models: ') + models.join(', ') + ' · ' + t('会话费用见 dsh-cost-meter', 'see dsh-cost-meter for cost')) : ''
1372
+ // cost/model plan (heavy=top / light=fast) from ROSTER.models
1373
+ var plan = (data && data.models) || {}
1374
+ var byM = {}
1375
+ roles.forEach(function (r) { var p = plan[r]; if (p && p.model) { byM[p.model] = byM[p.model] || []; byM[p.model].push(roleLabel(r)) } })
1376
+ var planLine = Object.keys(byM).sort().map(function (m) { return m.replace(/^deepseek-/, '') + '(' + byM[m].join('/') + ')' }).join(' · ')
1377
+ planLine = planLine ? t('模型计划:', 'Plan: ') + planLine : ''
1378
+
1379
+ var stepper = PHASES.map(function (p, i) { return h('li', { key: p, className: i === phaseIdx ? 'cur' : (i < phaseIdx ? 'done' : '') }, esc(phaseLabel(p))) })
1380
+
1381
+ // 活子代理里匹配不到角色的(host 侧按 label/括号角色 id/首条 prompt 推断;
1382
+ // workflow 派生的子代理 label 常为空)→ 如实列出,避免「有人跑着但名单全未启动」。
1383
+ var agentsLive = (data && data.agents) || []
1384
+ // 角色三源合并(members 权威 → agents[].role → workflow 卡 label 兜底)。
1385
+ // 旧实现只按 members[role].id 建索引 —— 对 workflow 派生子代理(host 解析不出角色)必然落空,
1386
+ // 于是面板把它们全列进「未匹配到角色」(用户实测看到 22 个)。
1387
+ var roleResolve = (function () {
1388
+ try { return resolveAgentRoles(members, roles, agentsLive, wfPhaseIv) }
1389
+ catch (e) {
1390
+ // 兜底:宁可退化成"全部未匹配"(用户至少还能看到如实清单),也不让浮层整块渲染失败。
1391
+ try { console.warn("[expert-team] 角色解析失败,已降级:", e && e.message) } catch (e2) {}
1392
+ return { byRole: {}, byId: {}, matched: {}, unmatched: agentsLive, roleless: agentsLive, extraByRole: {}, dispatched: 0, wfRole: {} }
1393
+ }
1394
+ })()
1395
+ var unmatchedAgents = roleResolve.unmatched
1396
+ var rolelessAgents = Array.isArray(roleResolve.roleless) ? roleResolve.roleless : []
1397
+ var extraByRole = (roleResolve.extraByRole && typeof roleResolve.extraByRole === 'object') ? roleResolve.extraByRole : {}
1398
+ // ⚠️ dispatched 必须在 roleResolve **之后**取值。var 会提升,写在前面会读到 undefined:
1399
+ // 真实事故(浏览器实测抓到,Node 层测试抓不到)——
1400
+ // TypeError: Cannot read properties of undefined (reading 'dispatched')
1401
+ // slot entry crashed in 'conversation.view' ← 嵌入式画布整块崩掉
1402
+ var dispatched = roleResolve.dispatched
1403
+ var roster = roles.map(function (r) {
1404
+ var m = members[r], live = (m && typeof m === 'object') ? m : null
1405
+ var nm = live ? (live.name || r) : r
1406
+ var col = live ? (live.color || '#5b8def') : '#9aa4b2'
1407
+ var ini = live ? (live.initial || nm.slice(0, 1).toUpperCase()) : nm.slice(0, 1).toUpperCase()
1408
+ var hasAgent = !!(live && live.active)
1409
+ var act = live ? (live.activity || '') : ''
1410
+ var st = act === 'running' ? 'running' : (act === 'idle' || act === 'ready' || act === 'inactive') ? 'idle' : ''
1411
+ var dot = !hasAgent ? '#c5ccd6' : st === 'running' ? '#22b07d' : st === 'idle' ? '#3b6ef5' : '#9aa4b2'
1412
+ var sub = live ? (live.model || live.planModel || live.label || live.id || '') : ((m && typeof m === 'object' && m.planModel) || '')
1413
+ var cur = tasks.filter(function (t) { return t.owner === r && ['in_progress', 'claimed', 'rework'].indexOf(t.status) >= 0 })
1414
+ var curT = cur[0]
1415
+ return h('div', { className: 'exp-row mem' + (st === 'running' ? ' exp-mem-run' : ''), key: r, onClick: function () { setFeedRole(feedRole === r ? null : r) }, title: (live && live.id ? t('子代理 ', 'agent ') + String(live.id).slice(0, 8) + (live.label ? ' · ' + String(live.label).slice(0, 40) : '') + ' · ' : '') + t('点击查看实时操作流', 'click for live feed') },
1416
+ h('span', { className: 'exp-mem-main' },
1417
+ h('span', { className: 'exp-ava', style: { background: col } }, esc(ini)),
1418
+ h('span', { className: 'exp-mem-name' }, esc(nm)),
1419
+ h('span', { className: 'exp-role' }, esc(roleLabel(r))),
1420
+ h('span', { className: 'exp-muted' }, esc(st ? (st === 'running' ? t('运行中', 'running') : t('空闲', 'idle')) : (hasAgent ? t('已派', 'dispatched') : t('未启动', 'not started'))))),
1421
+ h('span', { className: 'exp-mem-sub' }, sub ? esc(String(sub).slice(0, 20)) : null),
1422
+ curT ? h('div', { className: 'exp-mem-cur' }, '▶ ' + esc(String(curT.title || curT.spec || curT.id || '').slice(0, 24))) : null)
1423
+ })
1424
+
1425
+ var taskRows = tasks.map(function (tk) {
1426
+ var owner = tk.owner || '—'
1427
+ var mm = members[owner], live = (mm && typeof mm === 'object') ? mm : null
1428
+ var nm = live ? (live.name || owner) : owner
1429
+ var colC = live ? (live.color || '#5b8def') : '#9aa4b2'
1430
+ var ini = live ? (live.initial || nm.slice(0, 1).toUpperCase()) : nm.slice(0, 1).toUpperCase()
1431
+ var cur = ['in_progress', 'claimed', 'rework'].indexOf(tk.status) >= 0
1432
+ var f = arrOf(tk.findings).length
1433
+ return h('div', { className: 'exp-row task' + (cur ? ' cur' : ''), key: tk.id, onClick: function () { var same = selTaskV && selTaskV.id === tk.id; setSelTask(same ? null : tk); if (!same) focusPanelRight() } },
1434
+ h('span', { className: 'exp-ava', style: { background: colC } }, esc(ini)),
1435
+ h('div', { style: { flex: 1, minWidth: 0 } },
1436
+ h('div', { className: 'exp-task-t' }, esc(String(tk.id || '')) + ' · ' + esc(String(tk.title || tk.spec || '').slice(0, 34))),
1437
+ h('div', { className: 'exp-muted' },
1438
+ esc(nm) + ' ⟶ ' + esc(roleLabel(owner)) + ((live && (live.model || live.planModel)) ? ' · ' + esc(String(live.model || live.planModel).slice(0, 16)) : '') + (f ? ' · ' + f + t(' 条发现', ' findings') : '') + ((tk.round || 1) > 1 ? ' · 第' + tk.round + '轮' : ''))),
1439
+ h('span', { style: { display: 'flex', gap: 4, flex: 'none' } },
1440
+ tk.verdict ? verdictBadge(tk.verdict) : null,
1441
+ h('span', { className: 'exp-badge ' + stBadgeCls(tk.status) }, esc(stLabel(tk.status)))))
1442
+ })
1443
+ // 收起态摘要:按状态计数一行显示(默认折叠,点标题展开)
1444
+ var sumMap = {}
1445
+ tasks.forEach(function (tk) { var s = tk.status || 'pending'; sumMap[s] = (sumMap[s] || 0) + 1 })
1446
+ var listSummary = ['in_progress', 'claimed', 'pending', 'done', 'rework', 'blocked', 'failed'].filter(function (s) { return sumMap[s] }).map(function (s) { return sumMap[s] + ' ' + stLabel(s) }).join(' · ')
1447
+
1448
+ // 任务流转(全景):先把任务按 dependsOn 分层(同层=并行),再把
1449
+ // 「单卡连续链」合并为一行(横向滚动 + → 箭头),只在并行分支/汇合
1450
+ // 处换行——B 链 9 个任务一行滚过,而不是瀑布 9 层刷屏。
1451
+ function flowLevels(ts) {
1452
+ var byId = {}, lvl = {}
1453
+ ts.forEach(function (t) { byId[t.id] = t; lvl[t.id] = 0 })
1454
+ var changed = true, guard = 0
1455
+ while (changed && guard++ < 32) {
1456
+ changed = false
1457
+ ts.forEach(function (t) {
1458
+ arrOf(t.dependsOn).forEach(function (d) {
1459
+ if (byId[d] && lvl[t.id] <= lvl[d]) { lvl[t.id] = lvl[d] + 1; changed = true }
1460
+ })
1461
+ })
1462
+ }
1463
+ var maxL = 0
1464
+ ts.forEach(function (t) { maxL = Math.max(maxL, lvl[t.id]) })
1465
+ var rows = []
1466
+ for (var l = 0; l <= maxL; l++) rows.push(ts.filter(function (t) { return lvl[t.id] === l }))
1467
+ return rows
1468
+ }
1469
+ function flowRows(ts) {
1470
+ var levels = flowLevels(ts)
1471
+ var out = [], cur = []
1472
+ levels.forEach(function (lv) {
1473
+ if (lv.length === 1) {
1474
+ var t0 = lv[0]
1475
+ var deps = arrOf(t0.dependsOn)
1476
+ // continue the current chain when any dep is already in it
1477
+ var continues = cur.length && deps.some(function (d) { return cur.some(function (c) { return c.id === d }) })
1478
+ if (continues) { cur.push(t0); return }
1479
+ if (cur.length) out.push(cur)
1480
+ cur = [t0]
1481
+ return
1482
+ }
1483
+ if (cur.length) out.push(cur)
1484
+ cur = []
1485
+ out.push(lv.slice()) // parallel row: keep as its own line
1486
+ })
1487
+ if (cur.length) out.push(cur)
1488
+ return out
1489
+ }
1490
+ var flowArrow = h('span', { className: 'exp-flow-a2' }, '→')
1491
+ var flowCards = function (tk) {
1492
+ var owner = tk.owner || '—', mm = members[owner], live = (mm && typeof mm === 'object') ? mm : null
1493
+ var nm = live ? (live.name || owner) : owner, col = live ? (live.color || '#5b8def') : '#9aa4b2'
1494
+ var ini = live ? (live.initial || nm.slice(0, 1).toUpperCase()) : nm.slice(0, 1).toUpperCase()
1495
+ var cur = ['in_progress', 'claimed', 'rework'].indexOf(tk.status) >= 0
1496
+ return h('div', { className: 'exp-flow' + (cur ? ' cur' : ''), key: tk.id, onClick: function () { var same = selTaskV && selTaskV.id === tk.id; setSelTask(same ? null : tk); if (!same) focusPanelRight() } },
1497
+ h('div', { className: 'exp-flow-a' }, h('span', { className: 'exp-ava', style: { background: col } }, esc(ini)),
1498
+ h('b', null, esc(String(nm).slice(0, 10))), h('span', { className: 'exp-role' }, esc(roleLabel(owner)))),
1499
+ h('div', { className: 'exp-flow-t' }, esc(String(tk.id || '') + ' · ' + String(tk.title || tk.spec || '').slice(0, 24))),
1500
+ h('span', { className: 'exp-badge ' + stBadgeCls(tk.status) }, esc(stLabel(tk.status))))
1501
+ }
1502
+ // ── 实时投影的"按角色分组"视图 ──
1503
+ // TASKS.json 为空时没有 dependsOn 数据,DAG 只能画成一排(用户反馈"一排效果不好")。
1504
+ // 改为按 owner(角色)分组:每组标题带活动态计数,组内每行一个 agent(状态点 + label + 状态),
1505
+ // 点击行 → 选中该任务(右侧详情);无依赖数据这件事在顶部明确说明。
1506
+ var liveGroups = null
1507
+ if (usingLiveTasks) {
1508
+ var byRole = {}
1509
+ tasks.forEach(function (tk) {
1510
+ var r = tk.owner || '—'
1511
+ if (!byRole[r]) byRole[r] = []
1512
+ byRole[r].push(tk)
1513
+ })
1514
+ var roleOrder = Object.keys(byRole).sort(function (a, b) { return byRole[b].length - byRole[a].length })
1515
+ liveGroups = h('div', null,
1516
+ h('div', { className: 'exp-flow-note' }, t('⚠ 无依赖数据(TASKS.json 为空)→ 改为按角色分组展示;点每行看右侧详情', '⚠ no dependency data (TASKS.json empty) → grouped by role; click a row for detail')),
1517
+ roleOrder.map(function (r) {
1518
+ var arr = byRole[r]
1519
+ var runN = arr.filter(function (x) { return x.status === 'in_progress' }).length
1520
+ return h('div', { key: 'g-' + r, style: { margin: '6px 0 2px' } },
1521
+ h('div', { className: 'exp-mem-sub', style: { display: 'flex', gap: 6, alignItems: 'center', margin: '4px 0 2px' } },
1522
+ h('b', null, esc(roleLabel(r) || r)),
1523
+ h('span', { className: 'exp-muted' }, arr.length + t(' 个', '') + (runN ? ' · ' + runN + t(' 运行中', ' running') : ''))),
1524
+ arr.map(function (tk) {
1525
+ var cur = tk.status === 'in_progress'
1526
+ return h('div', {
1527
+ className: 'exp-row task' + (cur ? ' cur' : ''),
1528
+ key: tk.id,
1529
+ title: t('点击查看详情(右侧面板)', 'click for detail (right panel)'),
1530
+ onClick: function () { var same = selTaskV && selTaskV.id === tk.id; setSelTask(same ? null : tk); if (!same) focusPanelRight() }
1531
+ },
1532
+ h('span', { className: 'exp-ava', style: { background: cur ? '#0969da' : '#9aa4b2' } }, cur ? '▶' : '✓'),
1533
+ h('div', { style: { flex: 1, minWidth: 0 } },
1534
+ h('div', { className: 'exp-task-t' }, esc(String(tk.title || tk.id).slice(0, 60))),
1535
+ h('div', { className: 'exp-muted' }, esc(roleLabel(tk.owner) || t('未识别角色', 'unknown role')) + (tk.model ? ' · ' + esc(String(tk.model).slice(0, 16)) : ''))),
1536
+ h('span', { className: 'exp-badge ' + stBadgeCls(tk.status) }, esc(stLabel(tk.status))))
1537
+ }))
1538
+ }))
1539
+ }
1540
+
1541
+ // ── 任务人员流转(SPEC §3-S1「波次主轴 · 紧凑流转」+ UI.md「竖直时间轴 + 波次带」)──
1542
+ // 主轴 = **时间波次**:纵向=波次(时间先后),横向=同波并列的人。leader 收缩为顶部唯一一行,
1543
+ // 不占"一层"。三条禁止:不臆造绑定(tasks 无 agentId 就不挂任务)、不臆造层级(无 createdAt 不分波)、
1544
+ // 不把 ROSTER 计划当已上场的人。降级一律在视图内写明原因。
1545
+ var flowDiagram = null
1546
+ ;(function buildFlowView() {
1547
+ var agentsAll = (data && data.agents) || []
1548
+ var agentRole = {}
1549
+ agentsAll.forEach(function (a) { if (a && a.id) agentRole[String(a.id)] = a.role || '' })
1550
+ var wfPh = (wfPhaseIv && wfPhaseIv.phases) || []
1551
+ // 阶段归属:childId → 阶段 key(仅用于波头徽章,不参与分层)
1552
+ var phaseOf = {}
1553
+ wfPh.forEach(function (ph) { (ph.members || []).forEach(function (m) { if (m && m.childId) phaseOf[String(m.childId)] = String(ph.key || '') }) })
1554
+ // 人:以 host agents[] 为准;agents 为空但有实时投影任务时,用投影行当"人"(无 createdAt → 时刻未知)
1555
+ var people = agentsAll.filter(function (a) { return a && a.id }).map(function (a) {
1556
+ return { id: String(a.id), role: a.role || '', label: '', activity: a.activity || '', model: a.model || '', createdAt: a.createdAt, parentId: a.parentId || '', depth: a.depth || 0, projected: false }
1557
+ })
1558
+ if (!people.length) {
1559
+ var liveRows = (data && data.tasksLive) || []
1560
+ liveRows.forEach(function (r) { if (r && r.agentId) people.push({ id: String(r.agentId), role: r.owner || '', label: String(r.title || ''), activity: r.status === 'in_progress' ? 'running' : 'inactive', model: r.model || '', createdAt: 0, parentId: '', depth: 0, projected: true }) })
1561
+ }
1562
+ var roles = (data && data.roles) || []
1563
+ // 分层依据补全:workflow 派生的子代理没有会话 header,改用事件流真实时间戳。
1564
+ // `timing` 回报用了哪一源,UI 必须如实标注(不把推断当事实)。
1565
+ var timed = enrichPeopleTiming(people, (data && data.wfChildren) || null)
1566
+ people = timed.people
1567
+ var timing = timed.used
1568
+ var W = buildWaves(people, WAVE_GAP_MS)
1569
+ var summary = W.waves.length + t(' 波 · ', ' waves · ') + people.length + t(' 人 · ', ' people · ') + people.filter(function (p) { return p.activity === 'running' }).length + t(' 在跑', ' running')
1570
+
1571
+ function hhmmss(ms) { try { var d = new Date(ms); return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2) } catch (e) { return '' } }
1572
+ function durLabel(p) { if (!p.lastEventAt || !p.createdAt) return ''; var s = Math.max(0, Math.round((p.lastEventAt - p.createdAt) / 1000)); return s >= 60 ? Math.floor(s / 60) + 'm' + (s % 60) + 's' : s + 's' }
1573
+ function feedLine(id) { var arr = (data && data.feed && data.feed[id]) || []; var last = arr[arr.length - 1]; return last ? String(last.line || last.preview || '').slice(0, 28) : '' }
1574
+ // 同一角色多人时用 ①② 区分(禁止聚合)
1575
+ var roleCount = {}
1576
+ people.forEach(function (p) { roleCount[p.role] = (roleCount[p.role] || 0) + 1 })
1577
+ var roleSeen = {}
1578
+ function personTitle(p) {
1579
+ var base = p.role ? roleLabel(p.role) : (p.label || String(p.id).slice(0, 8))
1580
+ if (p.role && roleCount[p.role] > 1) { var n = (roleSeen[p.role] = (roleSeen[p.role] || 0) + 1); return base + ' ' + '①②③④⑤⑥⑦⑧⑨'[n - 1 >= 0 && n - 1 < 9 ? n - 1 : 8] }
1581
+ return base
1582
+ }
1583
+ // 绑定:委托模块内纯函数 bindTaskFor(可单测 + 变异验证)。
1584
+ // 推断命中的 id 登记到 inferredBind ⇒ UI 用「~」前缀明确标注是推断,不冒充精确。
1585
+ var inferredBind = {}
1586
+ function taskOf(p) { return bindTaskFor(p, tasks, inferredBind) }
1587
+ function miniCard(p, opts) {
1588
+ var tk = taskOf(p)
1589
+ var running = p.activity === 'running'
1590
+ var done = !running && p.activity === 'inactive'
1591
+ var rw = tk && (tk.status === 'rework' || tk.verdict === 'needs_revision')
1592
+ var fl = feedLine(p.id)
1593
+ return h('div', {
1594
+ className: 'etv-card' + (running ? ' run' : (rw ? ' rework' : (done ? ' done' : ''))),
1595
+ key: 'p-' + p.id,
1596
+ title: personTitle(p) + ' · ' + String(p.id).slice(0, 8) + (tk ? ' · ' + (inferredBind[String(tk.id)] ? '~' : '') + String(tk.id || '') + (inferredBind[String(tk.id)] ? t('(按角色推断的绑定:TASKS.json 未记录 agentId)', ' (inferred by role: no agentId in TASKS.json)') : '') : ''),
1597
+ onClick: function (e) { if (e && e.stopPropagation) e.stopPropagation(); try { requestFocus({ kind: 'agent', agentId: p.id, label: p.label || personTitle(p) }); focusPanelRight() } catch (e2) {} }
1598
+ },
1599
+ rw ? h('span', { className: 'etv-rework-tag' }, '↺R' + (tk.round || 1)) : null,
1600
+ h('div', { className: 'etv-card-h' },
1601
+ h('span', { className: 'exp-ava', style: { background: running ? 'var(--etv-run)' : (done ? 'var(--etv-ok)' : 'var(--etv-idle)'), fontSize: 10, width: 18, height: 18 } }, running ? '▶' : (done ? '✓' : '·')),
1602
+ h('span', { className: 'etv-role' }, esc(personTitle(p))),
1603
+ h('span', { className: 'etv-who' }, esc(String(p.id).slice(0, 8)))),
1604
+ tk ? h('div', { className: 'etv-task', onClick: function (e) { if (e && e.stopPropagation) e.stopPropagation(); try { setSelTask(tk); focusPanelRight() } catch (e2) {} } }, esc((inferredBind[String(tk.id)] ? '~' : '') + String(tk.id || '') + ' · ' + String(tk.title || tk.spec || ''))) : h('div', { className: 'etv-task', style: { color: 'var(--dsw-alias-label-tertiary)' } }, t('未绑定任务', 'no task bound')),
1605
+ h('div', { className: 'etv-card-f' },
1606
+ h('span', { className: 'etv-dot' + (running ? ' run' : (done ? '' : ' hollow')), style: running ? { background: 'var(--etv-run)' } : (done ? { background: 'var(--etv-ok)' } : {}) }),
1607
+ h('span', { style: { fontSize: 10.5, color: 'var(--dsw-alias-label-secondary)' } }, esc(fl || (running ? t('进行中', 'running') : t('已结束', 'finished')))),
1608
+ h('span', { className: 'etv-dur' }, esc(durLabel(p)))))
1609
+ }
1610
+ var body = []
1611
+ // 顶部说明 + 控件
1612
+ var degradeNote = null
1613
+ if (!people.length && roles.length) degradeNote = t('⚠ 尚未派工:以下为计划编制,不代表已上场', '⚠ not dispatched yet: planned roster only')
1614
+ else if (!people.length) degradeNote = t('还没有可展示的人员流转(无任务、无子代理)', 'nothing to show yet (no tasks, no subagents)')
1615
+ else if (!W.waves.length) degradeNote = t('⚠ 无创建时间记录,无法分批(已按角色分组展示)', '⚠ no createdAt records — cannot batch (grouped by role)')
1616
+ else if (W.waves.length === 1) degradeNote = t('仅检测到 1 个批次(无先后可分)', 'only one batch detected (no ordering)')
1617
+ else if (!wfPh.length) degradeNote = null
1618
+ body.push(h('div', { className: 'etv-note' },
1619
+ h('span', null, t('任务人员流转', 'Task-person flow')),
1620
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary)' } }, esc('W1–W' + Math.max(1, W.waves.length) + ' · ' + people.length + t('人 · ', ' ppl · ') + people.filter(function (p) { return p.activity === 'running' }).length + t('在跑', ' running'))),
1621
+ h('button', { className: 'etv-wide-btn', onClick: function () { try { setWide(!wideV) } catch (e) {} } }, esc(wideV ? t('窄视图', 'narrow') : t('宽视图', 'wide'))),
1622
+ h('button', { className: 'etv-wide-btn', onClick: function () { try { var el = document.querySelector('.exp-body'); if (el) el.scrollTop = el.scrollHeight } catch (e) {} } }, esc(t('⤓ 最新一波', '⤓ latest')))))
1623
+ if (degradeNote) body.push(h('div', { className: 'etv-degrade warn' }, esc(degradeNote)))
1624
+ // 分层依据必须**如实标注**:workflow 派生的子代理没有会话 header,改用事件流真实时间戳。
1625
+ // 不说清来源,用户会以为"时间"是会话创建时间(两者含义不同)。
1626
+ if (W.waves.length && (timing.event > 0 || timing.none > 0)) {
1627
+ var srcBits = []
1628
+ if (timing.header) srcBits.push(t(timing.header + ' 人取自子会话创建时间', timing.header + ' from child-session createdAt'))
1629
+ if (timing.event) srcBits.push(t(timing.event + ' 人取自父会话事件流 agent-start 时间戳(workflow 子代理无会话 header)', timing.event + ' from parent-session agent-start timestamps (workflow children have no session header)'))
1630
+ if (timing.none) srcBits.push(t(timing.none + ' 人无任何时间记录', timing.none + ' with no time record at all'))
1631
+ body.push(h('div', { className: 'etv-degrade info' }, esc(t('分批依据:', 'Layering source: ') + srcBits.join(';'))))
1632
+ }
1633
+ // ── 主视图:SVG 树 + 贝塞尔曲线(照 dsh-agent-teams 的 live 面板形态抄)──
1634
+ // 层 = 波次(时间轴);层内 = 同批并列的人(横向并排);层间 = 从上层节点发散的曲线。
1635
+ // 卡片内容 = 头像(角色色/首字母) + 「角色 名字」 + 任务标题/label + 状态行(✓已完成/●运行中)。
1636
+ // 名字/颜色来自 host 的 enrichMembers(data.members[role].{name,color,initial}),按 agentId 反查。
1637
+ var nameById = {}
1638
+ Object.keys(members || {}).forEach(function (r) {
1639
+ var m = members[r]
1640
+ if (m && m.id) nameById[String(m.id)] = { name: m.name || '', color: m.color || '', initial: m.initial || '', role: r }
1641
+ })
1642
+ function personNameColor(p) {
1643
+ var hit = nameById[String(p.id)]
1644
+ return { name: (hit && hit.name) || '', color: (hit && hit.color) || '#5b8def', initial: (hit && hit.initial) || (p.role ? roleLabel(p.role).slice(0, 1) : String(p.id).slice(0, 1).toUpperCase()) }
1645
+ }
1646
+ // 可画宽度三源:实测容器 → 全屏画布按窗口估 → 停靠侧栏宽度偏好。
1647
+ // 「宽视图」开关终于有作用了:放宽单卡上限(否则 12 波里每波只有 2–3 人时,
1648
+ // 卡片被 176px 封顶、右侧大片空白)。
1649
+ var baseW = flowW || (viewMode ? Math.max(320, ((typeof window !== 'undefined' && window.innerWidth) || 1200) - 340) : (typeof pw === 'number' ? pw : 360))
1650
+ var avail = Math.max(240, baseW - 26)
1651
+ var widest = 1
1652
+ W.waves.forEach(function (w) { widest = Math.max(widest, w.members.length) })
1653
+ var NGAP = 12, NODE_H = 84, LEVEL_GAP = 34, ROW_TITLE = 15
1654
+ var maxNodeW = wideV ? 268 : 200
1655
+ var NODE_W = Math.max(104, Math.min(maxNodeW, Math.floor((avail - (widest - 1) * NGAP) / widest)))
1656
+ var levels = [{ title: '', nodes: [{ leader: true, id: '__leader' }] }]
1657
+ W.waves.forEach(function (w) {
1658
+ levels.push({ title: 'W' + w.index + ' · +Δ' + Math.round((w.startAt - W.base) / 1000) + 's · ' + hhmmss(w.startAt), members: w.members, nodes: w.members.map(function (p) { return p }) })
1659
+ })
1660
+ var totalW = 0
1661
+ levels.forEach(function (lv) { totalW = Math.max(totalW, lv.nodes.length * NODE_W + (lv.nodes.length - 1) * NGAP) })
1662
+ totalW = Math.max(totalW, 200)
1663
+ var pos = {}
1664
+ levels.forEach(function (lv, li) {
1665
+ var rowW = lv.nodes.length * NODE_W + (lv.nodes.length - 1) * NGAP
1666
+ var x0 = (totalW - rowW) / 2
1667
+ lv.nodes.forEach(function (n, i) { pos[(n.id || ('ln' + li + i))] = { x: x0 + i * (NODE_W + NGAP), y: li * (NODE_H + LEVEL_GAP) + ROW_TITLE, li: li } })
1668
+ })
1669
+ // 节点状态语义(全明星 → 参考图的「✅ 已完成」,但多两档真实状态):
1670
+ // running(蓝·脉冲) / done(绿✓) / **settled=false 且有 runId → 未结算(红)** / idle(灰) / rework(橙虚线)
1671
+ // 「未结算」的数据源是父会话事件流里 **agent-start 有、agent-end 没有** —— 即被中断的扇出,
1672
+ // 过去这种节点会永远显示成"进行中",正是用户问的「任务为什么会中断」在画布上的盲点。
1673
+ function statusOf(p) {
1674
+ if (!p) return { txt: t('编排中', 'orchestrating'), col: 'var(--etv-lead)', kind: 'lead' }
1675
+ if (p.activity === 'running') return { txt: t('运行中', 'running'), col: 'var(--etv-run)', kind: 'run' }
1676
+ if (p.runId && p.settled === false) return { txt: t('未结算 · 扇出被中断', 'unsettled · fan-out interrupted'), col: 'var(--etv-fail)', kind: 'fail' }
1677
+ if (p.activity === 'inactive') return { txt: t('已完成', 'done'), col: 'var(--etv-ok)', kind: 'done' }
1678
+ return { txt: t('未启动', 'idle'), col: 'var(--etv-idle)', kind: 'idle' }
1679
+ }
1680
+ // ── 依赖边:由 TASKS.json 的 dependsOn 决定,**不再"只连上一层第 0 个节点"** ──
1681
+ // 旧实现把每一层都挂到上层的第一个节点上:多父多子直接画错,且与真实依赖无关。
1682
+ var personOfTask = {}
1683
+ people.forEach(function (p) { var tk = taskOf(p); if (tk && tk.id && !personOfTask[String(tk.id)]) personOfTask[String(tk.id)] = p })
1684
+ var layerOf = {}
1685
+ levels.forEach(function (lv, li) { lv.nodes.forEach(function (n, i) { layerOf[String(n.id || ('ln' + li + i))] = li }) })
1686
+ var depEdges = [], depSkipped = 0
1687
+ ;(tasks || []).forEach(function (tk) {
1688
+ if (!tk || !tk.id) return
1689
+ var to = personOfTask[String(tk.id)]
1690
+ if (!to) return
1691
+ var deps = Array.isArray(tk.dependsOn) ? tk.dependsOn : []
1692
+ deps.forEach(function (dd) {
1693
+ var from = personOfTask[String(dd)]
1694
+ if (!from || String(from.id) === String(to.id)) return
1695
+ var lf = layerOf[String(from.id)], lt = layerOf[String(to.id)]
1696
+ if (lf === undefined || lt === undefined) return
1697
+ if (lt <= lf) { depSkipped += 1; return } // 同层/回边画不出跨层曲线:如实跳过并计数,不硬画
1698
+ depEdges.push([String(from.id), String(to.id), String(tk.id)])
1699
+ })
1700
+ })
1701
+ // ── 关键路径:任务图上从"最深"节点回溯最长链(纯 computed,不改数据) ──
1702
+ var depthOf = {}, byTaskId = {}
1703
+ ;(tasks || []).forEach(function (tk) { if (tk && tk.id) byTaskId[String(tk.id)] = tk })
1704
+ function taskDepth(id, seen) {
1705
+ id = String(id || '')
1706
+ if (!id || depthOf[id] !== undefined) return depthOf[id] || 0
1707
+ if (seen[id]) return 0
1708
+ seen[id] = 1
1709
+ var tk = byTaskId[id]
1710
+ var deps = (tk && Array.isArray(tk.dependsOn)) ? tk.dependsOn : []
1711
+ var best = 0
1712
+ deps.forEach(function (dd) { if (byTaskId[String(dd)]) best = Math.max(best, taskDepth(String(dd), seen) + 1) })
1713
+ delete seen[id]
1714
+ depthOf[id] = best
1715
+ return best
1716
+ }
1717
+ var criticalTasks = {}
1718
+ ;(function markCritical() {
1719
+ var arr = Object.keys(byTaskId)
1720
+ if (!arr.length) return
1721
+ var deepest = '', best = -1
1722
+ arr.forEach(function (id) { var d = taskDepth(id, {}); if (d > best) { best = d; deepest = id } })
1723
+ var cur = deepest, guard = 0
1724
+ while (cur && guard++ < 64) {
1725
+ criticalTasks[cur] = 1
1726
+ var tk = byTaskId[cur]
1727
+ var deps = (tk && Array.isArray(tk.dependsOn)) ? tk.dependsOn.slice() : []
1728
+ var next = ''
1729
+ deps.forEach(function (dd) { if (byTaskId[String(dd)] && (next === '' || taskDepth(String(dd), {}) > taskDepth(next, {}))) next = String(dd) })
1730
+ cur = next
1731
+ }
1732
+ })()
1733
+ function isCriticalPerson(p) { var tk = taskOf(p); return !!(tk && tk.id && criticalTasks[String(tk.id)]) }
1734
+ // ── 血缘:悬停节点时高亮它的依赖祖先/后代(双向 BFS),其余淡出 ──
1735
+ var fwd = {}, rev = {}
1736
+ depEdges.forEach(function (e) { (fwd[e[0]] = fwd[e[0]] || []).push(e[1]); (rev[e[1]] = rev[e[1]] || []).push(e[0]) })
1737
+ // 血缘高亮复用 Panel 顶部的 hover 状态(dagSvg 也用它)——**不要在这里再声明同名变量**,
1738
+ // `var` 会提升并遮蔽外层状态,导致 hover 恒为 undefined(本文件已被这个坑咬过一次)。
1739
+ var lineage = null
1740
+ if (hoverId) {
1741
+ lineage = {}; lineage[String(hoverId)] = 1
1742
+ ;[[fwd, 0], [rev, 0]].forEach(function (pair) {
1743
+ var adj = pair[0], queue = [String(hoverId)], guard = 0
1744
+ while (queue.length && guard++ < 500) {
1745
+ var cur = queue.shift()
1746
+ ;(adj[cur] || []).forEach(function (nx) { if (!lineage[nx]) { lineage[nx] = 1; queue.push(nx) } })
1747
+ }
1748
+ })
1749
+ }
1750
+ function dimmed(id) { return !!(lineage && !lineage[String(id)]) }
1751
+
1752
+ var svgEdge = [], svgNode = [], svgLabel = []
1753
+ levels.forEach(function (lv, li) {
1754
+ if (li > 0) {
1755
+ var a = pos[levels[0].nodes[0].id || 'ln00'] // 根 = Lead
1756
+ lv.nodes.forEach(function (n, i) {
1757
+ var b = pos[(n.id || ('ln' + li + i))]
1758
+ if (!a || !b) return
1759
+ var x1 = a.x + NODE_W / 2, y1 = a.y + NODE_H, x2 = b.x + NODE_W / 2, y2 = b.y
1760
+ var midY = (y1 + y2) / 2
1761
+ var dim = dimmed(n.id)
1762
+ // 委派边:lead → 每一次扇出的成员。第 1 波实线;后续波**淡虚线**(确实由 lead 发起,
1763
+ // 但为了让「依赖边」成为视觉主角而降权),并在 tooltip 里写明。
1764
+ svgEdge.push(h('path', {
1765
+ key: 'e' + li + '-' + i, className: 'etv-curve', d: 'M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2,
1766
+ style: { opacity: dim ? .05 : (li === 1 ? .55 : .18), strokeDasharray: li === 1 ? null : '3 4' }
1767
+ },
1768
+ h('title', null, t('委派:Lead → 本次扇出成员', 'delegation: lead → this fan-out member'))))
1769
+ })
1770
+ }
1771
+ if (lv.title) svgLabel.push(h('text', { key: 'lt' + li, x: 2, y: li * (NODE_H + LEVEL_GAP) + 11, 'font-size': 9.5, fill: 'var(--dsw-alias-label-tertiary)' }, esc(lv.title)))
1772
+ })
1773
+ // 依赖边(视觉主角):上游任务的人 → 下游任务的人
1774
+ depEdges.forEach(function (e, i) {
1775
+ var a = pos[e[0]], b = pos[e[1]]
1776
+ if (!a || !b) return
1777
+ var x1 = a.x + NODE_W / 2, y1 = a.y + NODE_H, x2 = b.x + NODE_W / 2, y2 = b.y
1778
+ var midY = (y1 + y2) / 2
1779
+ var hot = lineage && (lineage[e[0]] && lineage[e[1]])
1780
+ svgEdge.push(h('path', {
1781
+ key: 'd' + i, className: 'etv-dep-edge', d: 'M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2,
1782
+ style: { opacity: (lineage && !hot) ? .07 : .95, strokeWidth: hot ? 2.4 : 1.4 }
1783
+ },
1784
+ h('title', null, t('依赖:', 'dependsOn: ') + e[2] + ' ← ' + (taskOf(people.filter(function (p) { return String(p.id) === e[0] })[0]) || {}).id)))
1785
+ })
1786
+ levels.forEach(function (lv, li) {
1787
+ lv.nodes.forEach(function (n, i) {
1788
+ var b = pos[(n.id || ('ln' + li + i))]
1789
+ if (!b) return
1790
+ if (n.leader) {
1791
+ svgNode.push(h('g', { key: 'lead', transform: 'translate(' + b.x + ' ' + b.y + ')' },
1792
+ h('rect', { width: NODE_W, height: NODE_H, rx: 10, className: 'etv-g-node lead' }),
1793
+ h('circle', { cx: 20, cy: 22, r: 9, fill: 'rgba(65,118,230,.2)', stroke: 'var(--etv-lead)', strokeWidth: 1.4 }),
1794
+ h('text', { x: 20, y: 26, 'font-size': 10, 'text-anchor': 'middle', fill: 'var(--etv-lead)' }, '◎'),
1795
+ h('text', { x: 36, y: 19, 'font-size': 11.5, 'font-weight': 700, fill: 'var(--dsw-alias-label-primary)' }, 'Lead Agent'),
1796
+ h('text', { x: 36, y: 33, 'font-size': 10, fill: 'var(--dsw-alias-label-secondary)' }, esc(t('协调专家任务', 'orchestrating'))),
1797
+ h('text', { x: 12, y: 62, 'font-size': 10, fill: 'var(--dsw-alias-label-secondary)' }, esc('✓ ' + t('运行中', 'running')))))
1798
+ return
1799
+ }
1800
+ var pc = personNameColor(n)
1801
+ var st = statusOf(n)
1802
+ var tk = taskOf(n)
1803
+ var title = tk ? String(tk.title || tk.spec || '') : (String(n.label || n.wfLabel || '').trim() || feedLine(n.id) || '')
1804
+ var idLine = tk ? ((inferredBind[String(tk.id)] ? '~' : '') + String(tk.id || '')) : (String(n.id || '').slice(0, 8))
1805
+ // 参考图是「任务标题两行」:SVG 没有 line-clamp,按显示宽度手工切成两行
1806
+ var per = Math.max(10, Math.floor((NODE_W - 30) / 6.1))
1807
+ var t1 = clipText(title, per)
1808
+ var rest = String(title).slice(t1.length)
1809
+ var t2 = rest ? clipText(rest, per) : ''
1810
+ var dim2 = dimmed(n.id)
1811
+ var crit = isCriticalPerson(n)
1812
+ var fileN = (tk && Array.isArray(tk.changedPaths)) ? tk.changedPaths.length : 0
1813
+ var badges = []
1814
+ if (crit) badges.push('★' + t('关键路径', 'critical'))
1815
+ if (fileN) badges.push(fileN + t(' 文件', ' files'))
1816
+ if (tk && tk.round > 1) badges.push('R' + tk.round)
1817
+ if (n.runId) badges.push(t('扇出 ', 'fan ') + String(n.runId).slice(0, 4))
1818
+ // tooltip 先拼好再传:直接内联一个超长三元表达式极易漏括号(本轮真踩过 SyntaxError)
1819
+ var nodeTip = (roleLabel(n.role) || '') + ' ' + (pc.name || '') + ' · ' + String(n.id).slice(0, 8) + ' · ' + st.txt
1820
+ if (n.runId) nodeTip += ' · ' + t('扇出 ', 'fan ') + String(n.runId).slice(0, 8) + (n.settled === false ? t('(未结算)', ' (unsettled)') : '')
1821
+ if (tk) nodeTip += ' · ' + String(tk.id || '') + ' ' + String(tk.title || '')
1822
+ if (crit) nodeTip += ' · ' + t('关键路径', 'critical path')
1823
+ // 子元素先组成数组再一次性传入:深度嵌套的 h(...) 极易漏括号(本轮真踩过 SyntaxError)
1824
+ var kids = [
1825
+ h('rect', { width: NODE_W, height: NODE_H, rx: 10, className: 'etv-g-node' + (st.kind === 'run' ? ' run' : (st.kind === 'fail' ? ' failed' : '')) + (crit ? ' crit' : ''), strokeWidth: crit ? 2 : undefined }),
1826
+ h('circle', { cx: 20, cy: 22, r: 9, fill: pc.color, opacity: .9 }),
1827
+ h('text', { x: 20, y: 26, 'font-size': 10, 'text-anchor': 'middle', fill: '#fff' }, esc(String(pc.initial || '?').slice(0, 1))),
1828
+ h('text', { x: 34, y: 19, 'font-size': 11, 'font-weight': 600, fill: 'var(--dsw-alias-label-primary)' }, esc((roleLabel(n.role) || t('未识别角色', 'unknown')) + (pc.name ? ' ' + pc.name : ''))),
1829
+ h('text', { x: 34, y: 33, 'font-size': 9.5, fill: 'var(--dsw-alias-label-tertiary)' }, esc(idLine)),
1830
+ h('text', { x: 12, y: 50, 'font-size': 10.5, fill: 'var(--dsw-alias-label-primary)' }, esc(t1)),
1831
+ t2 ? h('text', { x: 12, y: 63, 'font-size': 10.5, fill: 'var(--dsw-alias-label-primary)' }, esc(t2)) : null,
1832
+ h('circle', { cx: 16, cy: 74, r: 3.5, fill: st.col }),
1833
+ h('text', { x: 24, y: 77, 'font-size': 10, fill: 'var(--dsw-alias-label-secondary)' }, esc(st.txt + (durLabel(n) ? ' · ' + durLabel(n) : ''))),
1834
+ badges.length ? h('text', { x: NODE_W - 8, y: 77, 'font-size': 9, 'text-anchor': 'end', fill: 'var(--dsw-alias-label-tertiary)' }, esc(badges.join(' · '))) : null,
1835
+ h('title', null, esc(nodeTip)),
1836
+ ]
1837
+ svgNode.push(h('g', {
1838
+ key: 'n' + n.id,
1839
+ transform: 'translate(' + b.x + ' ' + b.y + ')',
1840
+ style: { cursor: 'pointer', opacity: dim2 ? .22 : 1 },
1841
+ onMouseEnter: function () { try { setHover(String(n.id)) } catch (e) {} },
1842
+ onMouseLeave: function () { try { setHover(null) } catch (e) {} },
1843
+ onClick: function () { try { requestFocus({ kind: 'agent', agentId: n.id, label: n.label || n.wfLabel || '' }); focusPanelRight() } catch (e) {} }
1844
+ }, kids))
1845
+ })
1846
+ })
1847
+ body.push(h('div', { className: 'etv-graph' },
1848
+ h('svg', { width: totalW, height: levels.length * (NODE_H + LEVEL_GAP) + 6, style: { display: 'block', maxWidth: 'none' } },
1849
+ h('g', null, svgEdge), h('g', null, svgLabel), h('g', null, svgNode))))
1850
+ if (depSkipped) body.push(h('div', { className: 'etv-degrade info' }, esc(t(depSkipped + ' 条依赖跨不了层(上游与下游在同一波,画曲线会误导)—— 已如实省略', depSkipped + ' dependency edge(s) cannot cross layers (both ends in one wave) — omitted rather than misdrawn'))))
1851
+
1852
+ if (W.waves.length === 1) body.push(h('div', { className: 'etv-degrade info' }, esc(t('仅检测到 1 个批次(无先后可分)', 'only one batch (no ordering)'))))
1853
+
1854
+ // 时刻未知(部分缺失)
1855
+ if (W.untimed.length) {
1856
+ body.push(h('div', { className: 'etv-group', key: 'untimed' },
1857
+ h('div', { className: 'etv-degrade info' }, t('⚠ ' + W.untimed.length + ' 人无创建时间(不参与分批,按派工顺序串成一条链)', '⚠ ' + W.untimed.length + ' without createdAt (chained in dispatch order)')),
1858
+ // SQL 单列 + 左脊 + 肘形连线:无时间戳不能分批,但按派工顺序串链比密排两列好读(用户要求"要有线连接")
1859
+ h('div', { className: 'etv-list' }, h('div', { className: 'etv-list-i' }),
1860
+ W.untimed.map(function (p) { return h('div', { className: 'etv-row', key: 'ur-' + p.id }, miniCard(p)) }))))
1861
+ }
1862
+ // 计划编制(未派工)
1863
+ if (!people.length && roles.length) {
1864
+ var planRows = roles.map(function (r) {
1865
+ return h('div', { className: 'etv-row', key: 'pl-' + r },
1866
+ h('div', { className: 'etv-card', style: { borderStyle: 'dashed', opacity: 0.75, cursor: 'default' } },
1867
+ h('div', { className: 'etv-card-h' }, h('span', { className: 'etv-role' }, esc(roleLabel(r))))))
1868
+ })
1869
+ body.push(h('div', { className: 'etv-group', key: 'plan' },
1870
+ h('div', { className: 'etv-degrade info' }, t('计划编制(未派工)', 'Planned roster (not dispatched)')),
1871
+ h('div', { className: 'etv-list' }, h('div', { className: 'etv-list-i' }), planRows)))
1872
+ }
1873
+ // 未绑定任务(真实 TASKS.json 无 agentId → 一律进这里,不臆造绑定)
1874
+ var boundIds = {}
1875
+ people.forEach(function (p) { var tk = taskOf(p); if (tk) boundIds[String(tk.id)] = 1 })
1876
+ var unbound = tasks.filter(function (tk) { return tk && !boundIds[String(tk.id)] })
1877
+ if (unbound.length) {
1878
+ var ubRows = unbound.slice(0, 40).map(function (tk) {
1879
+ return h('div', { className: 'etv-row', key: 'ub-' + tk.id },
1880
+ h('div', { className: 'etv-card tiny', onClick: function () { try { setSelTask(tk); focusPanelRight() } catch (e) {} } },
1881
+ h('div', { className: 'etv-card-h' },
1882
+ h('span', { className: 'etv-role' }, esc(String(tk.id || '') + ' · ' + String(tk.title || '').slice(0, 18))),
1883
+ h('span', { className: 'etv-who' }, esc(roleLabel(tk.owner) || '')))))
1884
+ })
1885
+ body.push(h('div', { className: 'etv-group', key: 'unbound' },
1886
+ h('div', { className: 'etv-degrade info', onClick: function () { setUnboundOpen(!unboundOpen) }, style: { cursor: 'pointer' } },
1887
+ t('⚠ 这些任务未与子代理建立绑定(TASKS.json 无 agentId 字段)· ', '⚠ tasks unbound to subagents (no agentId in TASKS.json) · ') + unbound.length + (unboundOpen ? ' ▾' : ' ▸')),
1888
+ unboundOpen ? h('div', { className: 'etv-list' }, h('div', { className: 'etv-list-i' }), ubRows) : null))
1889
+ }
1890
+ flowDiagram = h('div', { className: 'etv-root' }, body)
1891
+ })()
1892
+
1893
+ var panorama = null
1894
+ if (tasks.length) {
1895
+ var rows = flowRows(tasks)
1896
+ panorama = h('div', null,
1897
+ h('div', { className: 'exp-flow-note' }, t('已安排任务(一行=依赖顺序 →;换行=并行分支)', 'Assigned tasks (a row = dependency order →; line break = parallel branch)')),
1898
+ rows.map(function (row, i) {
1899
+ var cells = []
1900
+ row.forEach(function (tk, j) {
1901
+ if (j > 0) cells.push(h('span', { key: 'a' + j, className: 'exp-flow-a2' }, '→'))
1902
+ cells.push(flowCards(tk))
1903
+ })
1904
+ return h('div', { key: i },
1905
+ h('div', { className: 'exp-flow-row' }, cells),
1906
+ i < rows.length - 1 ? h('div', { className: 'exp-flow-sep' }) : null)
1907
+ }))
1908
+ } else if (roles.length) {
1909
+ panorama = h('div', { className: 'exp-empty' }, t('(暂无任务——设计阶段分工后这里显示任务流转链)', '(no tasks — the flow appears after design)'))
1910
+ }
1911
+ // 编制摘要(身份标记保留):横向一张卡,带状态/当前任务
1912
+ var rosterStrip = roles.length ? h('div', { className: 'exp-roster-strip' }, roles.map(function (r) {
1913
+ var m = members[r], live = (m && typeof m === 'object') ? m : null
1914
+ var nm = live ? (live.name || r) : r, col = live ? (live.color || '#5b8def') : '#9aa4b2', ini = live ? (live.initial || nm.slice(0, 1).toUpperCase()) : nm.slice(0, 1).toUpperCase()
1915
+ var hasAgent = !!(live && live.active)
1916
+ var act = live ? (live.activity || '') : '', st = act === 'running' ? 'running' : (act === 'idle' || act === 'ready' || act === 'inactive') ? 'idle' : ''
1917
+ var lbl = !hasAgent ? t('未启动', 'not started') : (st === 'running' ? t('运行中', 'running') : st === 'idle' ? t('空闲', 'idle') : t('已派', 'dispatched'))
1918
+ return h('span', { className: 'exp-roster-chip' + (st === 'running' ? ' exp-mem-run' : ''), key: r, onClick: function () { setFeedRole(feedRole === r ? null : r) }, title: t('点击查看实时操作流', 'click for live feed') },
1919
+ h('span', { className: 'exp-ava', style: { background: col } }, esc(ini)),
1920
+ esc(nm.slice(0, 6)) + ' ' + esc(roleLabel(r)) + ' · ' + esc(lbl))
1921
+ })) : null
1922
+
1923
+ // 覆盖矩阵:只渲染**形状合规**的条目。历史事故:`coverage` 曾被写成
1924
+ // `["pm","architect",…]`(角色名字符串数组),而 `c.constraint || ''` 对字符串取不到
1925
+ // 属性 ⇒ 渲染出**一串空行**,用户只看到空白却不知为何("消费端静默吞")。
1926
+ // 这里显式过滤 + 把被丢弃的条数如实说出来(信息不丢,也不静默)。
1927
+ var coverageArr = Array.isArray(coverage) ? coverage : []
1928
+ var coverageBad = coverageArr.filter(function (c) { return !c || typeof c !== 'object' || Array.isArray(c) }).length
1929
+ var coverageRows = coverageArr
1930
+ .filter(function (c) { return c && typeof c === 'object' && !Array.isArray(c) })
1931
+ .map(function (c, i) { return h('div', { className: 'exp-row', key: i }, h('span', { style: { flex: 1 } }, esc(c.constraint || '')), h('span', { className: 'exp-muted' }, esc(arrOf(c.tasks).join(', ')))) })
1932
+ if (coverageBad) coverageRows = coverageRows.concat([h('div', { className: 'exp-legend', key: 'cov-bad' }, t('⚠ 另有 ' + coverageBad + ' 条覆盖率记录形状不合规(应为 {constraint, tasks})—— 已跳过渲染,未静默吞掉;跑 /team migrate 可修', '⚠ ' + coverageBad + ' coverage record(s) have an invalid shape (expected {constraint, tasks}) — skipped, not silently swallowed; /team migrate can fix it'))])
1933
+ var artifactBar = ARTIFACT.map(function (a) { return h('span', { className: 'exp-art', key: a, onClick: function () { openArtifact(a, runId, workspace) } }, esc(a)) })
1934
+ var runsAll = (data && data.runs) || runsMeta
1935
+ // 归属标记(多会话协作):别的会话创建的 run 在下拉里标 👥,本/未知会话不加(避免噪音)
1936
+ var runSelector = runsAll.map(function (r) { var v = r.workspace + '\u0001' + r.runId; var ownerTag = (r.ownerSession && r.ownerSession !== sessionId) ? ' 👥' : ''; return h('option', { key: v, value: v }, esc(r.runId) + ownerTag + ' (' + esc(phaseLabel(r.phase)) + (r.workspace ? ' · ' + esc(basename(r.workspace) || r.workspace) : '') + ')') })
1937
+ var curSel = (workspace && runId) ? workspace + '\u0001' + runId : ''
1938
+ var selNode = runSelector.length ? h('select', { className: 'exp-art', value: curSel, onChange: function (e) { var p = e.target.value.split('\u0001'); setSelTask(null); setSelArt(null); setSelRun({ workspace: p[0], runId: p[1] }) } },
1939
+ (!curSel ? h('option', { key: '_', value: '', disabled: true }, t('选择 run…', 'Pick a run…')) : null), runSelector) : null
1940
+
1941
+ if (!isOpen && !viewMode) return null
1942
+ var panelCls = viewMode ? 'exp-panel exp-canvas' : ('exp-panel' + (docked ? '' : ' exp-float'))
1943
+ var panelStyle = viewMode ? null : (docked ? { width: pw + 'px' } : { left: pos.x, top: pos.y, width: pw + 'px' })
1944
+ var headCls = 'exp-head' + (docked || viewMode ? '' : ' exp-grab')
1945
+
1946
+ try {
1947
+ var TAB_ZH = { team: '人', tasks: '事', info: '料', board: '盘', settings: '设' }
1948
+ var tabBar = h('div', { className: 'exp-tabs' },
1949
+ ['team', 'tasks', 'info', 'board', 'settings'].map(function (tb) {
1950
+ return h('span', { key: tb, className: 'exp-tab' + (tab === tb ? ' on' : ''), onClick: function () { setTab(tb) } },
1951
+ esc((langNow === 'en' ? TAB_ZH[tb] : TAB_ZH[tb])) + (tb === 'tasks' ? ' ' + tasks.length : ''))
1952
+ }),
1953
+ violations.length ? h('span', { className: 'exp-viol-badge', onClick: function () { setTab('info') }, title: violations.join('\n') }, '⚠ ' + violations.length) : null)
1954
+
1955
+ // 「人」属于 run 的**归属会话**,不是发起请求的会话。host 已按归属解析;这里把
1956
+ // 解析不确定的情况**如实说出来**(不确定就说不确定,不默默把两个 run 的人混在一起)。
1957
+ var peopleNote = (data && typeof data.peopleSessionNote === 'string') ? data.peopleSessionNote : ''
1958
+ var peopleNoteBlock = peopleNote ? h('div', { className: 'exp-viol-list', key: 'peoplenote' },
1959
+ h('div', { style: { color: '#b3291e' } }, esc('⚠ ' + peopleNote))) : null
1960
+
1961
+ // 多会话协作(优化①):这个 run 属于别的会话时,顶部加一条**明确可见**的归属横幅 ——
1962
+ // "你在看别人的 run"。避免两个会话同时指挥同一个 run 时互相覆盖却毫无感知。
1963
+ var isOtherSessionRun = !!(data && data.stateOwnerSession && data.stateOwnerSession !== sessionId)
1964
+ var ownerBanner = isOtherSessionRun ? h('div', { className: 'exp-viol-list', key: 'owner-banner' },
1965
+ h('div', { style: { color: '#6a4a00', background: '#fff8e6', padding: '6px 10px', borderRadius: 6 } },
1966
+ esc('👥 这个 run 由**另一个会话**创建并拥有(' + data.stateOwnerSession.slice(0, 18) + '…)。你可以查看,但直接派工会与它竞争同一份工作区 —— 建议先与它协调,或确认它已结束后用 /team resume 接管。'))) : null
1967
+
1968
+ // 「任务为什么会中断」——host 从本会话事件流读出每次 workflow 扇出的命运:
1969
+ // 没有 run-end 的那次,其编排工具调用从未返回(回合被新消息打断,或进程退出),
1970
+ // 于是成员会永远停在「进行中」。没有卡片的运行也能在这里显示(卡片根本不存在)。
1971
+ var wfRuns = (data && Array.isArray(data.wfRuns)) ? data.wfRuns : []
1972
+ // 只报「没正常收尾」的:正在跑的扇出(running/unsettled)是常态,不该刷红条。
1973
+ var badRuns = wfRuns.filter(function (r) { return r && (r.status === 'interrupted' || r.status === 'failed') })
1974
+ var wfRunBlock = badRuns.length ? h('div', { className: 'exp-viol-list', key: 'wfruns' },
1975
+ h('div', { key: 'h', style: { fontWeight: 600 } }, esc(t('⚠ ' + badRuns.length + ' 次 workflow 扇出未正常结算', '⚠ ' + badRuns.length + ' workflow fan-out(s) did not settle cleanly'))),
1976
+ badRuns.slice(0, 4).map(function (r, i) {
1977
+ var why = r.status === 'interrupted'
1978
+ ? t('已中断——编排工具调用未返回(回合被新消息打断,或进程退出)', 'interrupted — the orchestrating tool call never returned (turn interrupted by a new message, or the process exited)')
1979
+ : t('失败——stopReason=error', 'failed — stopReason=error')
1980
+ return h('div', { key: 'wfr' + i },
1981
+ esc(String(r.name || r.runId || '').slice(0, 40) + ' · ' + r.started + t(' 派工 / ', ' dispatched / ') + r.unsettled + t(' 未结算 · ', ' unsettled · ') + why))
1982
+ })) : null
1983
+
1984
+ var teamTab = h('div', null,
1985
+ h('div', { className: 'exp-sec' }, t('团队编制(已派 ', 'Roster (') + dispatched + t(' / ', '/') + roles.length + t(' 个 agent)', ' agents)')),
1986
+ ownerBanner,
1987
+ wfRunBlock,
1988
+ peopleNoteBlock,
1989
+ roster.length ? roster : h('div', { className: 'exp-empty' }, t('(暂无成员)', '(no members)')),
1990
+ // 真实性提示:活子代理里没被名册认领的分两类,必须分开说 ——
1991
+ // ① 同角色的重复派工/历史 leg(**有角色**,只是每个角色只展示 1 个成员)
1992
+ // ② 真的解析不出角色的(label 为空、子会话日志不可读、事件流里也没有派工 label)
1993
+ // 把 ① 说成"未能解析出角色"是错的(本会话 30 个子代理 / 14 个角色,① 必然很多)。
1994
+ unmatchedAgents.length ? (function () {
1995
+ var extraKeys = Object.keys(extraByRole)
1996
+ var segs = []
1997
+ if (extraKeys.length) {
1998
+ segs.push(t('另有 ' + unmatchedAgents.length + ' 个活子代理是同一角色的重复派工/历史 leg(每个角色只展示 1 个成员),角色已知:',
1999
+ unmatchedAgents.length + ' live subagent(s) are repeat/historical legs of a role already shown (one member per role); roles known: ') +
2000
+ extraKeys.map(function (r) { return roleLabel(r) + '×' + extraByRole[r] }).join(' '))
2001
+ }
2002
+ if (rolelessAgents.length) {
2003
+ segs.push(t((extraKeys.length ? '其中 ' : '另有 ') + rolelessAgents.length + ' 个确实解析不出角色(label 为空、子会话日志不可读、本会话事件流里也没有其 workflow 派工 label):',
2004
+ (extraKeys.length ? 'of them, ' : '') + rolelessAgents.length + ' truly have no resolvable role (empty label, unreadable child log, and no workflow delegation label in this session log): ') +
2005
+ rolelessAgents.slice(0, 6).map(function (a) { return String(a.id || '').slice(0, 8) + (a.model ? '·' + a.model : '') }).join(' '))
2006
+ }
2007
+ return h('div', { className: 'exp-legend', key: 'unmatched' }, segs.join(' '))
2008
+ })() : null,
2009
+ // 可逆开关:本地可回退到 dsh 原生 workflow 卡(改动只影响"对话流卡片的点击去向")
2010
+ h('div', { className: 'exp-legend', key: 'wf-flag' },
2011
+ h('span', null, t('对话流 workflow 卡:', 'Conversation workflow card: ')),
2012
+ h('button', {
2013
+ className: 'exp-opt',
2014
+ onClick: function () { try { var cur = window.localStorage.getItem('et-native-workflow') === '1'; window.localStorage.setItem('et-native-workflow', cur ? '0' : '1'); notify() } catch (e) {} }
2015
+ }, esc(t('当前:', 'now: ') + (nativeWf ? t('dsh 原生卡', 'dsh native') : t('专家团卡', 'expert-team')) + t(' → 点击切换', ' → click to switch'))),
2016
+ h('span', { className: 'exp-muted' }, t('(切换后刷新页面生效)', '(refresh to apply)'))),
2017
+ feedRole ? (function () {
2018
+ var fm = members[feedRole], fid = (fm && fm.id) || ''
2019
+ var fe = (data && data.feed && fid && data.feed[fid]) || []
2020
+ return h('div', { className: 'exp-feed', key: 'feed' + feedRole },
2021
+ h('div', { className: 'exp-feed-h' },
2022
+ h('span', null, esc((fm && fm.name ? fm.name + ' · ' : '') + roleLabel(feedRole) + ' · ' + t('实时操作流', 'live feed'))),
2023
+ h('button', { className: 'exp-feed-x', onClick: function () { setFeedRole(null) } }, '✕')),
2024
+ fe.length ? fe.map(function (en, i) {
2025
+ var tm = ''
2026
+ try { var d2 = new Date(en.ts); tm = ('0' + d2.getHours()).slice(-2) + ':' + ('0' + d2.getMinutes()).slice(-2) + ':' + ('0' + d2.getSeconds()).slice(-2) } catch (e) {}
2027
+ return h('div', { className: 'exp-feed-i', key: i },
2028
+ h('span', { className: 'exp-feed-t' }, esc(tm)),
2029
+ h('span', { className: 'exp-feed-l' }, esc(en.line || '')),
2030
+ en.preview ? h('div', { className: 'exp-feed-p' }, esc(en.preview)) : null)
2031
+ }) : h('div', { className: 'exp-empty' }, t('(暂无操作记录——成员执行工具即显示)', '(no activity yet — appears as tools run)')))
2032
+ }()) : null,
2033
+ // 按 agentId 的实时细节流(对话流 workflow 卡点成员 → 右侧看"正在进行的细节记录")
2034
+ feedAgent ? (function () {
2035
+ var fid2 = String(feedAgent.id || '')
2036
+ var fe2 = (data && data.feed && fid2 && data.feed[fid2]) || []
2037
+ var liveA = ((data && data.agents) || []).filter(function (a) { return a && String(a.id) === fid2 })[0] || {}
2038
+ return h('div', { className: 'exp-feed', key: 'feedA' + fid2 },
2039
+ h('div', { className: 'exp-feed-h' },
2040
+ h('span', null, esc((feedAgent.label || fid2.slice(0, 8)) + ' · ' + t('实时细节记录', 'live detail') + (liveA.mode ? ' · ' + liveA.mode : '') + (liveA.model ? ' · ' + liveA.model : ''))),
2041
+ h('button', { className: 'exp-feed-x', onClick: function () { setFeedAgent(null) } }, '✕')),
2042
+ fe2.length ? fe2.map(function (en, i) {
2043
+ var tm2 = ''
2044
+ try { var d3 = new Date(en.ts); tm2 = ('0' + d3.getHours()).slice(-2) + ':' + ('0' + d3.getMinutes()).slice(-2) + ':' + ('0' + d3.getSeconds()).slice(-2) } catch (e) {}
2045
+ return h('div', { className: 'exp-feed-i', key: i },
2046
+ h('span', { className: 'exp-feed-t' }, esc(tm2)),
2047
+ h('span', { className: 'exp-feed-l' }, esc(en.line || '')),
2048
+ en.preview ? h('div', { className: 'exp-feed-p' }, esc(en.preview)) : null)
2049
+ }) : h('div', { className: 'exp-empty' }, t('(该成员暂无操作记录——它执行工具即显示)', '(no activity for this member yet)')))
2050
+ }()) : null,
2051
+ rosterStrip ? [h('div', { className: 'exp-sec' }, t('编制摘要', 'Roster')), rosterStrip] : null)
2052
+
2053
+ // ── F1 计划草稿编辑器(暂存 → 可编辑 → Approve & Run / Discard)──
2054
+ var ALL_ROLES = ['pm', 'architect', 'researcher', 'ui', 'backend', 'frontend', 'dba', 'sec', 'reviewer', 'qa', 'devops', 'docs']
2055
+ var planEditor = null
2056
+ if (draftV) {
2057
+ var dv = draftV
2058
+ planEditor = h('div', { className: 'exp-plan' },
2059
+ h('div', { className: 'exp-plan-h' },
2060
+ h('span', { className: 'exp-plan-t' }, t('📋 计划草稿(可编辑)', '📋 Plan draft (editable)')),
2061
+ h('span', { className: 'exp-muted' }, (dv.tasks || []).length + ' ' + t('任务', 'tasks') + ' · ' + (dv.roles || []).length + ' ' + t('角色', 'roles')),
2062
+ planMsg ? h('span', { className: 'exp-plan-msg' }, esc(planMsg)) : null),
2063
+ h('div', { className: 'exp-sec', style: { marginTop: 6 } }, t('角色编制', 'Roster')),
2064
+ h('div', { className: 'exp-plan-roles' },
2065
+ (dv.roles || []).map(function (r) {
2066
+ return h('span', { className: 'exp-plan-chip', key: r }, esc(roleLabel(r)),
2067
+ h('b', { className: 'exp-plan-x', title: t('移除', 'remove'), onClick: function () { draftEdit(function (d) { d.roles = (d.roles || []).filter(function (x) { return x !== r }) }) } }, '✕'))
2068
+ }),
2069
+ h('select', { className: 'exp-plan-add', value: '', onChange: function (e) {
2070
+ var v = e.target.value
2071
+ if (v) draftEdit(function (d) { d.roles = (d.roles || []).concat([v]) })
2072
+ e.target.value = ''
2073
+ } },
2074
+ h('option', { value: '' }, t('+ 添加角色', '+ add role')),
2075
+ ALL_ROLES.filter(function (r) { return (dv.roles || []).indexOf(r) < 0 }).map(function (r) { return h('option', { key: r, value: r }, roleLabel(r) + ' · ' + r) }))),
2076
+ h('div', { className: 'exp-sec', style: { marginTop: 10 } }, t('任务与依赖', 'Tasks & deps')),
2077
+ (dv.tasks || []).map(function (tk, i) {
2078
+ return h('div', { className: 'exp-plan-row', key: i },
2079
+ h('span', { className: 'exp-plan-id' }, esc(tk.id)),
2080
+ h('input', { className: 'exp-plan-in', value: tk.title || '', placeholder: t('任务标题', 'task title'),
2081
+ onChange: function (e) { var v = e.target.value; draftEdit(function (d) { d.tasks[i].title = v }) } }),
2082
+ h('select', { className: 'exp-plan-sel', value: tk.owner || '', onChange: function (e) { var v = e.target.value; draftEdit(function (d) { d.tasks[i].owner = v }) } },
2083
+ (dv.roles || []).map(function (r) { return h('option', { key: r, value: r }, roleLabel(r)) })),
2084
+ h('input', { className: 'exp-plan-dep', value: (tk.dependsOn || []).join(','), placeholder: t('依赖', 'deps'), title: t('依赖任务 id,逗号分隔', 'dependsOn ids, comma separated'),
2085
+ onChange: function (e) { var v = e.target.value; draftEdit(function (d) { d.tasks[i].dependsOn = v.split(',').map(function (s) { return s.trim() }).filter(Boolean) }) } }),
2086
+ h('b', { className: 'exp-plan-x', title: t('删除任务', 'delete task'), onClick: function () { draftEdit(function (d) { d.tasks.splice(i, 1) }) } }, '✕'))
2087
+ }),
2088
+ h('div', { className: 'exp-plan-btns' },
2089
+ h('button', { className: 'exp-plan-b', onClick: function () {
2090
+ draftEdit(function (d) { d.tasks = (d.tasks || []).concat([{ id: 'T-' + String((d.tasks || []).length + 1).padStart(2, '0'), kind: 'implementation', owner: (d.roles || [])[0] || 'frontend', title: '', spec: '', acceptance: [], inScope: [], verify: [], dependsOn: [] }]) })
2091
+ } }, t('+ 任务', '+ task')),
2092
+ h('button', { className: 'exp-plan-b', onClick: function () { planPost('', { draft: draftV }) } }, t('💾 保存草稿', '💾 Save')),
2093
+ h('button', { className: 'exp-plan-b go', onClick: function () { planPost('/approve', {}) } }, t('✅ 批准并运行', '✅ Approve & Run')),
2094
+ h('button', { className: 'exp-plan-b bad', onClick: function () { planPost('/discard', {}) } }, t('🗑 丢弃', '🗑 Discard'))))
2095
+ } else if (data && data.planStatus === 'discarded') {
2096
+ planEditor = h('div', { className: 'exp-plan-discarded' },
2097
+ t('🗑 计划已丢弃,禁止自动重建(如需重来请明确要求)', '🗑 Plan discarded — auto-recreate blocked'))
2098
+ }
2099
+
2100
+ var tasksTab = h('div', null,
2101
+ planEditor,
2102
+ h('div', { className: 'exp-sec exp-sec-col', onClick: function () { setListOpen(!listOpen) }, title: t('点击展开/收起', 'click to expand/collapse') },
2103
+ h('span', { className: 'exp-chev' }, listOpen ? '\u25be' : '\u25b8'),
2104
+ h('span', null, t('任务清单(点看详情)', 'Tasks (click for detail)') + ' (' + tasks.length + ')' + (usingLiveTasks ? t(' · 实时视图', ' · live') : '')),
2105
+ !listOpen && listSummary ? h('span', { className: 'exp-muted', style: { fontWeight: 400 } }, esc(listSummary)) : null),
2106
+ listOpen ? (taskRows.length ? taskRows : h('div', { className: 'exp-empty' }, t('(暂无任务)', '(no tasks)'))) : null,
2107
+ // 诚实标注:这些行不是 TASKS.json,而是从活子代理投影出来的只读视图。
2108
+ usingLiveTasks ? h('div', { className: 'exp-legend', key: 'live-note' },
2109
+ t('⚠ TASKS.json 为空(本轮未按协议回写任务)——以下 ' + tasks.length + ' 行为**实时子代理投影**:角色由各子代理的 prompt 推断,状态取活动态,依赖关系无从得知。', '⚠ TASKS.json is empty — the rows below are a LIVE projection of subagents (role inferred from each prompt; no dependency data).')) : null,
2110
+ h('div', { className: 'exp-sec' }, t('团队视图', 'View'),
2111
+ h('span', { className: 'exp-toggle', onClick: function () { setView('dag') }, style: viewV === 'dag' ? { borderColor: '#0969da', color: '#0969da' } : null }, usingLiveTasks ? t('按角色分组', 'By role') : t('任务依赖图', 'Task DAG')),
2112
+ h('span', { className: 'exp-toggle', onClick: function () { setView('panorama') }, style: viewV === 'panorama' ? { borderColor: '#0969da', color: '#0969da' } : null }, t('任务人员流转', 'Task-person flow'))),
2113
+ usingLiveTasks && viewV === 'dag'
2114
+ ? liveGroups
2115
+ : viewV === 'dag'
2116
+ ? h('div', null,
2117
+ h('div', { className: 'exp-legend' }, (function () {
2118
+ var counts = dagStatusCounts(tasks)
2119
+ return dagStatusOrder().map(function (s) {
2120
+ var m = dagStatusMeta(s)
2121
+ var n = counts[s] || 0
2122
+ var cls = 'exp-legend-item' + (m.active && n ? ' live' : '') + (n ? '' : ' zero')
2123
+ return h('span', { key: s, className: cls, title: m.label + ':' + n + ' 项' }, h('i', { style: { background: m.color } }), esc(m.label), n ? h('b', null, ' ' + n) : null)
2124
+ })
2125
+ })()),
2126
+ dagSvg(tasks, function (t) { var same = selTaskV && selTaskV.id === t.id; setSelTask(same ? null : t); if (!same) focusPanelRight() }, selTaskV && selTaskV.id, hoverId, setHover))
2127
+ : h('div', null, flowDiagram || panorama),
2128
+ selTaskV ? h(TaskDetail, { task: selTaskV, member: members[selTaskV.owner], current: tasks.filter(function (t2) { return t2.owner === selTaskV.owner && ['in_progress', 'claimed', 'rework'].indexOf(t2.status) >= 0 })[0], files: (data && data.files) || [], logTail: (data && data.logTail) || '', onOp: taskOp }) : null)
2129
+
2130
+ var infoTab = h('div', null,
2131
+ h('div', { className: 'exp-sec' }, t('质量门禁', 'Quality gates')),
2132
+ violations.length
2133
+ ? h('div', { className: 'exp-viol-list' }, violations.map(function (v, i) { return h('div', { key: i }, '❌ ' + esc(v)) }))
2134
+ : h('div', { className: 'exp-empty' }, t('✅ 无违规:verify/verdict/归属/依赖门一致', '✅ clean: verify/verdict/ownership/deps OK')),
2135
+ (data && data.logTail) ? [h('div', { className: 'exp-sec' }, t('实时日志(最新)', 'Run log (latest)')), h('pre', { className: 'exp-log' }, esc(data.logTail))] : null,
2136
+ h('div', { className: 'exp-sec' }, t('覆盖率(用户约束 → 任务)', 'Coverage (constraint → tasks)')),
2137
+ coverageRows.length ? coverageRows : h('div', { className: 'exp-empty' }, t('(design 阶段填)', '(set in design)')),
2138
+ h('div', { className: 'exp-sec' }, t('工件(点击预览)', 'Artifacts (click to preview)')),
2139
+ h('div', {}, artifactBar),
2140
+ selArtV ? h('div', { className: 'exp-preview' }, esc(artTextV || t('加载中…', 'Loading…'))) : null,
2141
+ h('div', { className: 'exp-sec' }, t('代码索引(Enter 搜索)', 'Code index (Enter to search)')),
2142
+ h('div', { className: 'exp-codeidx' },
2143
+ h('input', { className: 'exp-search', value: qV, placeholder: t('搜索符号/文件名/关键词…', 'Search symbol/file/keyword…'), onInput: function (e) { setQ(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') codeSearch() } }),
2144
+ qInfoV && qInfoV.source ? h('div', { className: 'exp-muted', style: { margin: '3px 0' } }, esc(t('索引时间 ', 'index ') + String(qInfoV.source).replace('T', ' ').slice(5, 16) + (qInfoV.built ? t(' · 已自动重建', ' · rebuilt') : ''))) : null,
2145
+ qHitsV ? h('div', { className: 'exp-qhits' },
2146
+ qHitsV.length ? qHitsV.slice(0, 12).map(function (ht, i) {
2147
+ return h('div', { key: i, className: 'exp-qhit', onClick: function () { openFileView(ht.file) }, title: t('点击在浮层内预览文件', 'click to preview') }, esc(String(ht.file) + ':' + ht.line + ' [' + ht.kind + '] ' + ht.name))
2148
+ }) : h('div', { className: 'exp-empty' }, t('无命中', 'no hits'))) : null),
2149
+ fileV ? h('div', { className: 'exp-fileview' },
2150
+ h('div', { className: 'exp-fileview-h' },
2151
+ h('span', null, esc(String(fileV.path))),
2152
+ h('button', { className: 'exp-feed-x', onClick: function () { setFileV(null) } }, '✕')),
2153
+ h('pre', { className: 'exp-log' }, esc((fileV.text || '').slice(0, 16000)))) : null)
2154
+
2155
+ var boardTab = h('div', null,
2156
+ h('div', { className: 'exp-sec' }, t('全部 run(跨工作区,按更新时间排序)', 'All runs (updated first)')),
2157
+ (runsAll.length ? runsAll.slice().sort(function (a, b) { return (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0) }).map(function (ri, i) {
2158
+ var tot = ri.total || 0, dn = ri.done || 0
2159
+ return h('div', { className: 'exp-board-row', key: i, onClick: function () { setSelRun({ workspace: ri.workspace, runId: ri.runId }) }, title: ri.workspace },
2160
+ h('span', { className: 'exp-board-t' }, esc(String(ri.updatedAt || '').replace('T', ' ').slice(5, 16))),
2161
+ h('span', { className: 'exp-board-n' }, esc(String(ri.runId).slice(0, 22))),
2162
+ h('span', { className: 'exp-muted' }, esc(phaseLabel(ri.phase) + '/' + statusLabel(ri.status) + ' · ' + dn + '/' + tot + (ri.members ? ' · ' + ri.members + '人' : '') + (ri.violations ? ' · ⚠' + ri.violations : ''))),
2163
+ h('span', { className: 'exp-role' }, esc(basename(ri.workspace) || ri.workspace)))
2164
+ }) : h('div', { className: 'exp-empty' }, t('(暂无 run)', '(no runs)')))
2165
+ )
2166
+ return h('div', { className: panelCls, style: panelStyle },
2167
+ h('style', null, CSS),
2168
+ (docked && !viewMode) ? h('div', { className: 'exp-dock-handle', title: '拖拽调宽', onMouseDown: function (e) { startResize(e, pw) } }) : null,
2169
+ h('div', { className: headCls, onMouseDown: function (e) { startDrag(e, docked && !viewMode, pos) } },
2170
+ h('div', { className: 'exp-title' }, h('span', { className: 'exp-live', title: t('实时刷新中', 'live') }), '🧑‍🔬 ' + t('专家团', 'Team')),
2171
+ h('span', { className: 'exp-run' }, runId ? esc(runId) : ''),
2172
+ h('span', { style: { display: 'flex', gap: 6 } },
2173
+ !viewMode ? h('button', { className: 'exp-close', title: docked ? t('转为可拖动浮窗', 'Make floating') : t('停靠回右侧', 'Dock right'), onClick: function () { setDock(!docked) } }, docked ? t('浮动', 'Float') : t('停靠', 'Dock')) : null,
2174
+ viewMode ? h('button', { className: 'exp-close', onClick: function () { try { if (props && props.openView) props.openView('chat', '') } catch (e) {} } }, t('回到对话', 'Chat')) : null,
2175
+ !viewMode ? h('button', { className: 'exp-close', onClick: setOpen }, t('关闭', 'Close')) : null)),
2176
+ h('div', { className: 'exp-modehint' },
2177
+ viewMode
2178
+ ? t('💡 全屏画布 · 想边聊边看?点会话头的「🧑‍💼」徽章打开并排面板', 'Full canvas · tip: use the 🧑‍💼 header badge for a side-by-side panel')
2179
+ : t('💡 并排面板 · 想要全屏画布?切到上方「🧑‍🔬 专家团」标签', 'Side panel · tip: switch to the 🧑‍🔬 team tab above for the full canvas')),
2180
+ h('div', { className: 'exp-body' },
2181
+ errV ? h('div', { className: 'exp-err' }, esc(errV)) : null,
2182
+ selNode,
2183
+ data ? [
2184
+ h('div', { className: 'exp-prog' }, h('div', { className: 'exp-prog-fill', style: { width: pct + '%' } })),
2185
+ h('div', { className: 'exp-meta' }, h('span', null, t('阶段', 'Phase') + ' ' + esc(phaseLabel(data.phase)) + ' / ' + esc(statusLabel(data.status)) + (tierBadge(data.tier) ? ' · ' + esc(tierBadge(data.tier)) : '') + (data.goal ? ' · ' + esc(String(data.goal).slice(0, 46)) : '')), h('b', null, pct + '%')),
2186
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '2px 10px', marginBottom: 4 } },
2187
+ modelsLine ? h('span', { className: 'exp-muted' }, esc(modelsLine)) : null,
2188
+ planLine ? h('span', { className: 'exp-muted' }, esc(planLine)) : null),
2189
+ h('ol', { className: 'exp-path' }, stepper),
2190
+ (data.pendingDecision && data.pendingDecision.options && data.pendingDecision.options.length) ? h(DecisionCard, { decision: data.pendingDecision, onChoose: function (id, label) { decide({ choice: label || id }) } }) : null,
2191
+ tabBar,
2192
+ tab === 'team' ? teamTab : (tab === 'tasks' ? tasksTab : (tab === 'board' ? boardTab : (tab === 'settings' ? h(SettingsTab, null) : infoTab)))
2193
+ ] : null))
2194
+ } catch (e) {
2195
+ return h('div', { className: 'exp-preview' }, 'PANEL ERR: ' + esc(String(e && e.stack ? e.stack : e)).slice(0, 800))
2196
+ }
2197
+ }
2198
+
2199
+ function Boundary(props) { var e = useState(null); if (e[0]) return h('div', { className: 'exp-preview' }, t('专家团面板渲染出错:', 'Team panel render error: ') + esc(String(e[0] && e[0].message || e[0]))); return h('div', {}, props.children) }
2200
+ function wrap(Component) { return function (props) { return h(Boundary, null, h(Component, props || {})) } }
2201
+
2202
+ // ── 共享 state 轮询(HeaderButton / LiveCapsule 复用,单例;无订阅者即停)──
2203
+ var liveStore = { data: null, sid: '', timer: 0, subs: new Set() }
2204
+ function liveTick() {
2205
+ var sid = liveStore.sid
2206
+ if (!sid || typeof fetch === 'undefined') return
2207
+ fetch('/plugins/dsh-expert-team/state?sessionId=' + encodeURIComponent(sid)).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
2208
+ if (!d) return
2209
+ liveStore.data = d
2210
+ try { harvestActivity(d) } catch (e) {}
2211
+ liveStore.subs.forEach(function (f) { try { f(d) } catch (e) {} })
2212
+ }).catch(function () {})
2213
+ }
2214
+ function useLiveState(sid) {
2215
+ var st = useState(liveStore.data); var data = st[0], setData = st[1]
2216
+ useEffect(function () {
2217
+ if (!sid) return undefined
2218
+ liveStore.sid = sid
2219
+ var fn = function (d) { setData(d) }
2220
+ liveStore.subs.add(fn)
2221
+ if (!liveStore.timer) { liveTick(); liveStore.timer = setInterval(liveTick, 2500) }
2222
+ return function () {
2223
+ liveStore.subs.delete(fn)
2224
+ if (!liveStore.subs.size && liveStore.timer) { clearInterval(liveStore.timer); liveStore.timer = 0 }
2225
+ }
2226
+ }, [sid])
2227
+ return data
2228
+ }
2229
+ // 头部徽章文案:让「并排面板」入口自带团队实时状态(与画布标签职责区分)
2230
+ function teamBadge(live) {
2231
+ if (!live || !live.ok) return { text: '🧑‍💼 专家团', title: '并排团队面板' }
2232
+ var tasks = live.tasks || []
2233
+ var done = tasks.filter(function (t) { return ['done', 'completed'].indexOf(t.status) >= 0 }).length
2234
+ var dec = !!(live.pendingDecision && live.pendingDecision.options && live.pendingDecision.options.length)
2235
+ var viol = (live.violations || []).length
2236
+ var members = live.members || {}
2237
+ var running = Object.keys(members).some(function (k) { var m = members[k]; return m && typeof m === 'object' && m.activity === 'running' })
2238
+ if (dec) return { text: '🤔 待你拍板', cls: ' dec', title: '团队等待你的决策' }
2239
+ if (viol) return { text: '⚠ ' + viol + ' 违规', cls: ' warn', title: '门禁违规 ' + viol + ' 项' }
2240
+ if (running) return { text: '🧑‍💼 ' + done + '/' + tasks.length + ' 运行中', cls: ' run', pulse: true, title: '运行中 · ' + phaseLabel(live.phase || '') + '/' + statusLabel(live.status || '') }
2241
+ if (live.status === 'complete' || live.phase === 'deliver') return { text: '✅ 已完成', cls: ' done', title: '已完成 · ' + phaseLabel(live.phase || '') + '/' + statusLabel(live.status || '') }
2242
+ if (!tasks.length && !Object.keys(members).length) return { text: '🧑‍💼 专家团', title: '本工作区还没有 run' }
2243
+ return { text: '🧑‍💼 ' + done + '/' + tasks.length, title: phaseLabel(live.phase || '') + '/' + statusLabel(live.status || '') }
2244
+ }
2245
+
2246
+ function HeaderButton(props) {
2247
+ var sid = props && props.sessionId
2248
+ useEffect(function () { if (sid) setCurrentSession(sid) }, [sid])
2249
+ var isOpen = useOpen()
2250
+ var live = useLiveState(sid)
2251
+ // Open DOCKED (md-preview style: reserves space, never covers the
2252
+ // workspace). The in-panel「浮动」button switches to a draggable float.
2253
+ function toggle() { if (!isOpen) { dockState = true; saveLS('et-dock', true); notify(); setOpen() } else setOpen() }
2254
+ // CSS must exist for the HEADER button too — the panel's own <style> is
2255
+ // only in the DOM while the panel is open. Inject once, globally.
2256
+ try {
2257
+ if (typeof document !== 'undefined' && !document.querySelector('style[data-et-css]')) {
2258
+ var stEl = document.createElement('style')
2259
+ stEl.setAttribute('data-et-css', '1')
2260
+ stEl.textContent = CSS
2261
+ document.head.appendChild(stEl)
2262
+ }
2263
+ } catch (e) {}
2264
+ var badge = teamBadge(live)
2265
+ var tip = '专家团 · ' + badge.title + '|点击' + (isOpen ? '收起' : '打开') + '并排面板(全屏画布:切到上方「🧑‍🔬 专家团」标签)'
2266
+ return h('button', { className: 'exp-open exp-live-badge' + (badge.cls || ''), title: tip, onClick: toggle },
2267
+ h('span', { className: 'exp-live-dot' + (badge.pulse ? ' on' : '') }),
2268
+ esc(badge.text))
2269
+ }
2270
+
2271
+ // ── 对话流「团队动态」胶囊(N1):轮询 state 的 feed/阶段/门禁/决策,取最近事件 ──
2272
+ var activityQ = [] // {icon, text, ts}
2273
+ function pushActivity(icon, text) {
2274
+ try {
2275
+ var last = activityQ[0]
2276
+ if (last && last.text === text && Date.now() - (last.ts || 0) < 4000) return
2277
+ activityQ.unshift({ icon: icon, text: String(text || '').slice(0, 90), ts: Date.now() })
2278
+ if (activityQ.length > 8) activityQ.length = 8
2279
+ notify()
2280
+ } catch (e) {}
2281
+ }
2282
+ var activitySeen = { phase: '', dec: '', viol: '', feed: '' }
2283
+ function harvestActivity(d) {
2284
+ try {
2285
+ if (!d || !d.ok) return
2286
+ if (d.phase && d.phase !== activitySeen.phase) { activitySeen.phase = d.phase; pushActivity('⚙️', '阶段 → ' + phaseLabel(String(d.phase))) }
2287
+ var dec = d.pendingDecision
2288
+ if (dec && dec.options && dec.options.length) {
2289
+ var dk = String(dec.title || '')
2290
+ if (dk !== activitySeen.dec) { activitySeen.dec = dk; pushActivity('🤔', '待你拍板:' + (String(dec.title || '').slice(0, 48) || '方案确认')) }
2291
+ } else if (activitySeen.dec) { activitySeen.dec = '' }
2292
+ if (d.violations && d.violations.length) {
2293
+ var vk = d.violations.join('|')
2294
+ if (vk !== activitySeen.viol) { activitySeen.viol = vk; pushActivity('⚠️', '门禁违规:' + String(d.violations[0]).slice(0, 48)) }
2295
+ }
2296
+ var feed = d.feed || {}
2297
+ var lastLine = '', lastRole = '', lastTs = 0
2298
+ Object.keys(feed).forEach(function (k) {
2299
+ var arr = feed[k] || []
2300
+ if (arr.length) {
2301
+ var e = arr[arr.length - 1]
2302
+ if (e && (!lastLine || (e.ts || 0) >= lastTs)) { lastLine = e.line || ''; lastRole = k; lastTs = e.ts || 0 }
2303
+ }
2304
+ })
2305
+ if (lastLine) {
2306
+ var fk = lastLine.slice(0, 80)
2307
+ if (fk !== activitySeen.feed) { activitySeen.feed = fk; pushActivity('📋', lastLine.slice(0, 68)) }
2308
+ }
2309
+ } catch (e) {}
2310
+ }
2311
+ function LiveCapsule(props) {
2312
+ var q = useExternal(function () { return activityQ.slice() })
2313
+ var openedS = useState(false); var opened = openedS[0], setOpened = openedS[1]
2314
+ var sid = props && props.sessionId
2315
+ useLiveState(sid) // 共享轮询:state 拉取与 harvestActivity 统一由 liveStore 驱动
2316
+ if (!q.length) return null
2317
+ var cur = q[0]
2318
+ return h('div', { className: 'exp-cap' + (opened ? ' open' : '') },
2319
+ h('span', { className: 'exp-cap-btn', onClick: function () { setOpened(!opened) }, title: t('专家团 · 团队动态', 'Team activity') },
2320
+ h('span', { className: 'exp-cap-dot' }),
2321
+ h('span', { className: 'exp-cap-tx' }, esc(String((cur.icon || '') + ' ' + cur.text).slice(0, 90)))),
2322
+ opened ? h('div', { className: 'exp-cap-panel' },
2323
+ q.map(function (a, i) {
2324
+ var tm = ''
2325
+ try { var dd = new Date(a.ts); tm = ('0' + dd.getHours()).slice(-2) + ':' + ('0' + dd.getMinutes()).slice(-2) } catch (e) {}
2326
+ return h('div', { className: 'exp-cap-row' + (i === 0 ? ' cur' : ''), key: i },
2327
+ h('span', { className: 'exp-cap-ic' }, esc(a.icon)), h('span', null, esc(a.text)),
2328
+ h('span', { className: 'exp-cap-t' }, esc(tm)))
2329
+ }),
2330
+ h('div', { className: 'exp-cap-foot' },
2331
+ h('button', { onClick: function () { try { if (!open) { dockState = true; saveLS('et-dock', true); notify(); setOpen() } } catch (e) {} } }, esc('🧑‍🔬 打开团队浮层'))))
2332
+ : null)
2333
+ }
2334
+
2335
+ // ── N2:待拍板 composer 横幅(conversation.composer 接管)──
2336
+ function DecisionComposer(props) {
2337
+ var p = props && props.pendingInteraction
2338
+ var interact = (p && p.decision) || {}
2339
+ var opts = interact.options || []
2340
+ function choose(id) {
2341
+ // 「查看方案」(大需求确认横幅):这是**查看**动作,不是决策 —— 直接打开该 run 的
2342
+ // SPEC.md 预览,**不发 /decide**(否则会把待决清掉,团队还没开工就被"确认"了)。
2343
+ if (/查看方案|view plan/i.test(String(id))) {
2344
+ try { openArtifact('SPEC.md', interact.runId || p.runId, p.workspace || interact.workspace) } catch (e) {}
2345
+ return
2346
+ }
2347
+ try {
2348
+ // 批 0-2:字段必须与 /decide 路由**逐字对齐**(sessionId|workspace 推 cwd + run + choice)。
2349
+ // 旧写法发 {sessionId, runId, id} ⇒ 路由判 400 bad params,用户点「执行」静默失败、
2350
+ // pendingDecision 永不清除。run/workspace 由 syncPending 随 pendingInteraction 发布。
2351
+ fetch('/plugins/dsh-expert-team/decide', {
2352
+ method: 'POST',
2353
+ headers: { 'Content-Type': 'application/json' },
2354
+ body: JSON.stringify({ sessionId: p.sessionId, workspace: p.workspace || '', run: p.runId || interact.runId || '', choice: id })
2355
+ }).then(function () { try { if (exports._resolvePending) exports._resolvePending() } catch (e) {} }).catch(function () {})
2356
+ } catch (e) {}
2357
+ }
2358
+ return h('div', { className: 'exp-decbar' },
2359
+ h('span', { className: 'exp-decbar-ic' }, '🤔'),
2360
+ h('div', { style: { flex: 1, minWidth: 0 } },
2361
+ h('div', { className: 'exp-decbar-t' }, esc(String(interact.title || t('专家团等待你的决策', 'Team needs your decision')))),
2362
+ interact.prompt ? h('div', { className: 'exp-decbar-p' }, esc(String(interact.prompt).slice(0, 90))) : null),
2363
+ h('div', { style: { display: 'flex', gap: 6, flex: 'none', flexWrap: 'wrap' } },
2364
+ opts.map(function (o) {
2365
+ var id = typeof o === 'string' ? o : ((o && o.id) || o.label || '')
2366
+ var lb = typeof o === 'string' ? o : (o && (o.label || o.id)) || id
2367
+ return h('button', { key: id, className: 'exp-opt', onClick: function () { choose(id) } }, esc(String(lb)))
2368
+ })))
2369
+ }
2370
+
2371
+ // Test hook (same convention as the host half's `_live`): lets the regression
2372
+ // suite assert text safety without booting the overlay in a browser.
2373
+ exports._live = { esc: esc, tierBadge: tierBadge, TIER_LABELS_ZH: TIER_LABELS_ZH, settingsFormModel: settingsFormModel }
2374
+ exports.inject = ['slots', 'sessions', 'remote', 'uiSession', 'uiConversation', 'locale']
2375
+ exports.apply = function (ctx) {
2376
+ console.log('[dsh-expert-team] client apply() called')
2377
+ try {
2378
+ // N2:待拍板 pendingInteraction 发布(composer select 据此接管)
2379
+ var publishPending = (ctx.uiSession && ctx.uiSession.registerPendingInteraction) ? ctx.uiSession.registerPendingInteraction(function () { return 5 }) : null
2380
+ var pendingPub = null
2381
+ function resolvePending(fn) { try { if (pendingPub) { pendingPub.unsub(); pendingPub = null } } catch (e) {} try { if (fn) fn() } catch (e) {} }
2382
+ function syncPending(dec, sessionId, runId, workspace) {
2383
+ try {
2384
+ if (!publishPending) return
2385
+ var has = !!(dec && dec.options && dec.options.length)
2386
+ // 批 0-2:必须把 run/workspace 一起发布,否则 composer 横幅回传时无法满足 /decide 的必需字段
2387
+ // (pendingDecision 本体只有 title/prompt/options,不含 runId —— 之前的 dec.runId 恒空,导致恒 400)
2388
+ var runIdV = runId || (dec && dec.runId) || ''
2389
+ var wsV = workspace || (dec && dec.workspace) || ''
2390
+ if (has && !pendingPub) {
2391
+ pendingPub = publishPending({ type: 'et-decision', sessionId: sessionId, runId: runIdV, workspace: wsV, decision: { title: dec.title, prompt: dec.prompt, options: dec.options, runId: runIdV, workspace: wsV } }, function () { return Promise.resolve() })
2392
+ } else if (!has && pendingPub) {
2393
+ pendingPub.unsub(); pendingPub = null
2394
+ }
2395
+ } catch (e) {}
2396
+ }
2397
+ exports._syncPending = syncPending
2398
+ exports._resolvePending = resolvePending
2399
+
2400
+ ctx.slots.inject('conversation.session.header.actions', function () {
2401
+ return ctx.slots.register({ name: 'conversation.session.header.actions', id: 'expert-team-open', order: 900, inject: function (sessionId) { return sessionId ? { sessionId: sessionId } : {} } }, wrap(HeaderButton))
2402
+ })
2403
+ ctx.slots.inject('shell.overlay', function () {
2404
+ return ctx.slots.register({ name: 'shell.overlay', id: 'expert-team-panel', order: 100 }, wrap(Panel))
2405
+ })
2406
+ ctx.slots.inject('shell.overlay', function () {
2407
+ return ctx.slots.register({ name: 'shell.overlay', id: 'expert-team-capsule', order: 110 }, wrap(LiveCapsule))
2408
+ })
2409
+ // N5:嵌入式画布视图标签(conversation.view,trajectory 同款范式)
2410
+ ctx.slots.inject('conversation.view', function () {
2411
+ return ctx.slots.register({
2412
+ name: 'conversation.view',
2413
+ id: 'expert-team',
2414
+ order: 20,
2415
+ label: function () { return '🧑‍🔬 ' + t('专家团', 'Team') },
2416
+ inject: function (sessionId) { return { sessionId: sessionId, mode: 'view' } }
2417
+ }, wrap(Panel))
2418
+ })
2419
+ // N2:待拍板横幅接管 composer(有 pendingInteraction=et-decision 时)
2420
+ ctx.slots.inject('conversation.composer', function () {
2421
+ return ctx.slots.register({
2422
+ name: 'conversation.composer',
2423
+ priority: 1,
2424
+ select: function (props) { var p = props && props.pendingInteraction; return (p && p.type === 'et-decision') ? p : null }
2425
+ }, wrap(DecisionComposer))
2426
+ })
2427
+ // 对话流角色化工具卡(官方 keyed slot):subagent_<role> 调用 → 角色卡
2428
+ Object.keys(ROLE_TOOL).forEach(function (wire) {
2429
+ ctx.slots.inject('tool.call.toolview', function () {
2430
+ return ctx.slots.register({ name: 'tool.call.toolview', key: wire }, wrap(RoleToolView))
2431
+ })
2432
+ })
2433
+ // 接管 dsh 原生 workflow 运行卡:点成员不再打开新标签页,改为右侧面板看实时细节。
2434
+ // order:-1 让本条目在同一 cell 上压过原生条目(renderer:每个 cell 取 order 最小的存活条目)。
2435
+ // 可逆开关:localStorage['et-native-workflow']='1' → 不注册,回退 dsh 原生卡(刷新生效)。
2436
+ try {
2437
+ if (!nativeWfOn()) {
2438
+ ctx.slots.inject('conversation.chat.node', function () {
2439
+ // ⚠️ keyed 插槽的遮蔽参数是 **priority**(不是 order):注册表实现里
2440
+ // 同 key 且同 priority 会**直接抛错**并让整个插件加载失败
2441
+ // (实测:「keyed slot "conversation.chat.node" already has an entry for key
2442
+ // "workflow-run" at priority 0 … register at a different priority to shadow it
2443
+ // (lowest renders)」);keyed 只按 priority 升序渲染,order 被忽略。
2444
+ // 这里动态挑一个未被占用的更低 priority,避免 dsh 改原生优先级后再撞。
2445
+ var prio = -1
2446
+ try {
2447
+ // 注意:下面的 register 必须自己 try/catch —— 它在本回调里执行,
2448
+ // 外层 try 只包住 ctx.slots.inject() 的调用,包不住回调抛错。
2449
+
2450
+ var existing = (typeof ctx.slots.entriesOfSlot === 'function') ? (ctx.slots.entriesOfSlot('conversation.chat.node') || []) : []
2451
+ var used = {}
2452
+ existing.forEach(function (en) {
2453
+ var k = en && en.options ? en.options.key : undefined
2454
+ if (k === 'workflow-run') used[en.options.priority == null ? 0 : en.options.priority] = 1
2455
+ })
2456
+ while (used[prio]) prio -= 1
2457
+ } catch (e4) {}
2458
+ try {
2459
+ return ctx.slots.register({ name: 'conversation.chat.node', key: 'workflow-run', priority: prio }, wrap(WorkflowRunCard))
2460
+ } catch (e5) {
2461
+ // 注册失败只放弃"接管卡片",绝不连累插件加载(历史上同 key 同 priority 会整站 Failed to load plugins)
2462
+ console.log('[dsh-expert-team] workflow card override refused, falling back to the native card:', e5 && e5.message || e5)
2463
+ return undefined
2464
+ }
2465
+ })
2466
+ }
2467
+ } catch (e3) { console.log('[dsh-expert-team] workflow card override skipped', e3 && e3.message || e3) }
2468
+ } catch (e) { console.log('[dsh-expert-team] apply() ERROR', e && e.message || e) }
2469
+ }
2470
+
2471
+ return module.exports
2472
+ },
2473
+ })