@aion0/forge 0.10.78 → 0.10.80

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 (38) hide show
  1. package/RELEASE_NOTES.md +9 -8
  2. package/app/api/code/route.ts +171 -54
  3. package/app/api/onboarding/route.ts +32 -0
  4. package/app/api/skills/local/route.ts +5 -4
  5. package/app/api/tasks/[id]/hook/stop/route.ts +15 -0
  6. package/app/api/tasks/route.ts +2 -1
  7. package/cli/mw.mjs +7 -5
  8. package/cli/mw.ts +8 -6
  9. package/components/CodeViewer.tsx +127 -41
  10. package/components/Dashboard.tsx +6 -2
  11. package/components/DocsViewer.tsx +34 -22
  12. package/components/HelpTerminal.tsx +9 -5
  13. package/components/OnboardingWizard.tsx +65 -1
  14. package/components/ProjectDetail.tsx +33 -7
  15. package/components/TaskDetail.tsx +28 -1
  16. package/components/TmuxTaskTerminal.tsx +105 -0
  17. package/components/WebTerminal.tsx +26 -8
  18. package/components/WorkspaceView.tsx +68 -47
  19. package/docs/design_automation_records/Automation Redesign.dc.html +2019 -0
  20. package/docs/design_automation_records/README.md +232 -0
  21. package/lib/agents/index.ts +9 -0
  22. package/lib/chat/agent-loop.ts +6 -0
  23. package/lib/chat/tool-dispatcher.ts +110 -9
  24. package/lib/fileTree.ts +28 -0
  25. package/lib/help-docs/01-settings.md +11 -0
  26. package/lib/help-docs/05-pipelines.md +31 -0
  27. package/lib/help-docs/07-projects.md +3 -1
  28. package/lib/help-docs/25-chat-tools.md +23 -0
  29. package/lib/pipeline.ts +27 -3
  30. package/lib/session-utils.ts +19 -0
  31. package/lib/task-manager.ts +73 -3
  32. package/lib/task-tmux-backend.ts +625 -0
  33. package/lib/terminal-standalone.ts +17 -0
  34. package/lib/workspace/skill-installer.ts +18 -8
  35. package/package.json +1 -1
  36. package/proxy.ts +5 -4
  37. package/src/core/db/database.ts +1 -0
  38. package/src/types/index.ts +3 -0
@@ -0,0 +1,2019 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <script src="./support.js"></script>
7
+ </head>
8
+ <body>
9
+ <x-dc>
10
+ <helmet>
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
14
+ <style>
15
+ * { box-sizing: border-box; }
16
+ html, body { margin: 0; padding: 0; background: #08090b; }
17
+ ::-webkit-scrollbar { width: 9px; height: 9px; }
18
+ ::-webkit-scrollbar-thumb { background: #2b2e34; border-radius: 6px; }
19
+ ::-webkit-scrollbar-track { background: transparent; }
20
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.25} }
21
+ @keyframes pulseRed { 0%,100%{box-shadow:0 0 0 0 rgba(241,91,74,.0)} 50%{box-shadow:0 0 0 4px rgba(241,91,74,.18)} }
22
+ @keyframes slideIn { from{opacity:0; transform:translateY(4px)} to{opacity:1; transform:none} }
23
+ </style>
24
+ </helmet>
25
+ <div style="font-family:'JetBrains Mono',ui-monospace,monospace; background:#08090b; color:#cdd0d6; min-height:100vh; display:flex; flex-direction:column;">
26
+
27
+ <div style="display:flex; align-items:center; gap:14px; padding:11px 20px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
28
+ <div style="display:flex; align-items:center; gap:9px;">
29
+ <div style="width:22px; height:22px; border-radius:6px; background:linear-gradient(135deg,#ff7d2e,#ff5c4d); display:flex; align-items:center; justify-content:center; font-size:13px;">▰</div>
30
+ <span style="font-weight:700; color:#f3f4f6; letter-spacing:.2px;">Forge</span>
31
+ <span style="color:#3f434b;">/</span>
32
+ <span style="color:#9aa0a8;">Automation Redesign</span>
33
+ </div>
34
+ <div style="flex:1;"></div>
35
+ <div style="display:flex; align-items:center; gap:16px; font-size:11.5px; color:#6b7079;">
36
+ <span style="display:flex; align-items:center; gap:6px;"><span style="width:9px; height:9px; border-radius:2px; background:#46c25a; display:inline-block;"></span>passed</span>
37
+ <span style="display:flex; align-items:center; gap:6px;"><span style="width:9px; height:9px; border-radius:2px; background:#f15b4a; display:inline-block;"></span>failed</span>
38
+ <span style="display:flex; align-items:center; gap:6px;"><span style="width:9px; height:9px; border-radius:2px; background:#4f9dff; display:inline-block;"></span>running</span>
39
+ <span style="display:flex; align-items:center; gap:6px;"><span style="width:9px; height:9px; border-radius:2px; background:#33363d; display:inline-block;"></span>skipped</span>
40
+ </div>
41
+ </div>
42
+
43
+ <div style="display:flex; flex:1; min-height:0;">
44
+
45
+ <aside style="width:268px; flex-shrink:0; background:#0b0c0e; border-right:1px solid #1d1f24; padding:18px 14px; overflow-y:auto;">
46
+ <sc-for list="{{ navTop }}" as="item" hint-placeholder-count="1">
47
+ <div onClick="{{ item.onSelect }}" style="{{ item.style }}">
48
+ <div style="display:flex; align-items:center; gap:8px;">
49
+ <span style="font-size:13px;">{{ item.icon }}</span>
50
+ <span style="font-weight:600; font-size:12.5px;">{{ item.label }}</span>
51
+ </div>
52
+ </div>
53
+ </sc-for>
54
+
55
+ <div style="font-size:10.5px; letter-spacing:1.2px; color:#52565e; text-transform:uppercase; margin:20px 6px 9px;">Pipeline</div>
56
+ <sc-for list="{{ navPipe }}" as="item" hint-placeholder-count="3">
57
+ <div onClick="{{ item.onSelect }}" style="{{ item.style }}">
58
+ <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:3px;">
59
+ <span style="font-weight:600; font-size:12.5px; color:{{ item.titleColor }};">{{ item.label }}</span>
60
+ <span style="{{ item.tagStyle }}">{{ item.tag }}</span>
61
+ </div>
62
+ <div style="font-size:11px; color:#6b7079; line-height:1.4;">{{ item.desc }}</div>
63
+ </div>
64
+ </sc-for>
65
+
66
+ <div style="font-size:10.5px; letter-spacing:1.2px; color:#52565e; text-transform:uppercase; margin:20px 6px 9px;">Task</div>
67
+ <sc-for list="{{ navTask }}" as="item" hint-placeholder-count="3">
68
+ <div onClick="{{ item.onSelect }}" style="{{ item.style }}">
69
+ <div style="display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:3px;">
70
+ <span style="font-weight:600; font-size:12.5px; color:{{ item.titleColor }};">{{ item.label }}</span>
71
+ <span style="{{ item.tagStyle }}">{{ item.tag }}</span>
72
+ </div>
73
+ <div style="font-size:11px; color:#6b7079; line-height:1.4;">{{ item.desc }}</div>
74
+ </div>
75
+ </sc-for>
76
+ </aside>
77
+
78
+ <main style="flex:1; min-width:0; overflow:auto; background:#08090b; padding:24px 28px 60px;">
79
+
80
+ <sc-if value="{{ showIntro }}" hint-placeholder-val="{{ true }}">
81
+ <div style="max-width:860px;">
82
+ <div style="font-family:'Inter',sans-serif; font-size:26px; font-weight:700; color:#f3f4f6; margin-bottom:6px;">Automation · 页面结构</div>
83
+ <div style="font-family:'Inter',sans-serif; color:#8b9098; font-size:14px; line-height:1.6; margin-bottom:26px;">按你的信息架构拆页:<b style="color:#cdd0d6;">Pipeline</b>(只管查看/编辑定义)与 <b style="color:#cdd0d6;">Pipeline Record</b>(运行记录)分开;Task 是<b style="color:#cdd0d6;">一次性</b>的,创建即运行、跑完即止,所以只有<b style="color:#cdd0d6;">单一 Task 页</b>(新建 + 列表 + 详情)。顶部导航可直接切换。</div>
84
+
85
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
86
+ <div style="background:#0f1013; border:1px solid #22252b; border-radius:10px; padding:18px;">
87
+ <div style="font-size:11px; letter-spacing:1px; color:#ff7d2e; text-transform:uppercase; margin-bottom:8px;">Pipeline / Pipeline Record</div>
88
+ <div style="font-family:'Inter',sans-serif; font-size:13px; color:#aeb3bb; line-height:1.7;"><b style="color:#cdd0d6;">Pipeline</b>:管理定义与步骤(查看 / 编辑),不混入运行数据。<b style="color:#cdd0d6;">Pipeline Record</b>:所有运行平铺成表,列出 pipeline id / 名 / run / 步骤 / 结果,按需展开每条自己的步骤。</div>
89
+ </div>
90
+ <div style="background:#0f1013; border:1px solid #22252b; border-radius:10px; padding:18px;">
91
+ <div style="font-size:11px; letter-spacing:1px; color:#4f9dff; text-transform:uppercase; margin-bottom:8px;">Task(合并为一页)</div>
92
+ <div style="font-family:'Inter',sans-serif; font-size:13px; color:#aeb3bb; line-height:1.7;">Task 一次性,无独立定义,不需要 Pipeline 那样的双页。一页搞定:<b style="color:#cdd0d6;">新建任务</b> + 平铺列表(搜索 / 快速过滤)+ 点行看详情(T3 终端驾驶舱)。</div>
93
+ </div>
94
+ </div>
95
+
96
+ <div style="font-family:'Inter',sans-serif; font-size:13px; color:#6b7079; margin-top:24px;">← 左侧或顶部导航在各页面间切换。Pipeline Record 里失败的 run 可 <b style="color:#ff7d2e;">Retry</b>、运行中的可 <b style="color:#f15b4a;">Cancel</b>;Task 行点开即在当页查看完整详情(日志/Result/Diff),失败可重试、运行中可取消。</div>
97
+ </div>
98
+ </sc-if>
99
+
100
+ <sc-if value="{{ showMgmt }}" hint-placeholder-val="{{ false }}">
101
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
102
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">Pipeline · 管理</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#ff7d2e; background:#ff7d2e22;">页面</span></div>
103
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">这里只做 pipeline 的<b style="color:#cdd0d6;">查看与编辑</b>——定义、步骤、触发方式。运行产生的记录在 <b style="color:#cdd0d6;">Pipeline Record</b> 页查看,两者分开。</div>
104
+ </div>
105
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;"> <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
106
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
107
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
108
+ </sc-for>
109
+ </div>
110
+ <div style="display:flex; min-height:560px; min-width:900px;">
111
+ <div style="width:248px; flex-shrink:0; border-right:1px solid #1d1f24; display:flex; flex-direction:column;">
112
+ <div style="padding:11px 14px; display:flex; align-items:center; gap:8px; border-bottom:1px solid #1d1f24;">
113
+ <span style="font-size:10.5px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Pipelines · 6</span>
114
+ <span style="flex:1;"></span>
115
+ <span style="font-size:11px; color:#0b0c0e; background:#ff7d2e; padding:3px 9px; border-radius:6px; cursor:pointer; font-weight:600;">+ New</span>
116
+ </div>
117
+ <div style="overflow-y:auto; flex:1;">
118
+ <sc-for list="{{ mgmt.list }}" as="p" hint-placeholder-count="6">
119
+ <button onClick="{{ p.onSelect }}" style="{{ p.rowStyle }}">
120
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:3px;">
121
+ <span style="width:6px; height:6px; border-radius:50%; flex-shrink:0; background:{{ p.lastDot }};"></span>
122
+ <span style="font-size:12.5px; font-weight:600; color:{{ p.nameColor }};">{{ p.name }}</span>
123
+ </div>
124
+ <div style="display:flex; align-items:center; gap:10px; padding-left:14px;">
125
+ <span style="font-size:10.5px; color:#6b7079; font-family:'JetBrains Mono',monospace;">{{ p.pid }}</span>
126
+ <span style="font-size:10.5px; color:#565b63;">{{ p.stepCount }} steps</span>
127
+ <span style="font-size:10.5px; color:#565b63;">{{ p.runs }} runs</span>
128
+ </div>
129
+ </button>
130
+ </sc-for>
131
+ </div>
132
+ </div>
133
+
134
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column;">
135
+ <div style="padding:18px 22px; border-bottom:1px solid #1d1f24;">
136
+ <div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
137
+ <span style="font-size:19px; font-weight:700; color:#f3f4f6;">{{ mgmt.sel.name }}</span>
138
+ <span style="font-size:11px; color:#6b7079; font-family:'JetBrains Mono',monospace;">{{ mgmt.sel.pid }}</span>
139
+ <span style="flex:1;"></span>
140
+ <span onClick="{{ mgmt.viewRecords }}" style="font-size:11.5px; color:#4f9dff; cursor:pointer; font-weight:600;">查看运行记录 →</span>
141
+ <span onClick="{{ mgmt.toggleEdit }}" style="{{ mgmt.editStyle }}">{{ mgmt.editLabel }}</span>
142
+ </div>
143
+ <div style="margin-top:10px; display:flex; align-items:center; gap:18px; font-size:12px; color:#8b9098;">
144
+ <span>trigger · <b style="color:#cdd0d6;">{{ mgmt.sel.trigger }}</b></span>
145
+ <span>steps · <b style="color:#cdd0d6;">{{ mgmt.sel.stepCount }}</b></span>
146
+ <span>runs · <b style="color:#cdd0d6;">{{ mgmt.sel.runs }}</b></span>
147
+ <span style="display:flex; align-items:center; gap:6px;">last · <span style="width:7px; height:7px; border-radius:50%; background:{{ mgmt.sel.lastDot }};"></span><b style="color:#cdd0d6;">{{ mgmt.sel.lastStatus }}</b></span>
148
+ </div>
149
+ </div>
150
+ <div style="flex:1; overflow-y:auto; padding:18px 22px;">
151
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
152
+ <span style="font-size:11px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Steps · 定义</span>
153
+ <span style="font-size:11px; color:#6b7079;">{{ mgmt.sel.stepCount }}</span>
154
+ <span style="flex:1;"></span>
155
+ <sc-if value="{{ mgmt.edit }}" hint-placeholder-val="{{ false }}">
156
+ <span style="font-size:11px; color:#46c25a; cursor:pointer; font-weight:600;">+ Add step</span>
157
+ </sc-if>
158
+ </div>
159
+ <div style="border:1px solid #1d1f24; border-radius:10px; overflow:hidden;">
160
+ <sc-for list="{{ mgmt.steps }}" as="s" hint-placeholder-count="6">
161
+ <div style="display:flex; align-items:flex-start; gap:12px; padding:12px 15px; border-bottom:1px solid #16181c;">
162
+ <sc-if value="{{ s.edit }}" hint-placeholder-val="{{ false }}">
163
+ <span style="color:#3f434b; font-size:13px; cursor:grab; padding-top:1px;">⠿</span>
164
+ </sc-if>
165
+ <span style="font-size:11px; color:#565b63; font-family:'JetBrains Mono',monospace; width:18px; flex-shrink:0; text-align:right; padding-top:1px;">{{ s.idx }}</span>
166
+ <div style="flex:1; min-width:0;">
167
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:5px;">
168
+ <span style="font-size:13px; font-weight:600; color:#e6e8eb;">{{ s.name }}</span>
169
+ <span style="font-size:9.5px; font-weight:600; padding:1px 7px; border-radius:5px; color:{{ s.kindColor }}; background:{{ s.kindColor }}1c;">{{ s.kindLabel }}</span>
170
+ </div>
171
+ <div style="font-size:11.5px; color:#7d828b; font-family:'JetBrains Mono',monospace; line-height:1.5; word-break:break-all;">{{ s.cmd }}</div>
172
+ </div>
173
+ <sc-if value="{{ s.edit }}" hint-placeholder-val="{{ false }}">
174
+ <span style="color:#5e636b; font-size:13px; cursor:pointer; padding-top:1px;">✕</span>
175
+ </sc-if>
176
+ </div>
177
+ </sc-for>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </sc-if>
184
+
185
+ <sc-if value="{{ showTRecord }}" hint-placeholder-val="{{ false }}">
186
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
187
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">Task · 任务</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#ff7d2e; background:#ff7d2e22;">页面</span></div>
188
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">任务是<b style="color:#cdd0d6;">一次性</b>的——<b style="color:#cdd0d6;">创建即运行,跑完即止</b>,没有需要单独管理的「定义」,所以不像 Pipeline 拆成两页。这一页就是<b style="color:#cdd0d6;">新建任务 + 最近/运行中任务列表</b>(平铺、可搜索、快速过滤)。点任意一行看它的详情(T3 终端驾驶舱)。</div>
189
+ </div>
190
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;"> <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
191
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
192
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
193
+ </sc-for>
194
+ </div>
195
+ <div style="padding:13px 18px; border-bottom:1px solid #1d1f24; display:flex; flex-direction:column; gap:11px; min-width:980px;">
196
+ <div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
197
+ <div style="flex:1; min-width:220px; display:flex; align-items:center; gap:8px; background:#08090b; border:1px solid #24262b; border-radius:8px; padding:7px 12px;">
198
+ <span style="color:#565b63; font-size:13px;">⌕</span>
199
+ <input value="{{ taskRec.search }}" onChange="{{ taskRec.onSearch }}" placeholder="按 task id / 命令 / 步骤 / pipeline 搜索…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
200
+ <sc-if value="{{ taskRec.search }}" hint-placeholder-val="{{ false }}">
201
+ <span onClick="{{ taskRec.clearSearch }}" style="color:#6b7079; cursor:pointer; font-size:14px;">×</span>
202
+ </sc-if>
203
+ </div>
204
+ <div style="font-size:11.5px; color:#7d828b; white-space:nowrap;"><b style="color:#e6e8eb;">{{ taskRec.total }}</b> tasks · <b style="color:#f15b4a;">{{ taskRec.failN }}</b> failed</div>
205
+ <span onClick="{{ taskRec.openCreate }}" style="font-size:12px; font-weight:600; color:#0b0c0e; background:#ff7d2e; padding:8px 15px; border-radius:8px; cursor:pointer; white-space:nowrap;">+ 新建任务</span>
206
+ </div>
207
+ <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
208
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-right:2px;">快速</span>
209
+ <sc-for list="{{ taskRec.quick }}" as="q" hint-placeholder-count="6">
210
+ <div onClick="{{ q.onSelect }}" style="{{ q.style }}">{{ q.label }}</div>
211
+ </sc-for>
212
+ </div>
213
+ <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
214
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Status</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;"><sc-for list="{{ taskRec.statusOpts }}" as="o" hint-placeholder-count="4"><div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div></sc-for></div></div>
215
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Provider</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;"><sc-for list="{{ taskRec.providerOpts }}" as="o" hint-placeholder-count="4"><div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div></sc-for></div></div>
216
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Time</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;"><sc-for list="{{ taskRec.timeOpts }}" as="o" hint-placeholder-count="4"><div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div></sc-for></div></div>
217
+ </div>
218
+ <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Pipeline</span><div style="display:flex; gap:3px; flex-wrap:wrap; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;"><sc-for list="{{ taskRec.pipeOpts }}" as="o" hint-placeholder-count="7"><div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div></sc-for></div></div>
219
+ </div>
220
+ <div style="min-width:980px; max-height:540px; overflow-y:auto;">
221
+ <div style="display:flex; align-items:center; gap:12px; padding:9px 16px; background:#0a0b0d; border-bottom:1px solid #1d1f24; position:sticky; top:0; z-index:2;">
222
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:76px; flex-shrink:0;">Status</span>
223
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:86px; flex-shrink:0;">Task ID</span>
224
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:200px; flex-shrink:0;">Pipeline · Step</span>
225
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:80px; flex-shrink:0;">Provider</span>
226
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; flex:1; min-width:80px;">Command</span>
227
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:58px; flex-shrink:0; text-align:right;">Cost</span>
228
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:58px; flex-shrink:0; text-align:right;">Dur</span>
229
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:62px; flex-shrink:0; text-align:right;">Started</span>
230
+ <span style="width:16px; flex-shrink:0;"></span>
231
+ </div>
232
+ <sc-for list="{{ taskRec.rows }}" as="r" hint-placeholder-count="12">
233
+ <div><div onClick="{{ r.onToggle }}" style="{{ r.rowStyle }}">
234
+ <span style="display:flex; align-items:center; gap:7px; width:76px; flex-shrink:0;">
235
+ <span style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:{{ r.statusColor }};"></span>
236
+ <span style="font-size:11px; color:{{ r.statusColor }}; font-weight:600;">{{ r.status }}</span>
237
+ </span>
238
+ <span style="font-size:12px; color:#c9cdd3; width:86px; flex-shrink:0; font-family:'JetBrains Mono',monospace;">{{ r.id }}</span>
239
+ <span style="width:200px; flex-shrink:0; overflow:hidden;">
240
+ <span style="display:block; font-size:12px; color:#c2c6cd; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.pipeName }}</span>
241
+ <span style="display:block; font-size:10.5px; color:#6b7079;">{{ r.step }}</span>
242
+ </span>
243
+ <span style="font-size:11.5px; color:{{ r.providerColor }}; width:80px; flex-shrink:0; font-weight:600;">{{ r.provider }}</span>
244
+ <span style="flex:1; min-width:80px; font-size:11px; color:#8b9098; font-family:'JetBrains Mono',monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.summary }}</span>
245
+ <span style="font-size:11px; color:#a978f0; width:58px; flex-shrink:0; text-align:right;">{{ r.cost }}</span>
246
+ <span style="font-size:11px; color:#6b7079; width:58px; flex-shrink:0; text-align:right;">{{ r.dur }}</span>
247
+ <span style="font-size:11px; color:#565b63; width:62px; flex-shrink:0; text-align:right;">{{ r.ago }}</span>
248
+ <span style="font-size:11px; color:#6b7079; width:16px; flex-shrink:0; text-align:right;">{{ r.openGlyph }}</span>
249
+ </div>
250
+ <sc-if value="{{ r.expanded }}" hint-placeholder-val="{{ false }}">
251
+ <div style="background:#070809; border-bottom:1px solid #1d1f24; border-left:2px solid {{ r.statusColor }}; animation:slideIn .15s ease;">
252
+ <div style="display:flex; align-items:center; gap:10px; padding:10px 18px; background:#0d0e11; border-bottom:1px solid #16181c;">
253
+ <span style="width:9px; height:9px; border-radius:50%; background:{{ r.detail.statusColor }};"></span>
254
+ <span style="font-size:12px; color:#9aa0a8; font-family:'JetBrains Mono',monospace;">task://{{ r.detail.id }}</span>
255
+ <span style="color:#3f434b;">·</span>
256
+ <span style="font-size:12px; font-weight:700; color:#f3f4f6;">{{ r.detail.step }}</span>
257
+ <span style="font-size:10.5px; font-weight:700; letter-spacing:.5px; padding:2px 8px; border-radius:5px; text-transform:uppercase; color:{{ r.detail.statusColor }}; background:{{ r.detail.statusColor }}1f;">{{ r.detail.status }}</span>
258
+ <span style="flex:1;"></span>
259
+ <span onClick="{{ r.onOpenFull }}" style="font-size:11px; color:#4f9dff; cursor:pointer; font-weight:600;">打开完整详情 ↗</span>
260
+ <sc-if value="{{ r.detail.retryable }}" hint-placeholder-val="{{ false }}">
261
+ <span onClick="{{ r.detail.onRetry }}" style="font-size:11px; font-weight:700; padding:6px 13px; border-radius:7px; cursor:pointer; color:#0b0c0e; background:#ff7d2e;">↻ Retry</span>
262
+ </sc-if>
263
+ <sc-if value="{{ r.detail.running }}" hint-placeholder-val="{{ false }}">
264
+ <span onClick="{{ r.detail.onCancel }}" style="font-size:11px; font-weight:700; padding:6px 13px; border-radius:7px; cursor:pointer; color:#f15b4a; background:#f15b4a18; border:1px solid #f15b4a55;">■ Cancel</span>
265
+ </sc-if>
266
+ <span style="font-size:11px; color:#0b0c0e; background:#1a1d22; color:#cdd0d6; padding:6px 12px; border-radius:7px; cursor:pointer; border:1px solid #2a2e35;">Re-run</span>
267
+ </div>
268
+
269
+ <div style="display:flex; align-items:center; gap:10px; padding:9px 18px; background:#0a0b0d; border-bottom:1px solid #16181c;">
270
+ <div style="flex:1; display:flex; align-items:center; gap:8px; background:#070809; border:1px solid #20232a; border-radius:7px; padding:6px 11px;">
271
+ <span style="color:#565b63; font-size:12px;">⌕</span>
272
+ <input value="{{ taskSearch }}" onChange="{{ onSearch }}" placeholder="grep log…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
273
+ <sc-if value="{{ taskSearch }}" hint-placeholder-val="{{ false }}">
274
+ <span onClick="{{ clearSearch }}" style="color:#6b7079; cursor:pointer; font-size:13px;">×</span>
275
+ </sc-if>
276
+ </div>
277
+ <div onClick="{{ toggleErrors }}" style="{{ errToggleStyle }}">
278
+ <span style="width:8px; height:8px; border-radius:2px; background:#f15b4a; display:inline-block;"></span>
279
+ <span style="font-size:11px;">errors {{ r.detail.errCount }}</span>
280
+ </div>
281
+ </div>
282
+
283
+ <div style="display:flex; min-height:200px;">
284
+ <div style="flex:1; min-width:0; overflow-y:auto; max-height:280px; padding:13px 0; font-size:12.5px; line-height:1.9; background:#070809;">
285
+ <sc-for list="{{ r.detail.logLines }}" as="ln" hint-placeholder-count="6">
286
+ <div style="display:flex; gap:0; padding:0 18px; background:{{ ln.rowBg }};">
287
+ <span style="color:#34373d; user-select:none; width:24px; text-align:right; flex-shrink:0; margin-right:14px;">{{ ln.n }}</span>
288
+ <span style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</span>
289
+ </div>
290
+ </sc-for>
291
+ </div>
292
+ <div style="width:13px; flex-shrink:0; background:#0a0b0d; border-left:1px solid #16181c; display:flex; flex-direction:column; gap:2px; padding:8px 3px;">
293
+ <sc-for list="{{ r.detail.mini }}" as="mm" hint-placeholder-count="8">
294
+ <span style="width:100%; height:5px; border-radius:1px; background:{{ mm.color }};"></span>
295
+ </sc-for>
296
+ </div>
297
+ </div>
298
+
299
+ <div style="display:flex; border-top:1px solid #16181c; min-height:150px;">
300
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid #16181c;">
301
+ <div style="padding:8px 18px; background:#0d0e11; border-bottom:1px solid #16181c; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Result</div>
302
+ <div style="flex:1; overflow:auto; max-height:170px; padding:11px 18px; font-size:11px; line-height:1.7; color:#a6abb3; white-space:pre; background:#08090b;">{{ r.detail.result }}</div>
303
+ </div>
304
+ <div style="flex:1.2; min-width:0; display:flex; flex-direction:column;">
305
+ <div style="padding:8px 18px; background:#0d0e11; border-bottom:1px solid #16181c; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Git Diff</div>
306
+ <div style="flex:1; overflow:auto; max-height:170px; padding:7px 0; font-size:10.5px; line-height:1.7; background:#08090b;">
307
+ <sc-for list="{{ r.detail.diff }}" as="d" hint-placeholder-count="6">
308
+ <div style="padding:0 18px; background:{{ d.bg }}; color:{{ d.color }}; white-space:pre;">{{ d.text }}</div>
309
+ </sc-for>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </sc-if>
315
+ </div>
316
+ </sc-for>
317
+ </div>
318
+ </div>
319
+
320
+ <sc-if value="{{ taskRec.createOpen }}" hint-placeholder-val="{{ false }}">
321
+ <div onClick="{{ taskRec.closeCreate }}" style="position:fixed; inset:0; background:rgba(4,5,7,.72); z-index:50; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace;">
322
+ <div onClick="{{ taskRec.stop }}" style="width:560px; max-width:92vw; background:#0e1013; border:1px solid #2a2e35; border-radius:14px; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.5);">
323
+ <div style="display:flex; align-items:center; gap:10px; padding:16px 20px; border-bottom:1px solid #1d1f24;">
324
+ <span style="font-size:15px; font-weight:700; color:#f3f4f6; font-family:'Inter',sans-serif;">新建任务</span>
325
+ <span style="font-size:11px; color:#6b7079;">创建即运行 · 一次性</span>
326
+ <span style="flex:1;"></span>
327
+ <span onClick="{{ taskRec.closeCreate }}" style="font-size:18px; color:#6b7079; cursor:pointer; line-height:1;">×</span>
328
+ </div>
329
+ <div style="padding:20px;">
330
+ <div style="font-size:11px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:7px;">Provider / Repo</div>
331
+ <select value="{{ taskRec.providerVal }}" onChange="{{ taskRec.setProvider }}" style="width:100%; background:#08090b; border:1px solid #24262b; border-radius:8px; padding:10px 12px; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12.5px; outline:none; margin-bottom:18px;">
332
+ <option>FortiNAC</option>
333
+ <option>scratch</option>
334
+ <option>canary</option>
335
+ <option>regress</option>
336
+ <option>depbump</option>
337
+ <option>docs</option>
338
+ </select>
339
+ <div style="font-size:11px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:7px;">Type</div>
340
+ <div style="display:flex; gap:4px; background:#08090b; border:1px solid #24262b; border-radius:8px; padding:4px; margin-bottom:18px;">
341
+ <div onClick="{{ taskRec.modeShell }}" style="{{ taskRec.modeShellStyle }}">⌘ Shell</div>
342
+ <div onClick="{{ taskRec.modeLlm }}" style="{{ taskRec.modeLlmStyle }}">✦ LLM Prompt</div>
343
+ </div>
344
+ <div style="font-size:11px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:7px;">Command / Prompt</div>
345
+ <textarea value="{{ taskRec.form.cmd }}" onChange="{{ taskRec.onCmd }}" placeholder="{{ taskRec.cmdPlaceholder }}" style="width:100%; min-height:96px; resize:vertical; background:#08090b; border:1px solid #24262b; border-radius:8px; padding:11px 13px; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12.5px; line-height:1.6; outline:none; box-sizing:border-box;"></textarea>
346
+ </div>
347
+ <div style="display:flex; align-items:center; gap:12px; padding:14px 20px; border-top:1px solid #1d1f24; background:#0b0c0e;">
348
+ <span style="font-size:11px; color:#6b7079;">运行后会出现在下方列表,状态 running。</span>
349
+ <span style="flex:1;"></span>
350
+ <span onClick="{{ taskRec.closeCreate }}" style="font-size:12px; color:#9aa0a8; cursor:pointer; padding:9px 14px;">取消</span>
351
+ <span onClick="{{ taskRec.runCreate }}" style="{{ taskRec.runStyle }}">▶ 创建并运行</span>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </sc-if>
356
+ </sc-if>
357
+
358
+ <sc-if value="{{ showP1 }}" hint-placeholder-val="{{ true }}">
359
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
360
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">Pipeline Record · 执行记录</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#ff7d2e; background:#ff7d2e22;">新方向</span></div>
361
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">所有 pipeline 的执行记录<b style="color:#cdd0d6;">平铺成一个表</b>,每行直接列出 pipeline id、pipeline 名、run id、步骤、结果、触发方式、耗时、时间。按条件筛选(状态 / pipeline / 时间 / 触发)。每条记录展开时渲染<b style="color:#cdd0d6;">它自己的步骤</b>——步骤拆细、每次不一样都没问题,并自动定位到失败步的日志。<span style="color:#6b7079;">(如需仍可切「按 pipeline 分组」。)</span></div>
362
+ </div>
363
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;">
364
+ <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
365
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
366
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
367
+ </sc-for>
368
+ </div>
369
+
370
+ <div style="padding:13px 18px; border-bottom:1px solid #1d1f24; display:flex; flex-direction:column; gap:11px; min-width:880px;">
371
+ <div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
372
+ <div style="flex:1; min-width:220px; display:flex; align-items:center; gap:8px; background:#08090b; border:1px solid #24262b; border-radius:8px; padding:7px 12px;">
373
+ <span style="color:#565b63; font-size:13px;">⌕</span>
374
+ <input value="{{ records.search }}" onChange="{{ records.onSearch }}" placeholder="按 run id / pipeline / 步骤名 筛选…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
375
+ <sc-if value="{{ records.search }}" hint-placeholder-val="{{ false }}">
376
+ <span onClick="{{ records.clearRecSearch }}" style="color:#6b7079; cursor:pointer; font-size:14px;">×</span>
377
+ </sc-if>
378
+ </div>
379
+ <div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;">
380
+ <sc-for list="{{ records.groupOpts }}" as="o" hint-placeholder-count="3">
381
+ <div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div>
382
+ </sc-for>
383
+ </div>
384
+ <div style="font-size:11.5px; color:#7d828b; white-space:nowrap;"><b style="color:#e6e8eb;">{{ records.total }}</b> runs · <b style="color:#f15b4a;">{{ records.failN }}</b> failed</div>
385
+ </div>
386
+ <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
387
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Status</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;">
388
+ <sc-for list="{{ records.statusOpts }}" as="o" hint-placeholder-count="3">
389
+ <div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div>
390
+ </sc-for>
391
+ </div></div>
392
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Time</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;">
393
+ <sc-for list="{{ records.timeOpts }}" as="o" hint-placeholder-count="3">
394
+ <div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div>
395
+ </sc-for>
396
+ </div></div>
397
+ <div style="display:flex; align-items:center; gap:8px;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Trigger</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;">
398
+ <sc-for list="{{ records.trigOpts }}" as="o" hint-placeholder-count="3">
399
+ <div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div>
400
+ </sc-for>
401
+ </div></div>
402
+ </div>
403
+ <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;"><span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase;">Pipeline</span><div style="display:flex; gap:3px; background:#0c0d10; border:1px solid #1a1d22; border-radius:8px; padding:3px;">
404
+ <sc-for list="{{ records.pipeOpts }}" as="o" hint-placeholder-count="3">
405
+ <div onClick="{{ o.onSelect }}" style="{{ o.style }}">{{ o.label }}</div>
406
+ </sc-for>
407
+ </div></div>
408
+ </div>
409
+
410
+ <div style="max-height:560px; overflow:auto;">
411
+ <div style="min-width:1040px;">
412
+ <sc-if value="{{ records.isGroup }}" hint-placeholder-val="{{ true }}">
413
+ <sc-for list="{{ records.groups }}" as="g" hint-placeholder-count="4">
414
+ <div>
415
+ <div onClick="{{ g.onToggle }}" style="{{ g.headerStyle }}">
416
+ <span style="font-size:11px; color:#6b7079; width:10px; flex-shrink:0;">{{ g.chev }}</span>
417
+ <span style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:{{ g.lastDot }};"></span>
418
+ <span style="font-size:13px; font-weight:700; color:#e6e8eb;">{{ g.name }}</span>
419
+ <span style="font-size:11px; color:#6b7079;">{{ g.count }} runs</span>
420
+ <sc-if value="{{ g.hasFail }}" hint-placeholder-val="{{ true }}">
421
+ <span style="font-size:10px; font-weight:700; color:#f15b4a; background:#f15b4a1c; padding:2px 7px; border-radius:5px;">{{ g.fails }} failed</span>
422
+ </sc-if>
423
+ <span style="flex:1;"></span>
424
+ <span style="font-size:9.5px; color:#565b63; letter-spacing:.5px;">recent</span>
425
+ <div style="display:flex; gap:2px;">
426
+ <sc-for list="{{ g.spark }}" as="s" hint-placeholder-count="12">
427
+ <span style="width:7px; height:14px; border-radius:2px; background:{{ s.bg }};"></span>
428
+ </sc-for>
429
+ </div>
430
+ </div>
431
+ <sc-for list="{{ g.runs }}" as="r" hint-placeholder-count="0">
432
+ <div>
433
+ <div onClick="{{ r.onToggle }}" style="{{ r.rowStyle }}">
434
+ <span style="font-size:10px; color:#565b63; width:12px; flex-shrink:0;">{{ r.chev }}</span>
435
+ <span style="display:flex; align-items:center; gap:7px; width:74px; flex-shrink:0;">
436
+ <span style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:{{ r.dotColor }};"></span>
437
+ <span style="font-size:11px; color:{{ r.summaryColor }}; font-weight:600;">{{ r.statusLabel }}</span>
438
+ </span>
439
+ <sc-if value="{{ r.showPipe }}" hint-placeholder-val="{{ true }}">
440
+ <span style="font-size:11.5px; color:#6b7079; width:90px; flex-shrink:0; font-family:'JetBrains Mono',monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.pid }}</span>
441
+ </sc-if>
442
+ <sc-if value="{{ r.showPipe }}" hint-placeholder-val="{{ true }}">
443
+ <span style="font-size:12px; font-weight:600; color:#c2c6cd; width:196px; flex-shrink:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.pipeName }}</span>
444
+ </sc-if>
445
+ <span style="font-size:12px; color:#c9cdd3; width:80px; flex-shrink:0;">{{ r.id }}</span>
446
+ <div style="flex:1; min-width:64px; display:flex; gap:1.5px;">
447
+ <sc-for list="{{ r.segments }}" as="seg" hint-placeholder-count="8">
448
+ <span style="flex:1; height:6px; border-radius:1px; background:{{ seg.bg }};"></span>
449
+ </sc-for>
450
+ </div>
451
+ <span style="font-size:11px; color:{{ r.summaryColor }}; width:150px; flex-shrink:0; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.summary }}</span>
452
+ <span style="font-size:10.5px; color:#7d828b; width:92px; flex-shrink:0;">{{ r.trigGlyph }} {{ r.trigger }}</span>
453
+ <span style="font-size:11px; color:#6b7079; width:64px; flex-shrink:0; text-align:right;">{{ r.dur }}</span>
454
+ <span style="font-size:11px; color:#565b63; width:62px; flex-shrink:0; text-align:right;">{{ r.ago }}</span>
455
+ <span style="position:sticky; right:0; width:104px; flex-shrink:0; display:flex; justify-content:flex-end; align-items:center; padding:11px 16px 11px 18px; margin:-11px -16px -11px 0; background:linear-gradient(90deg, transparent, #0c0d10 26%); pointer-events:none;">
456
+ <sc-if value="{{ r.quick.show }}" hint-placeholder-val="{{ false }}">
457
+ <span onClick="{{ r.quick.run }}" style="{{ r.quickStyle }} pointer-events:auto;">{{ r.quick.label }}</span>
458
+ </sc-if>
459
+ </span>
460
+ </div>
461
+ <sc-if value="{{ r.expanded }}" hint-placeholder-val="{{ false }}">
462
+ <div style="background:#08090b; border-bottom:1px solid #1d1f24; border-left:2px solid {{ r.dotColor }}; animation:slideIn .15s ease;">
463
+ <div style="display:flex; align-items:center; gap:8px; padding:11px 18px; border-bottom:1px solid #15171a; background:#0d0e11;">
464
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-right:2px;">Actions</span>
465
+ <sc-for list="{{ r.actions }}" as="ac" hint-placeholder-count="2">
466
+ <span onClick="{{ ac.run }}" style="font-size:11px; font-weight:600; padding:6px 12px; border-radius:7px; cursor:pointer; color:#cdd0d6; background:#1a1d22; border:1px solid #2a2e35;">{{ ac.label }}</span>
467
+ </sc-for>
468
+ </div>
469
+ <div style="padding:13px 18px 11px; overflow-x:auto;">
470
+ <div style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:9px;">{{ r.detail.count }} steps · 此 run 自己的步骤 · 点步骤切日志</div>
471
+ <div style="display:flex; align-items:center; gap:5px;">
472
+ <sc-for list="{{ r.detail.steps }}" as="st" hint-placeholder-count="6">
473
+ <div onClick="{{ st.onPick }}" style="{{ st.pillStyle }}">
474
+ <span style="color:{{ st.color }}; font-size:11px;">{{ st.glyph }}</span>
475
+ <span style="font-size:11px; color:#d4d7dc;">{{ st.name }}</span>
476
+ </div>
477
+ </sc-for>
478
+ </div>
479
+ </div>
480
+ <div style="display:flex; min-height:172px; border-top:1px solid #15171a;">
481
+ <div style="flex:1.7; min-width:0; display:flex; flex-direction:column; border-right:1px solid #15171a;">
482
+ <div style="display:flex; align-items:center; gap:9px; padding:8px 18px; background:#0d0e11; border-bottom:1px solid #15171a;">
483
+ <span style="color:{{ r.detail.focus.color }}; font-size:12px;">{{ r.detail.focus.glyph }}</span>
484
+ <span style="font-weight:700; color:#f3f4f6; font-size:12.5px;">{{ r.detail.focus.name }}</span>
485
+ <span style="font-size:9px; font-weight:600; padding:1px 6px; border-radius:4px; color:{{ r.detail.focus.kindColor }}; background:{{ r.detail.focus.kindColor }}1c;">{{ r.detail.focus.kind }}</span>
486
+ <span style="flex:1;"></span>
487
+ <span onClick="{{ r.detail.onOpenTaskLog }}" style="font-size:10.5px; color:#4f9dff; cursor:pointer; font-weight:600;">Open task detail →</span>
488
+ </div>
489
+ <div style="flex:1; overflow-y:auto; padding:11px 18px; font-size:11.5px; line-height:1.75; background:#08090b;">
490
+ <sc-for list="{{ r.detail.focusLog }}" as="ln" hint-placeholder-count="5">
491
+ <div style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</div>
492
+ </sc-for>
493
+ </div>
494
+ </div>
495
+ <div style="width:268px; flex-shrink:0; padding:10px 16px; background:#0a0b0d;">
496
+ <div style="font-size:10px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600; margin-bottom:7px;">Step output</div>
497
+ <sc-for list="{{ r.detail.focusOut }}" as="o" hint-placeholder-count="3">
498
+ <div style="display:flex; align-items:baseline; gap:9px; padding:4px 0; border-bottom:1px solid #141619;">
499
+ <span style="font-size:10.5px; color:#6b7079; width:84px; flex-shrink:0;">{{ o.k }}</span>
500
+ <span style="font-size:11.5px; color:{{ o.color }}; font-family:'JetBrains Mono',monospace; word-break:break-all;">{{ o.v }}</span>
501
+ </div>
502
+ </sc-for>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </sc-if>
507
+ </div>
508
+ </sc-for>
509
+ </div>
510
+ </sc-for>
511
+ </sc-if>
512
+ <sc-if value="{{ records.flat }}" hint-placeholder-val="{{ false }}">
513
+ <div style="display:flex; align-items:center; gap:12px; padding:9px 16px; background:#0a0b0d; border-bottom:1px solid #1d1f24; position:sticky; top:0; z-index:2;">
514
+ <span style="width:12px; flex-shrink:0;"></span>
515
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:74px; flex-shrink:0;">Status</span>
516
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:90px; flex-shrink:0;">Pipeline ID</span>
517
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:196px; flex-shrink:0;">Pipeline</span>
518
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:80px; flex-shrink:0;">Run ID</span>
519
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; flex:1; min-width:64px;">Steps</span>
520
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:150px; flex-shrink:0;">Result</span>
521
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:92px; flex-shrink:0;">Trigger</span>
522
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:64px; flex-shrink:0; text-align:right;">Duration</span>
523
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:62px; flex-shrink:0; text-align:right;">Started</span>
524
+ <span style="position:sticky; right:0; font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; width:104px; flex-shrink:0; text-align:right; padding:9px 16px 9px 18px; margin:-9px -16px -9px 0; background:linear-gradient(90deg, transparent, #0a0b0d 26%);">Actions</span>
525
+ </div>
526
+ <sc-for list="{{ records.flat }}" as="r" hint-placeholder-count="10">
527
+ <div>
528
+ <div onClick="{{ r.onToggle }}" style="{{ r.rowStyle }}">
529
+ <span style="font-size:10px; color:#565b63; width:12px; flex-shrink:0;">{{ r.chev }}</span>
530
+ <span style="display:flex; align-items:center; gap:7px; width:74px; flex-shrink:0;">
531
+ <span style="width:8px; height:8px; border-radius:50%; flex-shrink:0; background:{{ r.dotColor }};"></span>
532
+ <span style="font-size:11px; color:{{ r.summaryColor }}; font-weight:600;">{{ r.statusLabel }}</span>
533
+ </span>
534
+ <sc-if value="{{ r.showPipe }}" hint-placeholder-val="{{ true }}">
535
+ <span style="font-size:11.5px; color:#6b7079; width:90px; flex-shrink:0; font-family:'JetBrains Mono',monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.pid }}</span>
536
+ </sc-if>
537
+ <sc-if value="{{ r.showPipe }}" hint-placeholder-val="{{ true }}">
538
+ <span style="font-size:12px; font-weight:600; color:#c2c6cd; width:196px; flex-shrink:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.pipeName }}</span>
539
+ </sc-if>
540
+ <span style="font-size:12px; color:#c9cdd3; width:80px; flex-shrink:0;">{{ r.id }}</span>
541
+ <div style="flex:1; min-width:64px; display:flex; gap:1.5px;">
542
+ <sc-for list="{{ r.segments }}" as="seg" hint-placeholder-count="8">
543
+ <span style="flex:1; height:6px; border-radius:1px; background:{{ seg.bg }};"></span>
544
+ </sc-for>
545
+ </div>
546
+ <span style="font-size:11px; color:{{ r.summaryColor }}; width:150px; flex-shrink:0; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ r.summary }}</span>
547
+ <span style="font-size:10.5px; color:#7d828b; width:92px; flex-shrink:0;">{{ r.trigGlyph }} {{ r.trigger }}</span>
548
+ <span style="font-size:11px; color:#6b7079; width:64px; flex-shrink:0; text-align:right;">{{ r.dur }}</span>
549
+ <span style="font-size:11px; color:#565b63; width:62px; flex-shrink:0; text-align:right;">{{ r.ago }}</span>
550
+ <span style="position:sticky; right:0; width:104px; flex-shrink:0; display:flex; justify-content:flex-end; align-items:center; padding:11px 16px 11px 18px; margin:-11px -16px -11px 0; background:linear-gradient(90deg, transparent, #0c0d10 26%); pointer-events:none;">
551
+ <sc-if value="{{ r.quick.show }}" hint-placeholder-val="{{ false }}">
552
+ <span onClick="{{ r.quick.run }}" style="{{ r.quickStyle }} pointer-events:auto;">{{ r.quick.label }}</span>
553
+ </sc-if>
554
+ </span>
555
+ </div>
556
+ <sc-if value="{{ r.expanded }}" hint-placeholder-val="{{ false }}">
557
+ <div style="background:#08090b; border-bottom:1px solid #1d1f24; border-left:2px solid {{ r.dotColor }}; animation:slideIn .15s ease;">
558
+ <div style="display:flex; align-items:center; gap:8px; padding:11px 18px; border-bottom:1px solid #15171a; background:#0d0e11;">
559
+ <span style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-right:2px;">Actions</span>
560
+ <sc-for list="{{ r.actions }}" as="ac" hint-placeholder-count="2">
561
+ <span onClick="{{ ac.run }}" style="font-size:11px; font-weight:600; padding:6px 12px; border-radius:7px; cursor:pointer; color:#cdd0d6; background:#1a1d22; border:1px solid #2a2e35;">{{ ac.label }}</span>
562
+ </sc-for>
563
+ </div>
564
+ <div style="padding:13px 18px 11px; overflow-x:auto;">
565
+ <div style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:9px;">{{ r.detail.count }} steps · 此 run 自己的步骤 · 点步骤切日志</div>
566
+ <div style="display:flex; align-items:center; gap:5px;">
567
+ <sc-for list="{{ r.detail.steps }}" as="st" hint-placeholder-count="6">
568
+ <div onClick="{{ st.onPick }}" style="{{ st.pillStyle }}">
569
+ <span style="color:{{ st.color }}; font-size:11px;">{{ st.glyph }}</span>
570
+ <span style="font-size:11px; color:#d4d7dc;">{{ st.name }}</span>
571
+ </div>
572
+ </sc-for>
573
+ </div>
574
+ </div>
575
+ <div style="display:flex; min-height:172px; border-top:1px solid #15171a;">
576
+ <div style="flex:1.7; min-width:0; display:flex; flex-direction:column; border-right:1px solid #15171a;">
577
+ <div style="display:flex; align-items:center; gap:9px; padding:8px 18px; background:#0d0e11; border-bottom:1px solid #15171a;">
578
+ <span style="color:{{ r.detail.focus.color }}; font-size:12px;">{{ r.detail.focus.glyph }}</span>
579
+ <span style="font-weight:700; color:#f3f4f6; font-size:12.5px;">{{ r.detail.focus.name }}</span>
580
+ <span style="font-size:9px; font-weight:600; padding:1px 6px; border-radius:4px; color:{{ r.detail.focus.kindColor }}; background:{{ r.detail.focus.kindColor }}1c;">{{ r.detail.focus.kind }}</span>
581
+ <span style="flex:1;"></span>
582
+ <span onClick="{{ r.detail.onOpenTaskLog }}" style="font-size:10.5px; color:#4f9dff; cursor:pointer; font-weight:600;">Open task detail →</span>
583
+ </div>
584
+ <div style="flex:1; overflow-y:auto; padding:11px 18px; font-size:11.5px; line-height:1.75; background:#08090b;">
585
+ <sc-for list="{{ r.detail.focusLog }}" as="ln" hint-placeholder-count="5">
586
+ <div style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</div>
587
+ </sc-for>
588
+ </div>
589
+ </div>
590
+ <div style="width:268px; flex-shrink:0; padding:10px 16px; background:#0a0b0d;">
591
+ <div style="font-size:10px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600; margin-bottom:7px;">Step output</div>
592
+ <sc-for list="{{ r.detail.focusOut }}" as="o" hint-placeholder-count="3">
593
+ <div style="display:flex; align-items:baseline; gap:9px; padding:4px 0; border-bottom:1px solid #141619;">
594
+ <span style="font-size:10.5px; color:#6b7079; width:84px; flex-shrink:0;">{{ o.k }}</span>
595
+ <span style="font-size:11.5px; color:{{ o.color }}; font-family:'JetBrains Mono',monospace; word-break:break-all;">{{ o.v }}</span>
596
+ </div>
597
+ </sc-for>
598
+ </div>
599
+ </div>
600
+ </div>
601
+ </sc-if>
602
+ </div>
603
+ </sc-for>
604
+ </sc-if>
605
+ </div>
606
+ </div>
607
+ </div>
608
+ </sc-if>
609
+
610
+ </sc-if>
611
+
612
+ <sc-if value="{{ showTDetail }}" hint-placeholder-val="{{ false }}">
613
+ <div style="font-family:'Inter',sans-serif; margin-bottom:14px; display:flex; align-items:center; gap:10px;">
614
+ <span onClick="{{ fullTask.backToList }}" style="font-size:12px; color:#7d828b; cursor:pointer;">← Task</span>
615
+ <span style="color:#3f434b;">/</span>
616
+ <span style="font-size:12px; color:#9aa0a8;">Task Detail</span>
617
+ </div>
618
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;">
619
+ <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
620
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
621
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
622
+ </sc-for>
623
+ </div>
624
+
625
+ <div style="padding:18px 22px; border-bottom:1px solid #1d1f24; display:flex; align-items:center; gap:13px; flex-wrap:wrap;">
626
+ <span style="width:11px; height:11px; border-radius:50%; background:{{ fullTask.statusColor }}; box-shadow:0 0 8px {{ fullTask.statusColor }}77;"></span>
627
+ <span style="font-size:19px; font-weight:700; color:#f3f4f6; font-family:'JetBrains Mono',monospace;">{{ fullTask.meta.step }}</span>
628
+ <span style="font-size:11px; font-weight:700; letter-spacing:.5px; padding:3px 10px; border-radius:6px; text-transform:uppercase; color:{{ fullTask.statusColor }}; background:{{ fullTask.statusColor }}1f;">{{ fullTask.meta.status }}</span>
629
+ <span style="font-size:12px; color:#6b7079; font-family:'JetBrains Mono',monospace;">{{ fullTask.meta.id }}</span>
630
+ <span style="flex:1;"></span>
631
+ <sc-if value="{{ fullTask.retryable }}" hint-placeholder-val="{{ false }}">
632
+ <span onClick="{{ fullTask.onRetry }}" style="font-size:12px; font-weight:700; padding:8px 16px; border-radius:8px; cursor:pointer; color:#0b0c0e; background:#ff7d2e;">↻ Retry</span>
633
+ </sc-if>
634
+ <sc-if value="{{ fullTask.running }}" hint-placeholder-val="{{ false }}">
635
+ <span onClick="{{ fullTask.onCancel }}" style="font-size:12px; font-weight:700; padding:8px 16px; border-radius:8px; cursor:pointer; color:#f15b4a; background:#f15b4a18; border:1px solid #f15b4a55;">■ Cancel</span>
636
+ </sc-if>
637
+ <span style="font-size:12px; font-weight:600; padding:8px 16px; border-radius:8px; cursor:pointer; color:#cdd0d6; background:#1a1d22; border:1px solid #2a2e35;">Re-run</span>
638
+ </div>
639
+
640
+ <sc-if value="{{ fullTask.lineage.hasPipeline }}" hint-placeholder-val="{{ true }}">
641
+ <div onClick="{{ fullTask.lineage.onRun }}" style="display:flex; align-items:center; gap:8px; padding:11px 22px; background:#0e1622; border-bottom:1px solid #1d2b3d; cursor:pointer;">
642
+ <span style="font-size:11px; color:#6b7079;">来自</span>
643
+ <span style="font-size:12px; color:#c2c6cd; font-weight:600;">{{ fullTask.lineage.pipeName }}</span>
644
+ <span style="color:#3f434b;">›</span>
645
+ <span style="font-size:12px; color:#4f9dff; font-family:'JetBrains Mono',monospace;">run {{ fullTask.lineage.runId }}</span>
646
+ <span style="color:#3f434b;">›</span>
647
+ <span style="font-size:12px; color:#c2c6cd;">{{ fullTask.lineage.step }}</span>
648
+ <span style="flex:1;"></span>
649
+ <span style="font-size:11px; color:#4f9dff;">在 Pipeline Record 中查看 ↗</span>
650
+ </div>
651
+ </sc-if>
652
+
653
+ <div style="display:flex; min-width:900px;">
654
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid #1d1f24;">
655
+ <div style="padding:15px 20px; border-bottom:1px solid #16181c;">
656
+ <div style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:8px;">{{ fullTask.cmdLabel }}</div>
657
+ <div style="background:#070809; border:1px solid #20232a; border-radius:8px; padding:12px 14px; font-size:12px; color:#cdd0d6; font-family:'JetBrains Mono',monospace; line-height:1.6; white-space:pre-wrap; word-break:break-all;">{{ fullTask.cmd }}</div>
658
+ </div>
659
+
660
+ <div style="display:flex; align-items:center; gap:10px; padding:11px 20px; background:#0d0e11; border-bottom:1px solid #16181c;">
661
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Log</span>
662
+ <span style="font-size:10px; color:#565b63;">{{ fullTask.allLines }} lines</span>
663
+ <div style="flex:1; display:flex; align-items:center; gap:8px; background:#070809; border:1px solid #20232a; border-radius:7px; padding:5px 11px; margin-left:8px;">
664
+ <span style="color:#565b63; font-size:12px;">⌕</span>
665
+ <input value="{{ taskSearch }}" onChange="{{ onSearch }}" placeholder="grep log…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
666
+ <sc-if value="{{ taskSearch }}" hint-placeholder-val="{{ false }}">
667
+ <span onClick="{{ clearSearch }}" style="color:#6b7079; cursor:pointer; font-size:13px;">×</span>
668
+ </sc-if>
669
+ </div>
670
+ <div onClick="{{ toggleErrors }}" style="{{ errToggleStyle }}">
671
+ <span style="width:8px; height:8px; border-radius:2px; background:#f15b4a; display:inline-block;"></span>
672
+ <span style="font-size:11px;">errors {{ fullTask.errCount }}</span>
673
+ </div>
674
+ </div>
675
+ <div style="display:flex;">
676
+ <div style="flex:1; min-width:0; overflow-y:auto; max-height:300px; padding:13px 0; font-size:12.5px; line-height:1.95; background:#070809;">
677
+ <sc-for list="{{ fullTask.logLines }}" as="ln" hint-placeholder-count="8">
678
+ <div style="display:flex; gap:0; padding:0 20px; background:{{ ln.rowBg }};">
679
+ <span style="color:#34373d; user-select:none; width:26px; text-align:right; flex-shrink:0; margin-right:14px;">{{ ln.n }}</span>
680
+ <span style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</span>
681
+ </div>
682
+ </sc-for>
683
+ </div>
684
+ <div style="width:13px; flex-shrink:0; background:#0a0b0d; border-left:1px solid #16181c; display:flex; flex-direction:column; gap:2px; padding:8px 3px;">
685
+ <sc-for list="{{ fullTask.mini }}" as="mm" hint-placeholder-count="8">
686
+ <span style="width:100%; height:5px; border-radius:1px; background:{{ mm.color }};"></span>
687
+ </sc-for>
688
+ </div>
689
+ </div>
690
+ </div>
691
+
692
+ <div style="width:280px; flex-shrink:0; background:#0a0b0d; display:flex; flex-direction:column;">
693
+ <div style="padding:14px 18px; border-bottom:1px solid #16181c;">
694
+ <div style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:11px;">Details</div>
695
+ <sc-for list="{{ fullTask.facts }}" as="f" hint-placeholder-count="8">
696
+ <div style="display:flex; align-items:baseline; gap:10px; padding:6px 0; border-bottom:1px solid #131519;">
697
+ <span style="font-size:11px; color:#6b7079; width:108px; flex-shrink:0;">{{ f.k }}</span>
698
+ <span style="font-size:12px; color:{{ f.color }}; font-family:'JetBrains Mono',monospace; word-break:break-all;">{{ f.v }}</span>
699
+ </div>
700
+ </sc-for>
701
+ </div>
702
+ <div style="padding:14px 18px;">
703
+ <div style="font-size:10px; letter-spacing:1px; color:#565b63; text-transform:uppercase; margin-bottom:11px;">Phases</div>
704
+ <sc-for list="{{ fullTask.phases }}" as="ph" hint-placeholder-count="4">
705
+ <div style="display:flex; align-items:center; gap:10px; padding:5px 0;">
706
+ <span style="color:{{ ph.color }}; font-size:12px; width:14px; text-align:center;">{{ ph.glyph }}</span>
707
+ <span style="font-size:12px; color:#c2c6cd;">{{ ph.label }}</span>
708
+ </div>
709
+ </sc-for>
710
+ </div>
711
+ </div>
712
+ </div>
713
+
714
+ <div style="display:flex; border-top:1px solid #1d1f24; min-width:900px;">
715
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid #1d1f24;">
716
+ <div style="padding:10px 20px; background:#0d0e11; border-bottom:1px solid #16181c; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Result</div>
717
+ <div style="flex:1; overflow:auto; max-height:200px; padding:13px 20px; font-size:11.5px; line-height:1.7; color:#a6abb3; white-space:pre; background:#08090b;">{{ fullTask.result }}</div>
718
+ </div>
719
+ <div style="flex:1.2; min-width:0; display:flex; flex-direction:column;">
720
+ <div style="padding:10px 20px; background:#0d0e11; border-bottom:1px solid #16181c; display:flex; align-items:center; gap:8px;">
721
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Git Diff</span>
722
+ </div>
723
+ <div style="flex:1; overflow:auto; max-height:200px; padding:8px 0; font-size:11px; line-height:1.7; background:#08090b;">
724
+ <sc-for list="{{ fullTask.diff }}" as="d" hint-placeholder-count="6">
725
+ <div style="padding:0 20px; background:{{ d.bg }}; color:{{ d.color }}; white-space:pre;">{{ d.text }}</div>
726
+ </sc-for>
727
+ </div>
728
+ </div>
729
+ </div>
730
+ </div>
731
+ </sc-if>
732
+
733
+ <sc-if value="{{ showT1 }}" hint-placeholder-val="{{ false }}">
734
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
735
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">T1 · 三栏同屏</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#4f9dff; background:#4f9dff22;">保守</span></div>
736
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">把 Log / Result / Git&nbsp;Diff 三个 tab 拆成<b style="color:#cdd0d6;">同屏并列的三块</b> —— 不用再来回点。顶部加一条面包屑,<b style="color:#cdd0d6;">一键跳回它所属的 pipeline 步骤</b>。</div>
737
+ </div>
738
+
739
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;">
740
+ <div style="display:flex; gap:24px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24; font-size:13px;">
741
+ <span style="padding:13px 2px; color:#6b7079;">Schedules</span>
742
+ <span style="padding:13px 2px; color:#6b7079;">Pipelines</span>
743
+ <span style="padding:13px 2px; color:#f3f4f6; border-bottom:2px solid #4f9dff; font-weight:600;">Tasks</span>
744
+ <span style="flex:1;"></span>
745
+ <span style="font-size:11px; color:#6b7079;">0 running · 0 queued · 1118 done</span>
746
+ </div>
747
+
748
+ <div style="display:flex; min-height:560px; min-width:900px;">
749
+ <div style="width:300px; flex-shrink:0; border-right:1px solid #1d1f24; display:flex; flex-direction:column;">
750
+ <div style="padding:11px 14px; border-bottom:1px solid #1d1f24; display:flex; gap:7px;">
751
+ <span style="font-size:11px; padding:4px 9px; border-radius:6px; background:#4f9dff1f; color:#4f9dff; font-weight:600;">all 1227</span>
752
+ <span style="font-size:11px; padding:4px 9px; color:#6b7079;">failed 97</span>
753
+ </div>
754
+ <div style="overflow-y:auto; flex:1;">
755
+ <sc-for list="{{ tasks }}" as="t" hint-placeholder-count="8">
756
+ <button onClick="{{ t.onSelect }}" style="{{ t.rowStyle }}">
757
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:5px;">
758
+ <span style="font-size:12px; font-weight:700; color:{{ t.provColor }};">{{ t.prov }}</span>
759
+ <span style="flex:1;"></span>
760
+ <span style="font-size:11px; font-weight:600; color:{{ t.statusColor }};">{{ t.status }}</span>
761
+ </div>
762
+ <div style="font-size:11px; color:#8b9098; line-height:1.45; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;">{{ t.snip }}</div>
763
+ <div style="font-size:10px; color:#565b63; margin-top:5px;">{{ t.ago }} {{ t.cost }}</div>
764
+ </button>
765
+ </sc-for>
766
+ </div>
767
+ </div>
768
+
769
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column;">
770
+ <div style="padding:14px 20px; border-bottom:1px solid #1d1f24;">
771
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px; flex-wrap:wrap;">
772
+ <span style="font-size:10.5px; font-weight:700; letter-spacing:.5px; padding:3px 9px; border-radius:6px; text-transform:uppercase; color:{{ selTask.statusColor }}; background:{{ selTask.statusColor }}1f;">{{ selTask.status }}</span>
773
+ <span style="font-size:14px; font-weight:700; color:#f3f4f6;">{{ selTask.prov }}</span>
774
+ <span style="font-size:12px; color:#6b7079;">{{ selTask.id }}</span>
775
+ <span style="flex:1;"></span>
776
+ <span style="font-size:11.5px; color:#0b0c0e; background:#ff7d2e; padding:5px 13px; border-radius:7px; cursor:pointer; font-weight:600;">Re-run</span>
777
+ </div>
778
+ <div onClick="{{ gotoPipeline }}" style="display:inline-flex; align-items:center; gap:7px; font-size:11.5px; color:#4f9dff; cursor:pointer; padding:5px 10px; border:1px solid #1f3550; background:#0e1622; border-radius:7px;">
779
+ <span style="color:#6b7079;">from</span> pipeline 25b1fc48 <span style="color:#3f434b;">›</span> apply-fix <span>↗</span>
780
+ </div>
781
+ </div>
782
+
783
+ <div style="flex:1; display:flex; min-height:0;">
784
+ <div style="flex:1.5; min-width:0; display:flex; flex-direction:column; border-right:1px solid #1d1f24;">
785
+ <div style="padding:9px 16px; background:#0d0e11; border-bottom:1px solid #1d1f24; display:flex; align-items:center; gap:8px;">
786
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Log</span>
787
+ <span style="font-size:10px; color:#565b63;">{{ selTask.allLines }} lines</span>
788
+ </div>
789
+ <div style="flex:1; overflow-y:auto; padding:12px 16px; font-size:12px; line-height:1.85; background:#08090b;">
790
+ <sc-for list="{{ selTask.logLines }}" as="ln" hint-placeholder-count="8">
791
+ <div style="display:flex; gap:12px;">
792
+ <span style="color:#3f434b; user-select:none; width:18px; text-align:right; flex-shrink:0;">{{ ln.n }}</span>
793
+ <span style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</span>
794
+ </div>
795
+ </sc-for>
796
+ </div>
797
+ </div>
798
+
799
+ <div style="width:380px; flex-shrink:0; display:flex; flex-direction:column;">
800
+ <div style="flex:1; display:flex; flex-direction:column; border-bottom:1px solid #1d1f24; min-height:0;">
801
+ <div style="padding:9px 16px; background:#0d0e11; border-bottom:1px solid #1d1f24; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Result</div>
802
+ <div style="flex:1; overflow-y:auto; padding:12px 16px; font-size:11.5px; line-height:1.7; color:#a6abb3; white-space:pre; background:#0a0b0d;">{{ selTask.result }}</div>
803
+ </div>
804
+ <div style="flex:1.3; display:flex; flex-direction:column; min-height:0;">
805
+ <div style="padding:9px 16px; background:#0d0e11; border-bottom:1px solid #1d1f24; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Git Diff</div>
806
+ <div style="flex:1; overflow:auto; padding:8px 0; font-size:11px; line-height:1.7; background:#0a0b0d;">
807
+ <sc-for list="{{ selTask.diff }}" as="d" hint-placeholder-count="10">
808
+ <div style="padding:0 16px; background:{{ d.bg }}; color:{{ d.color }}; white-space:pre;">{{ d.text }}</div>
809
+ </sc-for>
810
+ </div>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </sc-if>
818
+
819
+ <sc-if value="{{ showT2 }}" hint-placeholder-val="{{ false }}">
820
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
821
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">T2 · 日志为主 + 搜索</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#46c25a; background:#46c25a22;">平衡</span></div>
822
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">日志是主角:占满主区,带<b style="color:#cdd0d6;">搜索框、只看错误、错误高亮</b>。Result 与 Git&nbsp;Diff 收进右侧<b style="color:#cdd0d6;">可折叠的停靠面板</b>,需要时点开,不打断读日志。</div>
823
+ </div>
824
+
825
+ <div style="background:#0b0c0e; border:1px solid #20232a; border-radius:12px; overflow:hidden;">
826
+ <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
827
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
828
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
829
+ </sc-for>
830
+ </div>
831
+
832
+ <div style="display:flex; min-height:560px; min-width:900px;">
833
+ <div style="width:250px; flex-shrink:0; border-right:1px solid #1d1f24; overflow-y:auto;">
834
+ <sc-for list="{{ tasks }}" as="t" hint-placeholder-count="8">
835
+ <button onClick="{{ t.onSelect }}" style="{{ t.rowStyle }}">
836
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
837
+ <span style="font-size:11.5px; font-weight:700; color:{{ t.provColor }};">{{ t.prov }}</span>
838
+ <span style="flex:1;"></span>
839
+ <span style="font-size:10.5px; font-weight:600; color:{{ t.statusColor }};">{{ t.status }}</span>
840
+ </div>
841
+ <div style="font-size:10.5px; color:#7d828b; line-height:1.4; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;">{{ t.snip }}</div>
842
+ </button>
843
+ </sc-for>
844
+ </div>
845
+
846
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column;">
847
+ <div style="padding:13px 18px; border-bottom:1px solid #1d1f24; display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
848
+ <span style="font-size:10.5px; font-weight:700; letter-spacing:.5px; padding:3px 9px; border-radius:6px; text-transform:uppercase; color:{{ selTask.statusColor }}; background:{{ selTask.statusColor }}1f;">{{ selTask.status }}</span>
849
+ <span style="font-size:14px; font-weight:700; color:#f3f4f6;">{{ selTask.prov }} · apply-fix</span>
850
+ <span onClick="{{ gotoPipeline }}" style="font-size:11px; color:#4f9dff; cursor:pointer;">↗ pipeline 25b1fc48</span>
851
+ </div>
852
+
853
+ <div style="flex:1; display:flex; min-height:0;">
854
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid #1d1f24;">
855
+ <div style="padding:10px 16px; background:#0d0e11; border-bottom:1px solid #1d1f24; display:flex; align-items:center; gap:10px;">
856
+ <div style="flex:1; display:flex; align-items:center; gap:8px; background:#08090b; border:1px solid #24262b; border-radius:7px; padding:6px 11px;">
857
+ <span style="color:#565b63; font-size:12px;">⌕</span>
858
+ <input value="{{ taskSearch }}" onChange="{{ onSearch }}" placeholder="搜索日志…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
859
+ <sc-if value="{{ taskSearch }}" hint-placeholder-val="{{ false }}">
860
+ <span onClick="{{ clearSearch }}" style="color:#6b7079; cursor:pointer; font-size:13px;">×</span>
861
+ </sc-if>
862
+ </div>
863
+ <div onClick="{{ toggleErrors }}" style="{{ errToggleStyle }}">
864
+ <span style="width:8px; height:8px; border-radius:2px; background:#f15b4a; display:inline-block;"></span>
865
+ <span style="font-size:11px;">errors {{ selTask.errCount }}</span>
866
+ </div>
867
+ </div>
868
+ <div style="flex:1; overflow-y:auto; padding:12px 0; font-size:12.5px; line-height:1.9; background:#08090b;">
869
+ <sc-for list="{{ selTask.logLines }}" as="ln" hint-placeholder-count="8">
870
+ <div style="display:flex; gap:0; padding:0 16px; background:{{ ln.rowBg }};">
871
+ <span style="width:3px; flex-shrink:0; margin-right:11px; border-radius:2px; background:{{ ln.gutter }};"></span>
872
+ <span style="color:#3f434b; user-select:none; width:20px; text-align:right; flex-shrink:0; margin-right:12px;">{{ ln.n }}</span>
873
+ <span style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</span>
874
+ </div>
875
+ </sc-for>
876
+ </div>
877
+ </div>
878
+
879
+ <div style="width:300px; flex-shrink:0; display:flex; flex-direction:column; background:#0a0b0d;">
880
+ <div style="border-bottom:1px solid #1d1f24;">
881
+ <div onClick="{{ toggleResult }}" style="padding:11px 16px; display:flex; align-items:center; gap:9px; cursor:pointer;">
882
+ <span style="font-size:11px; color:#565b63; width:10px;">{{ resultChev }}</span>
883
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Result</span>
884
+ </div>
885
+ <sc-if value="{{ showResult }}" hint-placeholder-val="{{ true }}">
886
+ <div style="padding:0 16px 14px 35px; font-size:11px; line-height:1.7; color:#a6abb3; white-space:pre;">{{ selTask.result }}</div>
887
+ </sc-if>
888
+ </div>
889
+ <div>
890
+ <div onClick="{{ toggleDiff }}" style="padding:11px 16px; display:flex; align-items:center; gap:9px; cursor:pointer;">
891
+ <span style="font-size:11px; color:#565b63; width:10px;">{{ diffChev }}</span>
892
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Git Diff</span>
893
+ <span style="flex:1;"></span>
894
+ <span style="font-size:10px; color:#46c25a;">+6</span>
895
+ <span style="font-size:10px; color:#f15b4a;">-1</span>
896
+ </div>
897
+ <sc-if value="{{ showDiff }}" hint-placeholder-val="{{ true }}">
898
+ <div style="padding:4px 0 12px; font-size:10.5px; line-height:1.65; overflow-x:auto;">
899
+ <sc-for list="{{ selTask.diff }}" as="d" hint-placeholder-count="10">
900
+ <div style="padding:0 16px; background:{{ d.bg }}; color:{{ d.color }}; white-space:pre;">{{ d.text }}</div>
901
+ </sc-for>
902
+ </div>
903
+ </sc-if>
904
+ </div>
905
+ </div>
906
+ </div>
907
+ </div>
908
+ </div>
909
+ </div>
910
+ </sc-if>
911
+
912
+ <sc-if value="{{ showT3 }}" hint-placeholder-val="{{ false }}">
913
+ <div style="font-family:'Inter',sans-serif; margin-bottom:16px;">
914
+ <div style="display:flex; align-items:center; gap:9px; margin-bottom:4px;"><span style="font-size:18px; font-weight:700; color:#f3f4f6;">T3 · 终端驾驶舱</span><span style="font-size:10px; font-weight:700; letter-spacing:.5px; padding:2px 7px; border-radius:5px; color:#ff7d2e; background:#ff7d2e22;">大胆</span></div>
915
+ <div style="font-size:13px; color:#8b9098; line-height:1.6;">把单个 task 变成一个<b style="color:#cdd0d6;">工作台</b>:中间是带搜索、严重度过滤、实时跟随的终端,右缘有<b style="color:#cdd0d6;">错误小地图</b>快速定位;下方 Result 与 Diff 永远并排可见。一屏掌控全部。</div>
916
+ </div>
917
+
918
+ <div style="background:#070809; border:1px solid #20232a; border-radius:12px; overflow:hidden;">
919
+ <div style="display:flex; gap:22px; align-items:center; padding:0 18px; background:#0d0e11; border-bottom:1px solid #1d1f24;">
920
+ <sc-for list="{{ topNav }}" as="tb" hint-placeholder-count="4">
921
+ <div onClick="{{ tb.onClick }}" style="{{ tb.style }}">{{ tb.label }}</div>
922
+ </sc-for>
923
+ </div>
924
+
925
+ <div style="display:flex; min-height:580px; min-width:920px;">
926
+ <div style="width:220px; flex-shrink:0; border-right:1px solid #16181c; overflow-y:auto; background:#0a0b0d;">
927
+ <sc-for list="{{ tasks }}" as="t" hint-placeholder-count="8">
928
+ <button onClick="{{ t.onSelect }}" style="{{ t.rowStyle }}">
929
+ <div style="display:flex; align-items:center; gap:7px;">
930
+ <span style="width:6px; height:6px; border-radius:50%; flex-shrink:0; background:{{ t.statusColor }};"></span>
931
+ <span style="font-size:11px; font-weight:600; color:{{ t.provColor }};">{{ t.prov }}</span>
932
+ <span style="flex:1;"></span>
933
+ <span style="font-size:9.5px; color:{{ t.statusColor }};">{{ t.status }}</span>
934
+ </div>
935
+ <div style="font-size:10px; color:#6b7079; line-height:1.4; margin-top:4px; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;">{{ t.snip }}</div>
936
+ </button>
937
+ </sc-for>
938
+ </div>
939
+
940
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column;">
941
+ <div style="display:flex; align-items:center; gap:10px; padding:10px 16px; background:#0d0e11; border-bottom:1px solid #16181c;">
942
+ <span style="width:9px; height:9px; border-radius:50%; background:#46c25a; animation:blink 1.6s infinite;"></span>
943
+ <span style="font-size:12px; color:#9aa0a8;">task://{{ selTask.id }}</span>
944
+ <span style="color:#3f434b;">·</span>
945
+ <span style="font-size:12px; font-weight:700; color:#f3f4f6;">apply-fix</span>
946
+ <span style="font-size:10.5px; font-weight:700; letter-spacing:.5px; padding:2px 8px; border-radius:5px; text-transform:uppercase; color:{{ selTask.statusColor }}; background:{{ selTask.statusColor }}1f;">{{ selTask.status }}</span>
947
+ <span style="flex:1;"></span>
948
+ <span onClick="{{ gotoPipeline }}" style="font-size:11px; color:#4f9dff; cursor:pointer;">↗ 25b1fc48 › apply-fix</span>
949
+ </div>
950
+
951
+ <div style="display:flex; align-items:center; gap:10px; padding:9px 16px; background:#0a0b0d; border-bottom:1px solid #16181c;">
952
+ <div style="flex:1; display:flex; align-items:center; gap:8px; background:#070809; border:1px solid #20232a; border-radius:7px; padding:6px 11px;">
953
+ <span style="color:#565b63; font-size:12px;">⌕</span>
954
+ <input value="{{ taskSearch }}" onChange="{{ onSearch }}" placeholder="grep log…" style="flex:1; background:transparent; border:none; outline:none; color:#cdd0d6; font-family:'JetBrains Mono',monospace; font-size:12px;" />
955
+ <sc-if value="{{ taskSearch }}" hint-placeholder-val="{{ false }}">
956
+ <span onClick="{{ clearSearch }}" style="color:#6b7079; cursor:pointer; font-size:13px;">×</span>
957
+ </sc-if>
958
+ </div>
959
+ <div onClick="{{ toggleErrors }}" style="{{ errToggleStyle }}">
960
+ <span style="width:8px; height:8px; border-radius:2px; background:#f15b4a; display:inline-block;"></span>
961
+ <span style="font-size:11px;">errors {{ selTask.errCount }}</span>
962
+ </div>
963
+ <div style="display:flex; align-items:center; gap:6px; padding:6px 11px; border-radius:7px; border:1px solid #1f4030; background:#0e1a12; color:#46c25a;">
964
+ <span style="font-size:11px;">⟳ tail</span>
965
+ </div>
966
+ </div>
967
+
968
+ <div style="flex:1; display:flex; min-height:0;">
969
+ <div style="flex:1; overflow-y:auto; padding:14px 0; font-size:12.5px; line-height:1.95; background:#070809;">
970
+ <sc-for list="{{ selTask.logLines }}" as="ln" hint-placeholder-count="10">
971
+ <div style="display:flex; gap:0; padding:0 16px; background:{{ ln.rowBg }};">
972
+ <span style="color:#34373d; user-select:none; width:24px; text-align:right; flex-shrink:0; margin-right:14px;">{{ ln.n }}</span>
973
+ <span style="color:{{ ln.color }}; white-space:pre-wrap;">{{ ln.text }}</span>
974
+ </div>
975
+ </sc-for>
976
+ </div>
977
+ <div style="width:14px; flex-shrink:0; background:#0a0b0d; border-left:1px solid #16181c; display:flex; flex-direction:column; gap:2px; padding:8px 3px;">
978
+ <sc-for list="{{ selTask.mini }}" as="mm" hint-placeholder-count="10">
979
+ <span style="width:100%; height:5px; border-radius:1px; background:{{ mm.color }};"></span>
980
+ </sc-for>
981
+ </div>
982
+ </div>
983
+
984
+ <div style="display:flex; border-top:1px solid #16181c; height:190px; flex-shrink:0;">
985
+ <div style="flex:1; min-width:0; display:flex; flex-direction:column; border-right:1px solid #16181c;">
986
+ <div style="padding:8px 16px; background:#0d0e11; border-bottom:1px solid #16181c; font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Result</div>
987
+ <div style="flex:1; overflow:auto; padding:11px 16px; font-size:11px; line-height:1.7; color:#a6abb3; white-space:pre; background:#08090b;">{{ selTask.result }}</div>
988
+ </div>
989
+ <div style="flex:1.2; min-width:0; display:flex; flex-direction:column;">
990
+ <div style="padding:8px 16px; background:#0d0e11; border-bottom:1px solid #16181c; display:flex; align-items:center; gap:8px;">
991
+ <span style="font-size:11px; letter-spacing:1px; color:#9aa0a8; text-transform:uppercase; font-weight:600;">Git Diff</span>
992
+ <span style="flex:1;"></span>
993
+ <span style="font-size:10px; color:#46c25a;">+6</span>
994
+ <span style="font-size:10px; color:#f15b4a;">-1</span>
995
+ </div>
996
+ <div style="flex:1; overflow:auto; padding:7px 0; font-size:10.5px; line-height:1.7; background:#08090b;">
997
+ <sc-for list="{{ selTask.diff }}" as="d" hint-placeholder-count="10">
998
+ <div style="padding:0 16px; background:{{ d.bg }}; color:{{ d.color }}; white-space:pre;">{{ d.text }}</div>
999
+ </sc-for>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+ </div>
1006
+ </sc-if>
1007
+
1008
+ </main>
1009
+ </div>
1010
+ </div>
1011
+ </x-dc>
1012
+ <script type="text/x-dc" data-dc-script>
1013
+ class Component extends DCLogic {
1014
+ state = {
1015
+ active: 'intro',
1016
+ selRunId: '25b1fc48',
1017
+ openSteps: { 'apply-fix': true },
1018
+ p2Open: 'apply-fix',
1019
+ selTaskId: 't_applyfix',
1020
+ taskSearch: '',
1021
+ errorsOnly: false,
1022
+ p3Filter: 'fail',
1023
+ p3Open: '25b1fc48',
1024
+ selType: 'bugfix',
1025
+ pFocus: null,
1026
+ failedRunsOnly: false,
1027
+ v2Run: '25b1fc48',
1028
+ v2Step: 'apply-fix',
1029
+ recStatus: 'all',
1030
+ recPipe: 'all',
1031
+ recTime: '7d',
1032
+ recTrigger: 'all',
1033
+ recSearch: '',
1034
+ recGroup: 'timeline',
1035
+ expandedRun: '25b1fc48',
1036
+ runFocus: {},
1037
+ expandedGroups: { bugfix: true },
1038
+ mgmtEdit: false,
1039
+ extraRuns: [], cancelledRuns: {}, retrySeq: 0,
1040
+ cancelledTasks: {},
1041
+ expandedTask: 't_applyfix',
1042
+ trStatus: 'all', trProvider: 'all', trPipe: 'all', trTime: '7d', trSearch: '', trCost: false,
1043
+ taskCreateOpen: false,
1044
+ createForm: { provider: 'FortiNAC', mode: 'shell', cmd: '' },
1045
+ createdTasks: [],
1046
+ createSeq: 0,
1047
+ t1Run: true, t1Result: true, t1Diff: true,
1048
+ };
1049
+
1050
+ K = {
1051
+ ok: '#46c25a', fail: '#f15b4a', run: '#4f9dff', skip: '#33363d', pend: '#26282e',
1052
+ orange: '#ff7d2e', blue: '#4f9dff', green: '#46c25a', red: '#f15b4a', purple: '#a978f0', yellow: '#d8a23f',
1053
+ bg: '#08090b', panel: '#0f1013', card: '#15171b', border: '#22252b', line: '#1d1f24',
1054
+ text: '#cdd0d6', dim: '#7d828b', faint: '#565b63',
1055
+ };
1056
+
1057
+ STEPS = [
1058
+ ['discover-ids', 'shell'], ['resolve', 'shell'], ['worktree-setup', 'shell'],
1059
+ ['fetch-bug-details', 'shell'], ['triage', 'default'], ['attach-files', 'shell'],
1060
+ ['plan-fix', 'default'], ['apply-fix', 'shell'], ['run-tests', 'shell'],
1061
+ ['open-mr', 'shell'], ['notify-teams', 'shell'],
1062
+ ];
1063
+ DUR = ['2.1s','1.3s','3.4s','5.2s','18.7s','4.0s','22.1s','6.8s','7.5s','2.2s','1.1s'];
1064
+ STAGES = [[0],[1],[2,3],[4],[5],[6],[7],[8],[9],[10]];
1065
+ PIPE_TYPES = [
1066
+ ['bugfix','mantis-bug-fix-batch',142,23,'fail'],
1067
+ ['triage','inbound-issue-triage',310,18,'fail'],
1068
+ ['canary','release-canary-deploy',61,9,'run'],
1069
+ ['regress','nightly-regression-suite',88,4,'ok'],
1070
+ ['depbump','dependency-bump-automation',209,12,'ok'],
1071
+ ['docs','docs-site-sync',47,1,'ok'],
1072
+ ];
1073
+
1074
+ // status patterns (length 11)
1075
+ P_OK = Array(11).fill('ok');
1076
+ P_APPLY = ['ok','ok','ok','ok','ok','ok','ok','fail','skip','skip','skip'];
1077
+ P_EARLY = ['fail','skip','skip','skip','skip','skip','skip','skip','skip','skip','skip'];
1078
+ P_MID = ['ok','ok','ok','ok','ok','fail','skip','skip','skip','skip','skip'];
1079
+ P_RUN = ['ok','ok','ok','ok','run','pend','pend','pend','pend','pend','pend'];
1080
+
1081
+ RUNS = [
1082
+ ['a4f9c2e1','run','now','—','P_RUN'],
1083
+ ['99634dc5','ok','Jun 11, 11:20 PM','1m 48s','P_OK'],
1084
+ ['cdedb454','ok','Jun 11, 11:13 PM','1m 52s','P_OK'],
1085
+ ['25b1fc48','fail','Jun 11, 07:50 PM','58s','P_APPLY'],
1086
+ ['7026c661','fail','Jun 11, 06:45 PM','12s','P_EARLY'],
1087
+ ['850747c9','ok','Jun 11, 05:46 PM','1m 40s','P_OK'],
1088
+ ['f6c9ff4a','fail','Jun 11, 05:45 PM','9s','P_EARLY'],
1089
+ ['e962b5b5','fail','Jun 10, 11:41 PM','1m 12s','P_MID'],
1090
+ ['41ea10b7','fail','Jun 10, 11:36 PM','11s','P_EARLY'],
1091
+ ['05a738af','fail','Jun 10, 11:33 PM','10s','P_EARLY'],
1092
+ ['69ac564c','ok','Jun 10, 10:33 PM','1m 39s','P_OK'],
1093
+ ['8bf7b81b','ok','Jun 10, 09:43 PM','1m 45s','P_OK'],
1094
+ ['0399a270','ok','Jun 10, 09:32 PM','1m 50s','P_OK'],
1095
+ ['5c805acf','fail','Jun 10, 08:32 PM','13s','P_EARLY'],
1096
+ ['c37431ae','ok','Jun 10, 02:17 PM','1m 41s','P_OK'],
1097
+ ];
1098
+
1099
+ LOGS = {
1100
+ 'discover-ids': ['+ for_each.source consumes outputs.bug_ids as CSV','→ discovered 1 bug: 1296959','✓ wrote outputs.bug_ids'],
1101
+ 'resolve': ['+ cd "$(git rev-parse --show-toplevel)"','+ command -v glab','glab 1.42.0 ok','✓ resolved repo: fortinet/fortinac'],
1102
+ 'worktree-setup': ['+ git worktree add .wt/1296959','Preparing worktree (new branch fix/1296959)','✓ worktree ready'],
1103
+ 'fetch-bug-details': ['→ GET mantis/1296959','Pulled summary, description, 4 notes, 1 attachment','✓ context cached (8.2 KB)'],
1104
+ 'triage': ['You are triaging a Mantis bug to decide if Forge should attempt an auto-fix.','→ decision: ATTEMPT_FIX (confidence 0.81)','✓ triage report written · $0.203'],
1105
+ 'attach-files': ['Found 1 attachment(s) on bug 1296959','→ MD5_detail.png (122093 bytes)','✓ ATTACH_COUNT=1 ATTACH_DIR=.wt/.attachments'],
1106
+ 'plan-fix': ['Analyzing 3 candidate files…','→ plan: patch networkConfig.js loadConfig() empty-guard','✓ plan written · $0.340'],
1107
+ 'apply-fix': ['+ git apply --check /tmp/fix-1296959.patch','error: patch failed: src/campusMgr/bin/networkConfig.js:142','error: src/campusMgr/bin/networkConfig.js: patch does not apply','hint: 3 hunks FAILED to apply cleanly','Retrying with 3-way merge…','error: applying 3-way merge failed: conflicting changes','✖ apply-fix exited with code 1'],
1108
+ 'run-tests': ['— skipped: upstream step apply-fix failed'],
1109
+ 'open-mr': ['— skipped: upstream step apply-fix failed'],
1110
+ 'notify-teams': ['— skipped: upstream step apply-fix failed'],
1111
+ };
1112
+
1113
+ TASKS = [
1114
+ ['t_applyfix','FortiNAC','failed','apply-fix · git apply /tmp/fix-1296959.patch','7m ago','','25b1fc48'],
1115
+ ['c2179ec2','FortiNAC','done','# Download all Attached Files from the Mantis bug into the worktree','5m ago','','25b1fc48'],
1116
+ ['t_triage','FortiNAC','done','You are triaging a Mantis bug to decide if Forge should auto-fix','5m ago','$0.203','25b1fc48'],
1117
+ ['t_plan','FortiNAC','done','# Plan the fix — patch networkConfig.js loadConfig empty-guard','6m ago','$0.340','25b1fc48'],
1118
+ ['t_resolve','FortiNAC','done','set -e cd "$(git rev-parse --show-toplevel)" && command -v glab','7m ago','','25b1fc48'],
1119
+ ['t_scratch1','scratch','failed','set -e cd "$(git rev-parse --show-toplevel)" && ./run.sh','7m ago','',''],
1120
+ ['t_fetch','FortiNAC','done','# Pull the FULL mantis bug — summary, description, notes','15m ago','','25b1fc48'],
1121
+ ['t_scratch2','scratch','failed','set -e cd "$(git rev-parse --show-toplevel)" && pytest -q','17m ago','',''],
1122
+ ];
1123
+
1124
+ TASK_LOG = {
1125
+ 't_applyfix': [
1126
+ '$ cd "$(git rev-parse --show-toplevel)"',
1127
+ '$ git fetch origin && git checkout -b fix/mantis-1296959',
1128
+ "Switched to a new branch 'fix/mantis-1296959'",
1129
+ '$ git apply --check /tmp/fix-1296959.patch',
1130
+ 'error: patch failed: src/campusMgr/bin/networkConfig.js:142',
1131
+ 'error: src/campusMgr/bin/networkConfig.js: patch does not apply',
1132
+ 'hint: 3 hunks FAILED to apply cleanly',
1133
+ 'Retrying with 3-way merge…',
1134
+ 'error: applying 3-way merge failed: conflicting changes',
1135
+ '✖ apply-fix exited with code 1',
1136
+ ],
1137
+ 'c2179ec2': [
1138
+ '$ download_attachments --bug 1296959',
1139
+ 'Found 1 attachment(s) on bug 1296959',
1140
+ '→ MD5_detail.png (122093 bytes)',
1141
+ 'ATTACH_COUNT=0 ATTACH_SKIPPED=0 ATTACH_DIR=SKIP/.attachments',
1142
+ '✓ done in 3.1s',
1143
+ ],
1144
+ };
1145
+
1146
+ TASK_RESULT = {
1147
+ 't_applyfix': '{\n "status": "failed",\n "bug_id": "1296959",\n "branch": "fix/mantis-1296959",\n "patch": "/tmp/fix-1296959.patch",\n "hunks_failed": 3,\n "exit_code": 1\n}',
1148
+ 'c2179ec2': '{\n "status": "done",\n "bug_id": "1296959",\n "attach_count": 1,\n "attach_dir": ".wt/.attachments",\n "bytes": 122093\n}',
1149
+ };
1150
+
1151
+ TASK_DIFF = {
1152
+ 't_applyfix': [
1153
+ ['diff --git a/src/campusMgr/bin/networkConfig.js b/src/campusMgr/bin/networkConfig.js','meta'],
1154
+ ['@@ -139,7 +139,11 @@ function loadConfig(path) {','hunk'],
1155
+ [' function loadConfig(path) {','ctx'],
1156
+ ['- const cfg = readFileSync(path, "utf8");','del'],
1157
+ ['+ const raw = readFileSync(path, "utf8");','add'],
1158
+ ['+ if (!raw.trim()) {','add'],
1159
+ ['+ throw new ConfigError("empty networkConfig at " + path);','add'],
1160
+ ['+ }','add'],
1161
+ ['+ const cfg = raw;','add'],
1162
+ [' return JSON.parse(cfg);','ctx'],
1163
+ [' }','ctx'],
1164
+ ],
1165
+ 'c2179ec2': [['— no changes (read-only step)','ctx']],
1166
+ };
1167
+
1168
+ // ---------- helpers ----------
1169
+ statusMeta(s) {
1170
+ const K = this.K;
1171
+ if (s === 'ok') return { color: K.ok, glyph: '✓', bar: K.ok, label: 'passed' };
1172
+ if (s === 'fail') return { color: K.fail, glyph: '✕', bar: K.fail, label: 'failed' };
1173
+ if (s === 'run') return { color: K.run, glyph: '◐', bar: K.run, label: 'running' };
1174
+ if (s === 'skip') return { color: '#5a5f67', glyph: '–', bar: K.skip, label: 'skipped' };
1175
+ return { color: '#4a4e56', glyph: '·', bar: K.pend, label: 'pending' };
1176
+ }
1177
+ pat(name) { return this[name] || this.P_OK; }
1178
+ taskStatusColor(s) { return s === 'done' ? this.K.blue : s === 'failed' ? this.K.red : s === 'running' ? this.K.run : this.K.dim; }
1179
+
1180
+ set(patch) { this.setState(patch); }
1181
+
1182
+ // ---------- nav ----------
1183
+ navItem(id, label, icon, tag, desc) {
1184
+ const K = this.K, active = this.state.active === id;
1185
+ const tagColor = tag === '保守' ? K.blue : tag === '平衡' ? K.green : K.orange;
1186
+ const base = 'padding:11px 12px; border-radius:9px; cursor:pointer; margin-bottom:6px; transition:background .12s; border:1px solid ' + (active ? '#2f333b' : 'transparent') + '; background:' + (active ? '#16181c' : 'transparent') + ';';
1187
+ return {
1188
+ onSelect: () => this.set({ active: id }),
1189
+ style: base,
1190
+ icon, label, tag, desc,
1191
+ titleColor: active ? '#f3f4f6' : '#c2c6cd',
1192
+ tagStyle: tag ? ('font-size:9.5px; font-weight:700; letter-spacing:.5px; padding:2px 6px; border-radius:5px; color:' + tagColor + '; background:' + tagColor + '22;') : 'display:none;',
1193
+ };
1194
+ }
1195
+
1196
+ // ---------- pipeline view models ----------
1197
+ runsVM() {
1198
+ const K = this.K, sel = this.state.selRunId;
1199
+ return this.RUNS.map(([id, status, ago, dur, patName]) => {
1200
+ const pat = this.pat(patName);
1201
+ const m = this.statusMeta(status);
1202
+ const segments = pat.map(s => ({ bg: this.statusMeta(s).bar }));
1203
+ const ok = pat.filter(s => s === 'ok').length;
1204
+ const att = pat.filter(s => s === 'ok' || s === 'fail').length;
1205
+ const isSel = id === sel;
1206
+ return {
1207
+ id, ago, dur, status,
1208
+ dotColor: m.color,
1209
+ running: status === 'run',
1210
+ pass: status === 'run' ? (ok + '/…') : (ok + '/' + att),
1211
+ passColor: status === 'fail' ? K.red : status === 'run' ? K.run : K.dim,
1212
+ segments,
1213
+ onSelect: () => this.set({ selRunId: id, pFocus: null }),
1214
+ rowStyle: 'display:block; width:100%; text-align:left; padding:9px 11px; border:none; border-left:2px solid ' + (isSel ? m.color : 'transparent') + '; background:' + (isSel ? '#15171b' : 'transparent') + '; cursor:pointer; border-bottom:1px solid #15171a;',
1215
+ chipStyle: 'display:flex; align-items:center; gap:7px; padding:6px 11px; border-radius:7px; cursor:pointer; white-space:nowrap; flex-shrink:0; border:1px solid ' + (isSel ? m.color + '88' : '#22252b') + '; background:' + (isSel ? m.color + '16' : 'transparent') + ';',
1216
+ };
1217
+ });
1218
+ }
1219
+
1220
+ detailVM() {
1221
+ const K = this.K;
1222
+ const row = this.RUNS.find(r => r[0] === this.state.selRunId) || this.RUNS[3];
1223
+ const [id, status, ago, dur] = row;
1224
+ const pat = this.pat(row[4]);
1225
+ const m = this.statusMeta(status);
1226
+ const steps = this.STEPS.map(([name, kind], i) => {
1227
+ const s = pat[i], sm = this.statusMeta(s);
1228
+ const open = !!this.state.openSteps[name];
1229
+ const kindColor = kind === 'default' ? K.purple : K.yellow;
1230
+ return {
1231
+ name, kind, kindColor, dur: (s === 'skip' || s === 'pend') ? '—' : this.DUR[i],
1232
+ glyph: sm.glyph, color: sm.color, isFailed: s === 'fail', isRunning: s === 'run',
1233
+ open, chev: open ? '▾' : '▸',
1234
+ rowStyle: 'display:flex; align-items:center; gap:11px; width:100%; text-align:left; padding:10px 13px; border:none; background:' + (s === 'fail' ? '#1a1315' : 'transparent') + '; border-bottom:1px solid #16181c; cursor:pointer;',
1235
+ log: (this.LOGS[name] || ['(no output)']).map(t => ({ text: t, color: /error|✖|FAILED/.test(t) ? K.red : t.startsWith('✓') ? K.green : t.startsWith('→') ? K.blue : '#9aa0a8' })),
1236
+ onToggle: () => this.set({ openSteps: { ...this.state.openSteps, [name]: !open } }),
1237
+ };
1238
+ });
1239
+ const okN = pat.filter(s => s === 'ok').length;
1240
+ const failN = pat.filter(s => s === 'fail').length;
1241
+ const skipN = pat.filter(s => s === 'skip').length;
1242
+ const fs = steps.find(s => s.isFailed) || null;
1243
+ return {
1244
+ id, status, ago, dur, statusColor: m.color, statusLabel: m.label,
1245
+ summary: okN + ' passed · ' + failN + ' failed · ' + skipN + ' skipped',
1246
+ segments: pat.map(s => ({ bg: this.statusMeta(s).bar })),
1247
+ steps, failedStep: fs,
1248
+ taskId: row[4] === 'P_APPLY' ? 't_applyfix' : null,
1249
+ };
1250
+ }
1251
+
1252
+ // ---------- task view models ----------
1253
+ tasksVM() {
1254
+ const K = this.K, sel = this.state.selTaskId;
1255
+ return this.TASKS.map(([id, prov, status, snip, ago, cost, runId]) => {
1256
+ const c = this.taskStatusColor(status);
1257
+ const isSel = id === sel;
1258
+ return {
1259
+ id, prov, status, snip, ago, cost,
1260
+ statusColor: c, provColor: prov === 'scratch' ? K.dim : '#c2c6cd',
1261
+ onSelect: () => this.set({ selTaskId: id }),
1262
+ rowStyle: 'display:block; width:100%; text-align:left; padding:11px 13px; border:none; border-left:2px solid ' + (isSel ? c : 'transparent') + '; background:' + (isSel ? '#15171b' : 'transparent') + '; cursor:pointer; border-bottom:1px solid #15171a;',
1263
+ };
1264
+ });
1265
+ }
1266
+
1267
+ taskMeta(id) {
1268
+ const nameById = {}; this.PIPE_DEFS.forEach(p => nameById[p.id] = p.name);
1269
+ const c = this.state.createdTasks.find(t => t.id === id);
1270
+ if (c) return { id, status: c.status, provider: c.provider, summary: c.summary, step: c.step, pipe: c.pipe || '', pipeName: c.pipe ? nameById[c.pipe] : 'ad-hoc task', runId: c.runId || '', cost: c.cost || '', ago: c.ago || 'just now', dur: c.dur || '—' };
1271
+ const t = this.TASKREC.find(x => x[0] === id);
1272
+ if (t) return { id, status: t[1], provider: t[2], summary: t[5], step: t[4] || '—', pipe: t[3] || '', pipeName: t[3] ? nameById[t[3]] : 'scratch (ad-hoc)', runId: t[10] || '', cost: t[6] || '', ago: t[7] || '', dur: t[8] || '—' };
1273
+ const l = this.TASKS.find(x => x[0] === id);
1274
+ if (l) return { id, status: l[2], provider: l[1], summary: l[3], step: '—', pipe: '', pipeName: '—', runId: l[6] || '', cost: l[5] || '', ago: l[4] || '', dur: '—' };
1275
+ return { id, status: 'done', provider: '—', summary: id, step: '—', pipe: '', pipeName: '—', runId: '', cost: '', ago: '', dur: '—' };
1276
+ }
1277
+ taskIdForRunStep(runId, stepName) {
1278
+ let t = this.TASKREC.find(x => x[10] === runId && x[4] === stepName);
1279
+ if (!t) t = this.TASKREC.find(x => x[4] === stepName);
1280
+ return t ? t[0] : 't_applyfix';
1281
+ }
1282
+ openTaskDetail(id) { this.set({ active: 'tdetail', selTaskId: id, taskSearch: '', errorsOnly: false }); }
1283
+ synthLog(meta) {
1284
+ const s = meta.status;
1285
+ if (s === 'running') return ['$ ' + meta.summary, '… task running — streaming output', '◐ in progress'];
1286
+ if (s === 'failed') return ['$ ' + meta.summary, 'error: command exited non-zero', '✖ task failed (exit 1)'];
1287
+ return ['$ ' + meta.summary, '✓ task completed'];
1288
+ }
1289
+ taskDetailVM(id) {
1290
+ const K = this.K;
1291
+ const meta = this.taskMeta(id);
1292
+ if (this.state.cancelledTasks[id] && meta.status === 'running') meta.status = 'failed';
1293
+ const lines = this.TASK_LOG[id] || this.synthLog(meta);
1294
+ const q = this.state.taskSearch.trim().toLowerCase();
1295
+ const errRe = /error|✖|fail|conflict/i;
1296
+ let filtered = lines.map((t, i) => {
1297
+ const isErr = errRe.test(t);
1298
+ return {
1299
+ n: i + 1, text: t, isErr,
1300
+ color: isErr ? K.red : t.startsWith('✓') ? K.green : t.startsWith('$') ? K.blue : t.startsWith('→') ? K.blue : '#a6abb3',
1301
+ gutter: isErr ? K.red : 'transparent',
1302
+ rowBg: isErr ? '#f15b4a10' : 'transparent',
1303
+ };
1304
+ });
1305
+ if (this.state.errorsOnly) filtered = filtered.filter(l => l.isErr);
1306
+ if (q) filtered = filtered.filter(l => l.text.toLowerCase().includes(q));
1307
+ const errCount = lines.filter(t => errRe.test(t)).length;
1308
+ const diff = (this.TASK_DIFF[id] || [['— no changes for this task', 'ctx']]).map(([text, type]) => ({
1309
+ text, color: type === 'add' ? K.green : type === 'del' ? K.red : type === 'hunk' ? K.purple : type === 'meta' ? K.dim : '#9aa0a8',
1310
+ bg: type === 'add' ? '#46c25a14' : type === 'del' ? '#f15b4a14' : 'transparent',
1311
+ }));
1312
+ const mini = lines.map(t => ({ color: errRe.test(t) ? K.red : '#2f3239', tall: errRe.test(t) }));
1313
+ return {
1314
+ id, prov: meta.provider, status: meta.status, snip: meta.summary, ago: meta.ago, cost: meta.cost, runId: meta.runId,
1315
+ provider: meta.provider, pipeName: meta.pipeName, step: meta.step,
1316
+ statusColor: this.taskStatusColor(meta.status),
1317
+ logLines: filtered, allLines: lines.length, errCount, mini,
1318
+ result: this.TASK_RESULT[id] || ('{\n "status": "' + meta.status + '",\n "task": "' + id + '"\n}'),
1319
+ diff,
1320
+ retryable: meta.status === 'failed', running: meta.status === 'running',
1321
+ onRetry: (e) => { if (e) e.stopPropagation(); const n = this.state.createSeq + 1; const t = { id: 't_re' + n, status: 'running', provider: meta.provider, pipe: '', step: meta.step, summary: meta.summary, cost: '', ago: 'just now', dur: '—', hrs: 0, runId: '' }; this.set({ createdTasks: [t, ...this.state.createdTasks], createSeq: n, expandedTask: t.id, selTaskId: t.id, trStatus: 'all', trProvider: 'all', trSearch: '' }); },
1322
+ onCancel: (e) => { if (e) e.stopPropagation(); this.set({ cancelledTasks: { ...this.state.cancelledTasks, [id]: true } }); },
1323
+ };
1324
+ }
1325
+ selTaskVM() { return this.taskDetailVM(this.state.selTaskId); }
1326
+
1327
+ fullTaskVM() {
1328
+ const K = this.K, id = this.state.selTaskId;
1329
+ const meta = this.taskMeta(id);
1330
+ if (this.state.cancelledTasks[id] && meta.status === 'running') meta.status = 'failed';
1331
+ const base = this.taskDetailVM(id);
1332
+ const isLlm = !!this.LLM_STEPS[meta.step];
1333
+ const cmd = (this.STEP_CMD[meta.step] || ['shell', meta.summary])[1];
1334
+ const sc = this.taskStatusColor(meta.status);
1335
+ const exit = meta.status === 'failed' ? '1' : meta.status === 'running' ? '—' : '0';
1336
+ const startClock = '11:' + (10 + (id.length * 7) % 49) + ' PM';
1337
+ // identity / facts
1338
+ const facts = [
1339
+ ['Task ID', id, '#c9cdd3'],
1340
+ ['Status', meta.status, sc],
1341
+ ['Type', isLlm ? 'LLM prompt' : 'Shell command', isLlm ? K.purple : K.yellow],
1342
+ ['Provider / Repo', meta.provider, '#c9cdd3'],
1343
+ ['Exit code', exit, exit === '0' ? K.green : exit === '1' ? K.red : '#8b9098'],
1344
+ ['Started', meta.ago + ' · ' + startClock, '#8b9098'],
1345
+ ['Duration', meta.dur, '#8b9098'],
1346
+ ['Cost', meta.cost || (isLlm ? '$0.000' : '—'), meta.cost ? K.purple : '#8b9098'],
1347
+ ];
1348
+ if (isLlm) { facts.push(['Model', 'claude-sonnet-4.6', '#8b9098']); facts.push(['Tokens', '4.2k in · 1.1k out', '#8b9098']); }
1349
+ if (meta.pipe === 'bugfix') facts.push(['Branch', 'fix/mantis-1296959', K.blue]);
1350
+ // lineage
1351
+ const lineage = {
1352
+ hasPipeline: meta.pipe && meta.runId,
1353
+ pipeName: meta.pipeName, runId: meta.runId, step: meta.step,
1354
+ onRun: () => this.set({ active: 'p1', expandedRun: meta.runId, recStatus: 'all', recPipe: 'all', recTime: 'all', recGroup: 'timeline' }),
1355
+ };
1356
+ const phases = (meta.status === 'failed'
1357
+ ? [['queued', 'ok'], ['provision env', 'ok'], ['execute', 'fail'], ['collect output', 'skip']]
1358
+ : meta.status === 'running'
1359
+ ? [['queued', 'ok'], ['provision env', 'ok'], ['execute', 'run'], ['collect output', 'pend']]
1360
+ : [['queued', 'ok'], ['provision env', 'ok'], ['execute', 'ok'], ['collect output', 'ok']]
1361
+ ).map(([label, s]) => ({ label, color: this.statusMeta(s).color, glyph: this.statusMeta(s).glyph }));
1362
+ return {
1363
+ ...base, meta, cmd, isLlm, cmdLabel: isLlm ? 'Prompt' : 'Command',
1364
+ statusColor: sc, exit, facts: facts.map(f => ({ k: f[0], v: f[1], color: f[2] })), lineage, phases,
1365
+ retryable: meta.status === 'failed', running: meta.status === 'running',
1366
+ onRetry: base.onRetry, onCancel: base.onCancel,
1367
+ backToList: () => this.set({ active: 'trecord' }),
1368
+ };
1369
+ }
1370
+
1371
+ // DAG columns (dependency stages) for P2
1372
+ p2Cols(detail) {
1373
+ const K = this.K;
1374
+ const stages = [[0],[1],[2,3],[4],[5],[6],[7],[8],[9],[10]];
1375
+ return stages.map((idxs, ci) => ({
1376
+ showArrow: ci > 0,
1377
+ steps: idxs.map(i => {
1378
+ const s = detail.steps[i];
1379
+ const picked = s.name === this.state.p2Open;
1380
+ const c = s.color;
1381
+ return {
1382
+ name: s.name, glyph: s.glyph, color: c, dur: s.dur, isFailed: s.isFailed,
1383
+ onPick: () => this.set({ p2Open: s.name }),
1384
+ chipStyle: 'display:flex; align-items:center; gap:7px; padding:8px 11px; border-radius:8px; cursor:pointer; white-space:nowrap; border:1px solid ' + (picked ? c : '#2a2d34') + '; background:' + (picked ? c + '1c' : '#101216') + ';' + (s.isFailed ? ' animation:pulseRed 2s infinite;' : ''),
1385
+ };
1386
+ }),
1387
+ }));
1388
+ }
1389
+
1390
+ // P3 failure-first command center
1391
+ p3() {
1392
+ const K = this.K;
1393
+ const filt = this.state.p3Filter;
1394
+ const all = this.RUNS.map(([id, status, ago, dur, pat]) => ({ id, status, ago, dur, pat: this.pat(pat) }));
1395
+ const counts = {
1396
+ all: all.length,
1397
+ fail: all.filter(r => r.status === 'fail').length,
1398
+ run: all.filter(r => r.status === 'run').length,
1399
+ ok: all.filter(r => r.status === 'ok').length,
1400
+ };
1401
+ const match = r => filt === 'all' ? true : filt === 'fail' ? r.status === 'fail' : filt === 'run' ? r.status === 'run' : r.status === 'ok';
1402
+ const feed = all.filter(match).map(r => {
1403
+ const fi = r.pat.findIndex(s => s === 'fail');
1404
+ const ri = r.pat.findIndex(s => s === 'run');
1405
+ const stepName = fi >= 0 ? this.STEPS[fi][0] : ri >= 0 ? this.STEPS[ri][0] : '—';
1406
+ const okN = r.pat.filter(s => s === 'ok').length;
1407
+ const log = (fi >= 0 || ri >= 0) ? (this.LOGS[stepName] || []) : [];
1408
+ const errLine = log.filter(t => /error|✖|FAILED/i.test(t)).slice(-1)[0] || (log.length ? log[log.length - 1] : '—');
1409
+ const open = this.state.p3Open === r.id;
1410
+ const sc = this.statusMeta(r.status).color;
1411
+ return {
1412
+ id: r.id, ago: r.ago, dur: r.dur, status: r.status, statusColor: sc,
1413
+ stepName, stepIndex: 'step ' + (Math.max(fi, ri) + 1) + '/11', okBadge: okN + ' passed',
1414
+ errLine, isFail: r.status === 'fail', isRun: r.status === 'run', open,
1415
+ chev: open ? '收起 ▾' : '展开日志 ▸',
1416
+ cardStyle: 'border:1px solid ' + (open ? sc + '66' : '#22252b') + '; border-left:3px solid ' + sc + '; background:' + (open ? '#121317' : '#0f1013') + '; border-radius:10px; margin-bottom:11px; overflow:hidden;',
1417
+ onToggle: () => this.set({ p3Open: open ? '' : r.id }),
1418
+ onOpenTask: () => this.set({ active: 't1', selTaskId: 't_applyfix' }),
1419
+ log: log.map(t => ({ text: t, color: /error|✖|FAILED/.test(t) ? K.red : t.startsWith('✓') ? K.green : t.startsWith('→') ? K.blue : '#9aa0a8' })),
1420
+ };
1421
+ });
1422
+ const heat = all.map(r => ({ color: this.statusMeta(r.status).color, title: r.id + ' · ' + r.status }));
1423
+ const filters = [
1424
+ ['all', 'All', counts.all], ['fail', 'Failed', counts.fail], ['run', 'Running', counts.run], ['ok', 'Passed', counts.ok],
1425
+ ].map(([id, label, n]) => {
1426
+ const active = filt === id;
1427
+ const c = id === 'fail' ? K.red : id === 'run' ? K.run : id === 'ok' ? K.green : '#c2c6cd';
1428
+ return {
1429
+ label, n, onSelect: () => this.set({ p3Filter: id }),
1430
+ style: 'display:flex; align-items:center; gap:8px; padding:9px 13px; border-radius:8px; cursor:pointer; border:1px solid ' + (active ? c + '55' : '#22252b') + '; background:' + (active ? c + '18' : 'transparent') + ';',
1431
+ labelColor: active ? c : '#9aa0a8', countColor: active ? c : '#6b7079',
1432
+ };
1433
+ });
1434
+ return { feed, heat, filters, failRate: Math.round(counts.fail / counts.all * 100) + '%', topFail: 'apply-fix', topFailN: counts.fail, total: counts.all };
1435
+ }
1436
+
1437
+ // ----- pipeline TYPES nav (solves: many types × many records) -----
1438
+ typesVM() {
1439
+ const K = this.K;
1440
+ return this.PIPE_TYPES.map(([id, name, runs, fails, last]) => {
1441
+ const sel = id === this.state.selType;
1442
+ const lc = this.statusMeta(last).color;
1443
+ return {
1444
+ id, name, runs, fails, sel, dot: lc, failBadge: fails > 0,
1445
+ onSelect: () => this.set({ selType: id }),
1446
+ nameColor: sel ? '#f3f4f6' : '#c2c6cd',
1447
+ rowStyle: 'display:block; width:100%; text-align:left; padding:10px 12px; border:none; border-left:2px solid ' + (sel ? K.orange : 'transparent') + '; background:' + (sel ? '#15171b' : 'transparent') + '; cursor:pointer; border-bottom:1px solid #131519;',
1448
+ chipStyle: 'display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:8px; cursor:pointer; white-space:nowrap; border:1px solid ' + (sel ? K.orange + '66' : '#22252b') + '; background:' + (sel ? K.orange + '14' : 'transparent') + ';',
1449
+ chipNameColor: sel ? '#f3f4f6' : '#9aa0a8',
1450
+ };
1451
+ });
1452
+ }
1453
+
1454
+ runsForTypeVM() {
1455
+ let list = this.runsVM();
1456
+ if (this.state.failedRunsOnly) list = list.filter(r => r.status === 'fail');
1457
+ return list;
1458
+ }
1459
+
1460
+ outputFor(name, status, dur) {
1461
+ const K = this.K;
1462
+ const map = {
1463
+ 'apply-fix': [['exit code','1','red'],['branch','fix/1296959',''],['patch','/tmp/fix-1296959.patch',''],['hunks failed','3','red'],['conflict','networkConfig.js','red']],
1464
+ 'triage': [['decision','ATTEMPT_FIX','green'],['confidence','0.81',''],['cost','$0.203','']],
1465
+ 'plan-fix': [['files touched','1',''],['cost','$0.340','']],
1466
+ 'attach-files': [['attachments','1',''],['bytes','122093','']],
1467
+ 'discover-ids': [['bugs found','1',''],['bug_ids','1296959','']],
1468
+ 'fetch-bug-details': [['notes','4',''],['cached','8.2 KB','']],
1469
+ };
1470
+ let rows = map[name];
1471
+ if (!rows) rows = status === 'skip' ? [['—','skipped','']] : status === 'run' ? [['status','running','run'],['elapsed',dur,'']] : [['exit code','0','green'],['duration',dur,'']];
1472
+ else if (status === 'ok' && name !== 'apply-fix') rows = [['exit code','0','green'], ...rows];
1473
+ return rows.map(([k, v, c]) => ({ k, v, color: c === 'red' ? K.red : c === 'green' ? K.green : c === 'run' ? K.run : '#c8ccd2' }));
1474
+ }
1475
+
1476
+ // ----- shared horizontal track builder (V1 + V3) -----
1477
+ buildTrack(runId, focusName) {
1478
+ const K = this.K;
1479
+ const row = this.RUNS.find(r => r[0] === runId) || this.RUNS[3];
1480
+ const [id, status, ago, dur] = row;
1481
+ const pat = this.pat(row[4]);
1482
+ const m = this.statusMeta(status);
1483
+ let focusIdx = pat.findIndex(s => s === 'fail');
1484
+ if (focusName) { const fi = this.STEPS.findIndex(s => s[0] === focusName); if (fi >= 0) focusIdx = fi; }
1485
+ if (focusIdx < 0) focusIdx = pat.findIndex(s => s === 'run');
1486
+ if (focusIdx < 0) { for (let i = pat.length - 1; i >= 0; i--) { if (pat[i] === 'ok') { focusIdx = i; break; } } }
1487
+ if (focusIdx < 0) focusIdx = 0;
1488
+ const stages = this.STAGES.map((idxs, ci) => ({
1489
+ showArrow: ci > 0,
1490
+ parallel: idxs.length > 1,
1491
+ isFork: idxs.length > 1,
1492
+ isMerge: idxs.length === 1 && ci > 0 && this.STAGES[ci - 1].length > 1,
1493
+ isLine: ci > 0 && idxs.length === 1 && this.STAGES[ci - 1].length === 1,
1494
+ steps: idxs.map(i => {
1495
+ const s = pat[i], sm = this.statusMeta(s);
1496
+ const [name, kind] = this.STEPS[i];
1497
+ const kindColor = kind === 'default' ? K.purple : K.yellow;
1498
+ const isFocus = i === focusIdx;
1499
+ return {
1500
+ idx: i, name, kind, kindColor, status: s, glyph: sm.glyph, color: sm.color,
1501
+ dur: (s === 'skip' || s === 'pend') ? '—' : this.DUR[i],
1502
+ isFocus, isFailed: s === 'fail', isRunning: s === 'run',
1503
+ onPick: () => this.set({ pFocus: name, v2Step: name }),
1504
+ pillStyle: 'display:flex; align-items:center; gap:8px; padding:9px 12px; border-radius:9px; cursor:pointer; white-space:nowrap; border:1px solid ' + (isFocus ? sm.color : '#262a31') + '; background:' + (isFocus ? sm.color + '1e' : '#101216') + ';' + (s === 'run' ? ' animation:pulseRed 2s infinite;' : ''),
1505
+ nodeStyle: 'width:42px; height:42px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:16px; cursor:pointer; color:' + sm.color + '; background:' + (isFocus ? sm.color + '22' : '#0d0f12') + '; border:2px solid ' + (isFocus ? sm.color : '#2a2e35') + ';' + (isFocus ? ' box-shadow:0 0 0 4px ' + sm.color + '22;' : '') + (s === 'run' ? ' animation:pulseRed 2s infinite;' : ''),
1506
+ labelColor: isFocus ? '#e6e8eb' : '#8b9098',
1507
+ };
1508
+ }),
1509
+ }));
1510
+ const fstep = this.STEPS[focusIdx];
1511
+ const fs = pat[focusIdx];
1512
+ const fm = this.statusMeta(fs);
1513
+ return {
1514
+ id, status, ago, dur, statusColor: m.color, statusLabel: m.label,
1515
+ summary: pat.filter(s => s === 'ok').length + ' passed · ' + pat.filter(s => s === 'fail').length + ' failed · ' + pat.filter(s => s === 'skip').length + ' skipped',
1516
+ segments: pat.map(s => ({ bg: this.statusMeta(s).bar })),
1517
+ stages,
1518
+ focus: { name: fstep[0], kind: fstep[1], kindColor: fstep[1] === 'default' ? K.purple : K.yellow, glyph: fm.glyph, color: fm.color, dur: (fs === 'skip' || fs === 'pend') ? '—' : this.DUR[focusIdx], status: fs, label: fm.label, isFailed: fs === 'fail', isRunning: fs === 'run', stepIndex: 'step ' + (focusIdx + 1) + '/11', autoNote: fs === 'fail' ? '自动定位到失败步骤' : fs === 'run' ? '自动定位到运行中步骤' : '自动定位到最后步骤' },
1519
+ focusLog: (this.LOGS[fstep[0]] || ['(no output)']).map(t => ({ text: t, color: /error|✖|FAILED/.test(t) ? K.red : t.startsWith('✓') ? K.green : t.startsWith('→') ? K.blue : '#9aa0a8' })),
1520
+ focusOut: this.outputFor(fstep[0], fs, this.DUR[focusIdx]),
1521
+ taskLinkable: row[4] === 'P_APPLY',
1522
+ };
1523
+ }
1524
+
1525
+ // ----- failure radar matrix (V2) -----
1526
+ matrixVM() {
1527
+ const K = this.K;
1528
+ const rows = this.RUNS.map(([id, status, ago, dur, patName]) => {
1529
+ const pat = this.pat(patName);
1530
+ const cells = pat.map((s, i) => {
1531
+ const sm = this.statusMeta(s);
1532
+ const sel = id === this.state.v2Run && this.STEPS[i][0] === this.state.v2Step;
1533
+ const filled = s === 'ok' || s === 'fail' || s === 'run';
1534
+ return {
1535
+ status: s, isFail: s === 'fail', isRun: s === 'run',
1536
+ onClick: () => this.set({ v2Run: id, v2Step: this.STEPS[i][0] }),
1537
+ cellStyle: 'width:21px; height:21px; border-radius:5px; cursor:pointer; background:' + (filled ? sm.bar + (s === 'ok' ? '' : '') : '#16181c') + '; opacity:' + (s === 'skip' || s === 'pend' ? '.5' : '1') + '; border:1.5px solid ' + (sel ? '#ffffff' : (s === 'fail' ? sm.bar : 'transparent')) + ';' + (s === 'fail' ? ' box-shadow:0 0 7px ' + sm.bar + '88;' : '') + (s === 'run' ? ' animation:pulseRed 2s infinite;' : ''),
1538
+ };
1539
+ });
1540
+ const selRow = id === this.state.v2Run;
1541
+ return {
1542
+ id, ago, dur, status, statusColor: this.statusMeta(status).color, isFail: status === 'fail', cells, selRow,
1543
+ rowStyle: 'display:flex; align-items:center; gap:10px; padding:5px 12px; cursor:pointer; border-radius:7px; background:' + (selRow ? '#15171b' : 'transparent') + ';',
1544
+ onSelectRow: () => { const fi = pat.findIndex(s => s === 'fail'); const idx = fi >= 0 ? fi : Math.max(pat.findIndex(s => s === 'run'), 0); this.set({ v2Run: id, v2Step: this.STEPS[idx][0] }); },
1545
+ };
1546
+ });
1547
+ const cols = this.STEPS.map(([name], i) => {
1548
+ const fails = this.RUNS.filter(r => this.pat(r[4])[i] === 'fail').length;
1549
+ return { name, fails, hot: fails >= 4, labelColor: fails >= 4 ? K.red : '#8b9098' };
1550
+ });
1551
+ const worst = cols.reduce((a, b) => b.fails > a.fails ? b : a, cols[0]);
1552
+ return { rows, cols, drawer: this.buildTrack(this.state.v2Run, this.state.v2Step), worst };
1553
+ }
1554
+
1555
+ // ===== Unified execution records (heterogeneous steps per run) =====
1556
+ PIPE_DEFS = [
1557
+ { id:'bugfix', name:'mantis-bug-fix-batch', trigger:'webhook', steps:['discover-ids','resolve','worktree-setup','fetch-bug-details','triage','attach-files','plan-fix','apply-fix','run-tests','open-mr','notify-teams'] },
1558
+ { id:'triage', name:'inbound-issue-triage', trigger:'webhook', steps:['fetch-issue','classify','dedup-check','assign-owner','post-comment'] },
1559
+ { id:'canary', name:'release-canary-deploy', trigger:'manual', steps:['build','push-image','deploy-canary','smoke-test','watch-metrics','promote'] },
1560
+ { id:'regress', name:'nightly-regression-suite', trigger:'schedule', steps:['checkout','install','unit','integration','e2e','report'] },
1561
+ { id:'depbump', name:'dependency-bump-automation', trigger:'schedule', steps:['scan-deps','pick-bump','patch','build','test','open-mr'] },
1562
+ { id:'docs', name:'docs-site-sync', trigger:'schedule', steps:['pull-content','render','link-check','publish'] },
1563
+ ];
1564
+ LLM_STEPS = { triage:1, 'plan-fix':1, classify:1, 'assign-owner':1, 'dedup-check':1 };
1565
+ PIDS = { bugfix:'pl_8a3f12', triage:'pl_2c91a0', canary:'pl_5f7e3b', regress:'pl_d40c88', depbump:'pl_19be57', docs:'pl_a7f001' };
1566
+ STEP_CMD = {
1567
+ 'discover-ids':['shell','for id in $(echo "$BUG_IDS" | tr "," "\\n"); do emit "$id"; done'],
1568
+ 'resolve':['shell','cd "$(git rev-parse --show-toplevel)" && command -v glab'],
1569
+ 'worktree-setup':['shell','git worktree add .wt/$BUG -b fix/$BUG'],
1570
+ 'fetch-bug-details':['shell','glab api mantis/$BUG > .ctx/bug.json'],
1571
+ 'triage':['llm','You are triaging a Mantis bug to decide if Forge should attempt an auto-fix…'],
1572
+ 'attach-files':['shell','download_attachments --bug $BUG --dir .wt/.attachments'],
1573
+ 'plan-fix':['llm','Given the bug context, produce a minimal patch plan for networkConfig.js…'],
1574
+ 'apply-fix':['shell','git apply --check /tmp/fix-$BUG.patch && git apply /tmp/fix-$BUG.patch'],
1575
+ 'run-tests':['shell','mvn -q test -Dtest=NetworkConfigTest'],
1576
+ 'open-mr':['shell','glab mr create --fill --source fix/$BUG --target main'],
1577
+ 'notify-teams':['shell','curl -X POST "$TEAMS_WEBHOOK" -d @.ctx/summary.json'],
1578
+ 'security-scan':['shell','trivy fs --severity HIGH,CRITICAL .'],
1579
+ 'fetch-issue':['shell','gh issue view $ISSUE --json title,body,labels'],
1580
+ 'classify':['llm','Classify this inbound issue into: bug · feature · question · spam…'],
1581
+ 'dedup-check':['llm','Search existing issues for duplicates of this report…'],
1582
+ 'assign-owner':['llm','Pick the most likely owner from CODEOWNERS for the affected paths…'],
1583
+ 'post-comment':['shell','gh issue comment $ISSUE --body-file .ctx/triage.md'],
1584
+ 'build':['shell','docker build -t svc:$SHA .'],
1585
+ 'push-image':['shell','docker push registry/svc:$SHA'],
1586
+ 'deploy-canary':['shell','kubectl set image deploy/svc svc=registry/svc:$SHA -n canary'],
1587
+ 'smoke-test':['shell','curl -sf https://canary.svc/healthz'],
1588
+ 'watch-metrics':['shell','promtool query "error_rate < 0.01" --for 5m'],
1589
+ 'promote':['shell','kubectl rollout restart deploy/svc -n prod'],
1590
+ 'checkout':['shell','git checkout $REF'],
1591
+ 'install':['shell','npm ci'],
1592
+ 'unit':['shell','npm run test:unit'],
1593
+ 'integration':['shell','npm run test:int'],
1594
+ 'e2e':['shell','npx playwright test'],
1595
+ 'report':['shell','node scripts/report.js > report.html'],
1596
+ 'scan-deps':['shell','npm outdated --json'],
1597
+ 'pick-bump':['llm','Choose safe dependency bumps given changelogs and semver…'],
1598
+ 'patch':['shell','npm install $DEP@$VER'],
1599
+ 'test':['shell','npm test'],
1600
+ 'pull-content':['shell','git pull && sync_content.sh'],
1601
+ 'render':['shell','hugo --minify'],
1602
+ 'link-check':['shell','lychee ./public'],
1603
+ 'publish':['shell','rsync -a public/ "$CDN"'],
1604
+ };
1605
+ TASKREC = [
1606
+ ['t_applyfix','failed','FortiNAC','bugfix','apply-fix','git apply --check /tmp/fix-1296959.patch','', '7m ago','6.8s',0.6,'25b1fc48'],
1607
+ ['c2179ec2','done','FortiNAC','bugfix','attach-files','download_attachments --bug 1296959 --dir .wt/.attach','','7m ago','3.1s',0.6,'25b1fc48'],
1608
+ ['t_triage','done','FortiNAC','bugfix','triage','You are triaging a Mantis bug to decide if Forge…','$0.203', '8m ago','22.1s',0.7,'25b1fc48'],
1609
+ ['t_plan','done','FortiNAC','bugfix','plan-fix','Plan the fix — patch networkConfig.js loadConfig…','$0.340','8m ago','18.4s',0.7,'25b1fc48'],
1610
+ ['t_smoke','failed','canary','canary','smoke-test','curl -sf https://canary-7d2a.svc.local/healthz','', '2m ago','41s',0.1,'7d2a01bc'],
1611
+ ['t_e2e','failed','regress','regress','e2e','npx playwright test --shard 3/4','', '1h ago','4m 02s',1,'f0a3d9e7'],
1612
+ ['t_scratch1','failed','scratch','','','set -e && cd "$(git rev-parse --show-toplevel)" && ./run.sh','', '7m ago','2.0s',0.6,''],
1613
+ ['t_classify','done','triage','triage','classify','Classify this inbound issue into bug/feature/question…','$0.041','18m ago','3.2s',0.3,'11c4e0a2'],
1614
+ ['t_depscan','done','depbump','depbump','scan-deps','npm outdated --json','', '2h ago','9.0s',2,'6b9920f1'],
1615
+ ['t_pickbump','done','depbump','depbump','pick-bump','Choose safe dependency bumps given changelogs…','$0.118','2h ago','6.7s',2,'6b9920f1'],
1616
+ ['t_test','failed','depbump','depbump','test','npm test','', '1d ago','58s',26,'9c0b4e88'],
1617
+ ['t_resolve','failed','FortiNAC','bugfix','resolve','command -v glab','', '3h ago','0.4s',3,'7026c661'],
1618
+ ['t_linkcheck','done','docs','docs','link-check','lychee ./public','', '6h ago','12s',6,'c5d22a90'],
1619
+ ['t_promote','done','canary','canary','promote','kubectl rollout restart deploy/svc -n prod','', '4h ago','21s',4,'3f8e1d40'],
1620
+ ['t_scratch2','failed','scratch','','','pytest -q','', '17m ago','5.0s',0.3,''],
1621
+ ['t_runtests','failed','FortiNAC','bugfix','run-tests','mvn -q test -Dtest=NetworkConfigTest','', '8h ago','7.5s',8,'e962b5b5'],
1622
+ ['t_watch','running','canary','canary','watch-metrics','promtool query "error_rate < 0.01"','', 'now','—',0,'a4f9c2e1'],
1623
+ ];
1624
+
1625
+ RECS_RAW = [
1626
+ { id:'a4f9c2e1', pipe:'bugfix', ago:'now', dur:'—', status:'run', focus:'run-tests', hrs:0 },
1627
+ { id:'7d2a01bc', pipe:'canary', ago:'2m ago', dur:'41s', status:'fail', focus:'smoke-test', hrs:0.1 },
1628
+ { id:'99634dc5', pipe:'bugfix', ago:'12m ago', dur:'1m 48s', status:'ok', hrs:0.2, steps:['discover-ids','resolve','worktree-setup','fetch-bug-details','triage','attach-files','plan-fix','apply-fix','security-scan','run-tests','open-mr','notify-teams'] },
1629
+ { id:'11c4e0a2', pipe:'triage', ago:'18m ago', dur:'12s', status:'ok', hrs:0.3 },
1630
+ { id:'25b1fc48', pipe:'bugfix', ago:'40m ago', dur:'58s', status:'fail', focus:'apply-fix', hrs:0.7 },
1631
+ { id:'f0a3d9e7', pipe:'regress', ago:'1h ago', dur:'8m 12s', status:'fail', focus:'e2e', hrs:1 },
1632
+ { id:'cdedb454', pipe:'bugfix', ago:'1h ago', dur:'1m 52s', status:'ok', hrs:1.4 },
1633
+ { id:'6b9920f1', pipe:'depbump', ago:'2h ago', dur:'2m 05s', status:'ok', hrs:2 },
1634
+ { id:'7026c661', pipe:'bugfix', ago:'3h ago', dur:'12s', status:'fail', focus:'resolve', hrs:3 },
1635
+ { id:'aa17b3c2', pipe:'triage', ago:'3h ago', dur:'9s', status:'ok', hrs:3.3 },
1636
+ { id:'3f8e1d40', pipe:'canary', ago:'4h ago', dur:'3m 30s', status:'ok', hrs:4 },
1637
+ { id:'850747c9', pipe:'bugfix', ago:'5h ago', dur:'1m 40s', status:'ok', hrs:5, steps:['discover-ids','resolve','worktree-setup','fetch-bug-details','triage','attach-files','plan-fix','apply-fix','run-tests','open-mr'] },
1638
+ { id:'c5d22a90', pipe:'docs', ago:'6h ago', dur:'38s', status:'ok', hrs:6 },
1639
+ { id:'e962b5b5', pipe:'bugfix', ago:'8h ago', dur:'1m 12s', status:'fail', focus:'run-tests', hrs:8 },
1640
+ { id:'2a7f0cc1', pipe:'regress', ago:'12h ago', dur:'7m 50s', status:'ok', hrs:12 },
1641
+ { id:'41ea10b7', pipe:'bugfix', ago:'1d ago', dur:'11s', status:'fail', focus:'worktree-setup',hrs:24 },
1642
+ { id:'9c0b4e88', pipe:'depbump', ago:'1d ago', dur:'1m 58s', status:'fail', focus:'test', hrs:26 },
1643
+ { id:'69ac564c', pipe:'bugfix', ago:'1d ago', dur:'1m 39s', status:'ok', hrs:28 },
1644
+ { id:'0399a270', pipe:'triage', ago:'2d ago', dur:'10s', status:'ok', hrs:48 },
1645
+ { id:'5c805acf', pipe:'bugfix', ago:'2d ago', dur:'13s', status:'fail', focus:'apply-fix', hrs:50 },
1646
+ { id:'c37431ae', pipe:'canary', ago:'2d ago', dur:'3m 10s', status:'ok', hrs:52 },
1647
+ { id:'d1f6a022', pipe:'docs', ago:'3d ago', dur:'35s', status:'ok', hrs:72 },
1648
+ ];
1649
+
1650
+ EXTRA_LOGS = {
1651
+ 'smoke-test': ['+ curl -sf https://canary-7d2a.svc.local/healthz','→ HTTP 502 Bad Gateway','retry 1/6 … 502','retry 6/6 … 502','health gate not satisfied after 6 retries','✖ smoke-test exited with code 1'],
1652
+ 'e2e': ['Running 142 e2e specs across 4 shards…','✓ 138 passed','✖ checkout.spec.ts:88 — timeout 30000ms exceeded','✖ cart.spec.ts:41 — expected 200, got 503','4 failing','✖ e2e exited with code 1'],
1653
+ 'test': ['+ pytest -q','............F......','FAILED tests/test_config.py::test_load_empty_guard','AssertionError: ConfigError not raised','✖ 1 failed, 46 passed in 18.3s'],
1654
+ 'resolve': ['+ cd "$(git rev-parse --show-toplevel)"','+ command -v glab','glab: command not found','✖ resolve exited with code 127'],
1655
+ 'worktree-setup': ['+ git worktree add .wt/1296959 -b fix/1296959','fatal: .wt/1296959 already exists and is not empty','✖ worktree-setup exited with code 128'],
1656
+ 'run-tests': ['+ mvn -q test -Dtest=NetworkConfigTest','[ERROR] NetworkConfigTest.loadConfig:142 expected ConfigError','[ERROR] Tests run: 18, Failures: 1','✖ run-tests exited with code 1'],
1657
+ };
1658
+
1659
+ recColor(s) { return this.statusMeta(s).bar; }
1660
+
1661
+ deriveStates(stepNames, status, focusName) {
1662
+ const fi = focusName ? stepNames.indexOf(focusName) : -1;
1663
+ return stepNames.map((n, i) => {
1664
+ if (status === 'ok' || fi < 0) return 'ok';
1665
+ if (i < fi) return 'ok';
1666
+ if (i === fi) return status === 'fail' ? 'fail' : 'run';
1667
+ return status === 'fail' ? 'skip' : 'pend';
1668
+ });
1669
+ }
1670
+
1671
+ enrichRun(r) {
1672
+ const byId = {}; this.PIPE_DEFS.forEach(p => byId[p.id] = p);
1673
+ const def = byId[r.pipe];
1674
+ const stepNames = r.steps || def.steps;
1675
+ const states = this.deriveStates(stepNames, r.status, r.focus);
1676
+ return { ...r, pid: this.PIDS[r.pipe], pipeName: def.name, trigger: r.trigger || def.trigger, stepNames, states };
1677
+ }
1678
+ buildRecords() {
1679
+ const base = this.RECS_RAW.map(r => this.enrichRun(r));
1680
+ const extra = this.state.extraRuns.map(r => this.enrichRun(r));
1681
+ const all = [...extra, ...base];
1682
+ return all.map(r => {
1683
+ if (this.state.cancelledRuns[r.id]) {
1684
+ return { ...r, status: 'fail', cancelled: true, states: this.deriveStates(r.stepNames, 'fail', r.focus) };
1685
+ }
1686
+ return r;
1687
+ });
1688
+ }
1689
+ retryRun(rec) {
1690
+ const n = this.state.retrySeq + 1;
1691
+ const nr = { id: 're' + n + '_' + rec.id.slice(0, 4), pipe: rec.pipe, ago: 'just now', dur: '—', status: 'run', focus: rec.stepNames[0], hrs: 0, steps: rec.steps };
1692
+ this.set({ extraRuns: [nr, ...this.state.extraRuns], retrySeq: n, expandedRun: nr.id, recStatus: 'all', recPipe: 'all', recTime: 'all', recGroup: 'timeline' });
1693
+ }
1694
+ cancelRun(id) { this.set({ cancelledRuns: { ...this.state.cancelledRuns, [id]: true } }); }
1695
+
1696
+ stepLogFor(name, status) {
1697
+ const K = this.K;
1698
+ let lines = this.EXTRA_LOGS[name] || this.LOGS[name];
1699
+ if (!lines) {
1700
+ lines = status === 'fail' ? ['✖ ' + name + ' exited with code 1']
1701
+ : status === 'run' ? ['◐ ' + name + ' running…', '… streaming output']
1702
+ : status === 'skip' ? ['— skipped (upstream failed)']
1703
+ : status === 'pend' ? ['· queued'] : ['+ ' + name, '✓ ' + name + ' completed'];
1704
+ }
1705
+ return lines.map(t => ({ text: t, color: /error|✖|FAILED|fatal|not found/i.test(t) ? K.red : t.startsWith('✓') ? K.green : t.startsWith('→') ? K.blue : '#9aa0a8' }));
1706
+ }
1707
+
1708
+ buildRunDetail(rec) {
1709
+ const K = this.K;
1710
+ const names = rec.stepNames, states = rec.states;
1711
+ let fi = names.indexOf(this.state.runFocus[rec.id]);
1712
+ if (fi < 0) fi = states.indexOf('fail');
1713
+ if (fi < 0) fi = states.indexOf('run');
1714
+ if (fi < 0) { for (let i = states.length - 1; i >= 0; i--) if (states[i] === 'ok') { fi = i; break; } }
1715
+ if (fi < 0) fi = 0;
1716
+ const steps = names.map((name, i) => {
1717
+ const s = states[i], sm = this.statusMeta(s);
1718
+ const kind = this.LLM_STEPS[name] ? 'default' : 'shell';
1719
+ const isFocus = i === fi;
1720
+ return {
1721
+ name, status: s, glyph: sm.glyph, color: sm.color, isFocus,
1722
+ kindColor: kind === 'default' ? K.purple : K.yellow,
1723
+ onPick: () => this.set({ runFocus: { ...this.state.runFocus, [rec.id]: name } }),
1724
+ pillStyle: 'display:flex; align-items:center; gap:7px; padding:7px 10px; border-radius:8px; cursor:pointer; white-space:nowrap; flex-shrink:0; border:1px solid ' + (isFocus ? sm.color : '#262a31') + '; background:' + (isFocus ? sm.color + '1e' : '#101216') + ';' + (s === 'run' ? ' animation:pulseRed 2s infinite;' : ''),
1725
+ };
1726
+ });
1727
+ const fName = names[fi], fState = states[fi], fm = this.statusMeta(fState);
1728
+ const fTaskId = this.taskIdForRunStep(rec.id, fName);
1729
+ return {
1730
+ steps, count: names.length,
1731
+ onOpenTaskLog: () => this.openTaskDetail(fTaskId),
1732
+ focus: { name: fName, glyph: fm.glyph, color: fm.color, kind: this.LLM_STEPS[fName] ? 'llm' : 'shell', kindColor: this.LLM_STEPS[fName] ? K.purple : K.yellow },
1733
+ focusLog: this.stepLogFor(fName, fState),
1734
+ focusOut: this.outputFor(fName, fState, '—'),
1735
+ taskLinkable: rec.id === '25b1fc48',
1736
+ };
1737
+ }
1738
+
1739
+ recRowVM(r, showPipe) {
1740
+ const K = this.K;
1741
+ const m = this.statusMeta(r.status);
1742
+ const okN = r.states.filter(s => s === 'ok').length;
1743
+ const summary = r.status === 'fail' ? ((r.cancelled ? 'cancelled · ' : 'failed · ') + r.focus) : r.status === 'run' ? ('running · ' + r.focus) : (r.stepNames.length + ' steps passed');
1744
+ const expanded = this.state.expandedRun === r.id;
1745
+ const trigGlyph = r.trigger === 'schedule' ? '◷' : r.trigger === 'manual' ? '☞' : '⚡';
1746
+ const isFail = r.status === 'fail', isRun = r.status === 'run';
1747
+ const actions = isRun
1748
+ ? [ { label: '■ Cancel', danger: true, run: (e) => { e && e.stopPropagation(); this.cancelRun(r.id); } },
1749
+ { label: '⊙ Follow live', danger: false, run: (e) => { e && e.stopPropagation(); this.set({ expandedRun: r.id }); } } ]
1750
+ : isFail
1751
+ ? [ { label: '↻ Retry from ' + r.focus, danger: false, run: (e) => { e && e.stopPropagation(); this.retryRun(r); } },
1752
+ { label: '↻ Re-run all', danger: false, run: (e) => { e && e.stopPropagation(); this.retryRun(r); } } ]
1753
+ : [ { label: '↻ Re-run', danger: false, run: (e) => { e && e.stopPropagation(); this.retryRun(r); } } ];
1754
+ const quick = isRun ? { show: true, label: '■ Cancel', color: K.red, run: (e) => { e && e.stopPropagation(); this.cancelRun(r.id); } }
1755
+ : isFail ? { show: true, label: '↻ Retry', color: K.orange, run: (e) => { e && e.stopPropagation(); this.retryRun(r); } }
1756
+ : { show: false, label: '', color: '', run: () => {} };
1757
+ return {
1758
+ id: r.id, pid: r.pid, pipeName: r.pipeName, showPipe, ago: r.ago, dur: r.dur, status: r.status,
1759
+ statusLabel: r.cancelled ? 'cancelled' : m.label,
1760
+ dotColor: m.color, summary, summaryColor: r.status === 'fail' ? K.red : r.status === 'run' ? K.run : '#7d828b',
1761
+ trigger: r.trigger, trigGlyph, stepCount: r.stepNames.length + ' steps',
1762
+ segments: r.states.map(s => ({ bg: this.statusMeta(s).bar })),
1763
+ expanded, chev: expanded ? '▾' : '▸',
1764
+ actions, quick,
1765
+ quickStyle: 'font-size:10.5px; font-weight:700; padding:4px 9px; border-radius:6px; cursor:pointer; white-space:nowrap; border:1px solid ' + quick.color + '55; background:' + quick.color + '18; color:' + quick.color + ';',
1766
+ rowStyle: 'display:flex; align-items:center; gap:12px; padding:11px 16px; cursor:pointer; border-bottom:1px solid #15171a; background:' + (expanded ? '#121317' : 'transparent') + '; border-left:2px solid ' + (expanded ? m.color : 'transparent') + ';',
1767
+ onToggle: () => this.set({ expandedRun: expanded ? '' : r.id }),
1768
+ detail: expanded ? this.buildRunDetail(r) : null,
1769
+ };
1770
+ }
1771
+
1772
+ recordsVM() {
1773
+ const K = this.K, st = this.state;
1774
+ const all = this.buildRecords();
1775
+ const statusMap = { failed: 'fail', running: 'run', passed: 'ok' };
1776
+ const lim = st.recTime === '24h' ? 24 : st.recTime === '7d' ? 168 : st.recTime === '30d' ? 720 : 1e9;
1777
+ const q = st.recSearch.trim().toLowerCase();
1778
+ const list = all.filter(r => {
1779
+ if (st.recStatus !== 'all' && r.status !== statusMap[st.recStatus]) return false;
1780
+ if (st.recPipe !== 'all' && r.pipe !== st.recPipe) return false;
1781
+ if (st.recTrigger !== 'all' && r.trigger !== st.recTrigger) return false;
1782
+ if (r.hrs > lim) return false;
1783
+ if (q && !(r.id.includes(q) || r.pipeName.toLowerCase().includes(q) || (r.focus || '').toLowerCase().includes(q))) return false;
1784
+ return true;
1785
+ });
1786
+ const total = list.length, failN = list.filter(r => r.status === 'fail').length, runN = list.filter(r => r.status === 'run').length;
1787
+
1788
+ let groups = null, flat = null;
1789
+ if (st.recGroup === 'pipeline') {
1790
+ groups = this.PIPE_DEFS.map(p => {
1791
+ const rs = list.filter(r => r.pipe === p.id);
1792
+ if (!rs.length) return null;
1793
+ const gf = rs.filter(r => r.status === 'fail').length;
1794
+ const lastM = this.statusMeta(rs[0].status);
1795
+ const open = !!st.expandedGroups[p.id];
1796
+ const spark = all.filter(r => r.pipe === p.id).slice(0, 12).reverse().map(r => ({ bg: this.statusMeta(r.status).bar }));
1797
+ return {
1798
+ id: p.id, name: p.name, count: rs.length, fails: gf, hasFail: gf > 0,
1799
+ lastDot: lastM.color, spark, open, chev: open ? '▾' : '▸',
1800
+ headerStyle: 'display:flex; align-items:center; gap:12px; padding:12px 16px; cursor:pointer; background:#0d0e11; border-bottom:1px solid #1d1f24;' + (open ? '' : ''),
1801
+ onToggle: () => this.set({ expandedGroups: { ...st.expandedGroups, [p.id]: !open } }),
1802
+ runs: open ? rs.map(r => this.recRowVM(r, false)) : [],
1803
+ };
1804
+ }).filter(Boolean);
1805
+ } else {
1806
+ flat = list.map(r => this.recRowVM(r, true));
1807
+ }
1808
+
1809
+ const seg = (opts, cur, key, cmap) => opts.map(([val, label]) => {
1810
+ const active = val === cur, c = cmap && cmap[val];
1811
+ return { label, active, onSelect: () => this.set({ [key]: val }),
1812
+ style: 'padding:6px 12px; border-radius:7px; cursor:pointer; font-size:11px; font-weight:500; white-space:nowrap; border:1px solid ' + (active ? (c || '#3a3f48') : 'transparent') + '; background:' + (active ? (c ? c + '1c' : '#1a1d22') : 'transparent') + '; color:' + (active ? (c || '#e6e8eb') : '#8b9098') + ';' };
1813
+ });
1814
+
1815
+ return {
1816
+ groups, flat, total, failN, runN,
1817
+ isGroup: st.recGroup === 'pipeline',
1818
+ statusOpts: seg([['all','All'],['failed','Failed'],['running','Running'],['passed','Passed']], st.recStatus, 'recStatus', { failed:K.red, running:K.run, passed:K.green }),
1819
+ timeOpts: seg([['24h','24h'],['7d','7d'],['30d','30d'],['all','All time']], st.recTime, 'recTime'),
1820
+ trigOpts: seg([['all','All'],['schedule','◷ Scheduled'],['manual','☞ Manual'],['webhook','⚡ Webhook']], st.recTrigger, 'recTrigger'),
1821
+ pipeOpts: seg([['all','All pipelines'], ...this.PIPE_DEFS.map(p => [p.id, p.name])], st.recPipe, 'recPipe', { [st.recPipe]: K.orange }),
1822
+ groupOpts: seg([['timeline','平铺 Flat'],['pipeline','按 pipeline 分组']], st.recGroup, 'recGroup'),
1823
+ search: st.recSearch,
1824
+ onSearch: (e) => this.set({ recSearch: e.target.value }),
1825
+ clearRecSearch: () => this.set({ recSearch: '' }),
1826
+ };
1827
+ }
1828
+
1829
+ // ===== Pipeline management (view + edit definitions) =====
1830
+ stepDef(name) {
1831
+ const K = this.K;
1832
+ const d = this.STEP_CMD[name] || ['shell', '$ run ' + name];
1833
+ return { type: d[0], cmd: d[1], kindColor: d[0] === 'llm' ? K.purple : K.yellow, kindLabel: d[0] === 'llm' ? 'llm' : 'shell' };
1834
+ }
1835
+ pipeMgmtVM() {
1836
+ const K = this.K, recs = this.buildRecords();
1837
+ const list = this.PIPE_DEFS.map(p => {
1838
+ const rs = recs.filter(r => r.pipe === p.id);
1839
+ const sel = p.id === this.state.selType;
1840
+ const last = rs[0] ? this.statusMeta(rs[0].status).color : '#4a4e56';
1841
+ return {
1842
+ id: p.id, name: p.name, pid: this.PIDS[p.id], stepCount: p.steps.length, runs: rs.length, lastDot: last, sel,
1843
+ onSelect: () => this.set({ selType: p.id, mgmtEdit: false }),
1844
+ rowStyle: 'display:block; width:100%; text-align:left; padding:11px 13px; border:none; border-left:2px solid ' + (sel ? K.orange : 'transparent') + '; background:' + (sel ? '#15171b' : 'transparent') + '; cursor:pointer; border-bottom:1px solid #131519;',
1845
+ nameColor: sel ? '#f3f4f6' : '#c2c6cd',
1846
+ };
1847
+ });
1848
+ const p = this.PIPE_DEFS.find(x => x.id === this.state.selType) || this.PIPE_DEFS[0];
1849
+ const recsP = recs.filter(r => r.pipe === p.id);
1850
+ const edit = this.state.mgmtEdit;
1851
+ const steps = p.steps.map((name, i) => {
1852
+ const d = this.stepDef(name);
1853
+ return { idx: i + 1, name, type: d.type, cmd: d.cmd, kindColor: d.kindColor, kindLabel: d.kindLabel, edit };
1854
+ });
1855
+ return {
1856
+ list, edit,
1857
+ sel: { id: p.id, name: p.name, pid: this.PIDS[p.id], trigger: p.trigger, stepCount: p.steps.length, runs: recsP.length, lastDot: recsP[0] ? this.statusMeta(recsP[0].status).color : '#4a4e56', lastStatus: recsP[0] ? this.statusMeta(recsP[0].status).label : '—' },
1858
+ steps,
1859
+ toggleEdit: () => this.set({ mgmtEdit: !edit }),
1860
+ editLabel: edit ? 'Done' : 'Edit',
1861
+ editStyle: 'font-size:11.5px; padding:6px 14px; border-radius:7px; cursor:pointer; font-weight:600; ' + (edit ? 'color:#0b0c0e; background:#46c25a;' : 'color:#cdd0d6; background:#1a1d22; border:1px solid #2a2e35;'),
1862
+ viewRecords: () => this.set({ active: 'p1', recPipe: p.id, recGroup: 'timeline' }),
1863
+ };
1864
+ }
1865
+
1866
+ // ===== Task records (flat, search, quick filters) =====
1867
+ taskRecVM() {
1868
+ const K = this.K, st = this.state;
1869
+ const nameById = {}; this.PIPE_DEFS.forEach(p => nameById[p.id] = p.name);
1870
+ const created = this.state.createdTasks.map(c => ({ ...c, pipeName: c.pipe ? nameById[c.pipe] : 'ad-hoc task' }));
1871
+ const baseRecs = this.TASKREC.map(t => {
1872
+ const [id, status, provider, pipe, step, summary, cost, ago, dur, hrs, runId] = t;
1873
+ return { id, status, provider, pipe, step, summary, cost, ago, dur, hrs, runId, pipeName: pipe ? nameById[pipe] : 'scratch (ad-hoc)' };
1874
+ });
1875
+ const recs = [...created, ...baseRecs].map(r => this.state.cancelledTasks[r.id] && r.status === 'running' ? { ...r, status: 'failed', cancelled: true } : r);
1876
+ const statusMap = { done: 'done', failed: 'failed', running: 'running' };
1877
+ const lim = st.trTime === '24h' ? 24 : st.trTime === '7d' ? 168 : st.trTime === '30d' ? 720 : 1e9;
1878
+ const q = st.trSearch.trim().toLowerCase();
1879
+ const list = recs.filter(r => {
1880
+ if (st.trStatus !== 'all' && r.status !== statusMap[st.trStatus]) return false;
1881
+ if (st.trProvider !== 'all' && r.provider !== st.trProvider) return false;
1882
+ if (st.trPipe !== 'all' && r.pipe !== st.trPipe) return false;
1883
+ if (st.trCost && !r.cost) return false;
1884
+ if (r.hrs > lim) return false;
1885
+ if (q && !(r.id.toLowerCase().includes(q) || r.summary.toLowerCase().includes(q) || (r.step || '').toLowerCase().includes(q) || r.pipeName.toLowerCase().includes(q) || r.provider.toLowerCase().includes(q))) return false;
1886
+ return true;
1887
+ });
1888
+ const rowVM = (r) => {
1889
+ const c = this.taskStatusColor(r.status);
1890
+ const expanded = this.state.expandedTask === r.id;
1891
+ return {
1892
+ id: r.id, status: r.status, statusColor: c, provider: r.provider, providerColor: r.provider === 'scratch' ? K.dim : '#c2c6cd',
1893
+ pipeName: r.pipeName, step: r.step || '—', summary: r.summary, cost: r.cost || '—', dur: r.dur, ago: r.ago,
1894
+ expanded, openGlyph: expanded ? '▾' : '▸',
1895
+ onOpenFull: (e) => { if (e) e.stopPropagation(); this.openTaskDetail(r.id); },
1896
+ onToggle: () => this.set({ expandedTask: expanded ? '' : r.id, taskSearch: '', errorsOnly: false, selTaskId: r.id }),
1897
+ rowStyle: 'display:flex; align-items:center; gap:12px; padding:10px 16px; cursor:pointer; border-bottom:1px solid #15171a; background:' + (expanded ? '#121317' : 'transparent') + '; border-left:2px solid ' + (expanded ? c : 'transparent') + ';',
1898
+ detail: expanded ? this.taskDetailVM(r.id) : null,
1899
+ };
1900
+ };
1901
+ const providers = ['all', ...Array.from(new Set(recs.map(r => r.provider)))];
1902
+ const seg = (opts, cur, key, cmap) => opts.map(([val, label]) => {
1903
+ const active = val === cur, cc = cmap && cmap[val];
1904
+ return { label, active, onSelect: () => this.set({ [key]: val }),
1905
+ style: 'padding:6px 12px; border-radius:7px; cursor:pointer; font-size:11px; font-weight:500; white-space:nowrap; border:1px solid ' + (active ? (cc || '#3a3f48') : 'transparent') + '; background:' + (active ? (cc ? cc + '1c' : '#1a1d22') : 'transparent') + '; color:' + (active ? (cc || '#e6e8eb') : '#8b9098') + ';' };
1906
+ });
1907
+ const qchips = [
1908
+ ['全部', st.trStatus === 'all' && st.trProvider === 'all' && !st.trCost && st.trTime === '7d' && st.trPipe === 'all', () => this.set({ trStatus:'all', trProvider:'all', trPipe:'all', trTime:'7d', trCost:false, trSearch:'' }), '#c2c6cd'],
1909
+ ['失败', st.trStatus === 'failed', () => this.set({ trStatus:'failed' }), K.red],
1910
+ ['运行中', st.trStatus === 'running', () => this.set({ trStatus:'running' }), K.run],
1911
+ ['scratch', st.trProvider === 'scratch', () => this.set({ trProvider: st.trProvider === 'scratch' ? 'all' : 'scratch' }), K.yellow],
1912
+ ['有费用', st.trCost, () => this.set({ trCost: !st.trCost }), K.purple],
1913
+ ['今天', st.trTime === '24h', () => this.set({ trTime: st.trTime === '24h' ? '7d' : '24h' }), K.blue],
1914
+ ].map(([label, active, onSelect, c]) => ({ label, active, onSelect,
1915
+ style: 'padding:6px 13px; border-radius:16px; cursor:pointer; font-size:11.5px; font-weight:600; white-space:nowrap; border:1px solid ' + (active ? c : '#2a2e35') + '; background:' + (active ? c + '1e' : 'transparent') + '; color:' + (active ? c : '#9aa0a8') + ';' }));
1916
+ return {
1917
+ rows: list.map(rowVM), total: list.length, failN: list.filter(r => r.status === 'failed').length,
1918
+ statusOpts: seg([['all','All'],['done','Done'],['failed','Failed'],['running','Running']], st.trStatus, 'trStatus', { failed:K.red, running:K.run, done:K.blue }),
1919
+ providerOpts: seg(providers.map(p => [p, p === 'all' ? 'All' : p]), st.trProvider, 'trProvider'),
1920
+ pipeOpts: seg([['all','All'], ...this.PIPE_DEFS.map(p => [p.id, p.name])], st.trPipe, 'trPipe', { [st.trPipe]: K.orange }),
1921
+ timeOpts: seg([['24h','24h'],['7d','7d'],['30d','30d'],['all','All']], st.trTime, 'trTime'),
1922
+ quick: qchips,
1923
+ search: st.trSearch, onSearch: (e) => this.set({ trSearch: e.target.value }), clearSearch: () => this.set({ trSearch: '' }),
1924
+ createOpen: st.taskCreateOpen,
1925
+ openCreate: () => this.set({ taskCreateOpen: true }),
1926
+ closeCreate: () => this.set({ taskCreateOpen: false }),
1927
+ stop: (e) => { e.stopPropagation(); },
1928
+ form: st.createForm,
1929
+ canRun: !!st.createForm.cmd.trim(),
1930
+ runStyle: 'font-size:12px; font-weight:600; padding:9px 18px; border-radius:8px; cursor:' + (st.createForm.cmd.trim() ? 'pointer' : 'not-allowed') + '; color:' + (st.createForm.cmd.trim() ? '#0b0c0e' : '#6b7079') + '; background:' + (st.createForm.cmd.trim() ? '#ff7d2e' : '#1a1d22') + ';',
1931
+ setProvider: (e) => this.set({ createForm: { ...st.createForm, provider: e.target.value } }),
1932
+ onCmd: (e) => this.set({ createForm: { ...st.createForm, cmd: e.target.value } }),
1933
+ modeShell: () => this.set({ createForm: { ...st.createForm, mode: 'shell' } }),
1934
+ modeLlm: () => this.set({ createForm: { ...st.createForm, mode: 'llm' } }),
1935
+ isShell: st.createForm.mode === 'shell', isLlm: st.createForm.mode === 'llm',
1936
+ modeShellStyle: 'flex:1; text-align:center; padding:8px; border-radius:7px; cursor:pointer; font-size:12px; font-weight:600; color:' + (st.createForm.mode === 'shell' ? '#0b0c0e' : '#9aa0a8') + '; background:' + (st.createForm.mode === 'shell' ? '#d8a23f' : 'transparent') + ';',
1937
+ modeLlmStyle: 'flex:1; text-align:center; padding:8px; border-radius:7px; cursor:pointer; font-size:12px; font-weight:600; color:' + (st.createForm.mode === 'llm' ? '#0b0c0e' : '#9aa0a8') + '; background:' + (st.createForm.mode === 'llm' ? '#a978f0' : 'transparent') + ';',
1938
+ providerVal: st.createForm.provider,
1939
+ cmdPlaceholder: st.createForm.mode === 'shell' ? '$ 输入要执行的命令,如 pytest -q' : '输入给模型的提示,如 Summarize the failing test and propose a fix…',
1940
+ runCreate: () => {
1941
+ const cf = st.createForm; if (!cf.cmd.trim()) return;
1942
+ const n = st.createSeq + 1;
1943
+ const t = { id: 't_new' + n, status: 'running', provider: cf.provider, pipe: '', step: cf.mode === 'llm' ? 'prompt' : 'command', summary: cf.cmd.trim(), cost: '', ago: 'just now', dur: '—', hrs: 0, runId: '' };
1944
+ this.set({ createdTasks: [t, ...st.createdTasks], createSeq: n, taskCreateOpen: false, createForm: { provider: cf.provider, mode: cf.mode, cmd: '' }, trStatus: 'all', trProvider: 'all', trSearch: '' });
1945
+ },
1946
+ };
1947
+ }
1948
+
1949
+ topNavVM() {
1950
+ const a = this.state.active;
1951
+ const tab = (label, on, active, inert) => ({ label, onClick: inert ? (() => {}) : on, active,
1952
+ style: 'padding:13px 4px; font-size:13px; cursor:' + (inert ? 'default' : 'pointer') + '; color:' + (active ? '#f3f4f6' : '#6b7079') + ';' + (active ? ' border-bottom:2px solid #4f9dff; font-weight:600;' : ' border-bottom:2px solid transparent;') });
1953
+ return [
1954
+ tab('Schedules', () => {}, false, true),
1955
+ tab('Pipeline', () => this.set({ active: 'pipe' }), a === 'pipe'),
1956
+ tab('Pipeline Record', () => this.set({ active: 'p1' }), a === 'p1'),
1957
+ tab('Task', () => this.set({ active: 'trecord' }), a === 'trecord' || a === 't3' || a === 'tdetail'),
1958
+ ];
1959
+ }
1960
+
1961
+ renderVals() {
1962
+ const a = this.state.active;
1963
+ const detail = this.detailVM();
1964
+ const p2sel = detail.steps.find(s => s.name === this.state.p2Open) || detail.failedStep || detail.steps[0];
1965
+ return {
1966
+ p2cols: this.p2Cols(detail),
1967
+ p2sel,
1968
+ p3: this.p3(),
1969
+ detail0: detail,
1970
+ navTop: [this.navItem('intro', '总览 Overview', '◫', '', '')],
1971
+ navPipe: [
1972
+ this.navItem('pipe', 'Pipeline · 管理', '▤', '', '查看 / 编辑 pipeline 定义与步骤'),
1973
+ this.navItem('p1', 'Pipeline Record · 执行记录', '▦', '', '所有运行记录平铺 · 搜索筛选 · 展开看步骤'),
1974
+ ],
1975
+ navTask: [
1976
+ this.navItem('trecord', 'Task · 任务', '▤', '', '创建任务 + 查看最近/运行中(任务一次性,跑完即止)'),
1977
+ ],
1978
+ showIntro: a === 'intro',
1979
+ showP1: a === 'p1', showP2: a === 'p2', showP3: a === 'p3',
1980
+ showT1: a === 't1', showT2: a === 't2', showT3: a === 't3',
1981
+ showTDetail: a === 'tdetail',
1982
+ fullTask: this.fullTaskVM(),
1983
+ workflow: 'fortinet-mantis-bug-fix-batch',
1984
+ runs: this.runsVM(),
1985
+ records: this.recordsVM(),
1986
+ topNav: this.topNavVM(),
1987
+ mgmt: this.pipeMgmtVM(),
1988
+ taskRec: this.taskRecVM(),
1989
+ showMgmt: a === 'pipe',
1990
+ showTRecord: a === 'trecord',
1991
+ pipeTypes: this.typesVM(),
1992
+ runsForType: this.runsForTypeVM(),
1993
+ track: this.buildTrack(this.state.selRunId, this.state.pFocus),
1994
+ matrix: this.matrixVM(),
1995
+ failedRunsOnly: this.state.failedRunsOnly,
1996
+ toggleFailedRuns: () => this.set({ failedRunsOnly: !this.state.failedRunsOnly }),
1997
+ failToggleStyle: 'display:flex; align-items:center; gap:6px; padding:4px 9px; border-radius:6px; cursor:pointer; font-size:10.5px; border:1px solid ' + (this.state.failedRunsOnly ? '#f15b4a55' : '#22252b') + '; background:' + (this.state.failedRunsOnly ? '#f15b4a18' : 'transparent') + '; color:' + (this.state.failedRunsOnly ? '#f15b4a' : '#8b9098') + ';',
1998
+ detail,
1999
+ tasks: this.tasksVM(),
2000
+ selTask: this.selTaskVM(),
2001
+ taskSearch: this.state.taskSearch,
2002
+ errorsOnly: this.state.errorsOnly,
2003
+ onSearch: (e) => this.set({ taskSearch: e.target.value }),
2004
+ clearSearch: () => this.set({ taskSearch: '' }),
2005
+ toggleErrors: () => this.set({ errorsOnly: !this.state.errorsOnly }),
2006
+ gotoTaskFromStep: () => this.openTaskDetail('t_applyfix'),
2007
+ gotoPipeline: () => this.set({ active: 'p1', expandedRun: '25b1fc48', recStatus: 'all', recPipe: 'all', expandedGroups: { ...this.state.expandedGroups, bugfix: true } }),
2008
+ showResult: this.state.t1Result, showDiff: this.state.t1Diff,
2009
+ toggleResult: () => this.set({ t1Result: !this.state.t1Result }),
2010
+ toggleDiff: () => this.set({ t1Diff: !this.state.t1Diff }),
2011
+ resultChev: this.state.t1Result ? '▾' : '▸', diffChev: this.state.t1Diff ? '▾' : '▸',
2012
+ errToggleStyle: 'display:flex; align-items:center; gap:7px; padding:6px 11px; border-radius:7px; cursor:pointer; white-space:nowrap; border:1px solid ' + (this.state.errorsOnly ? '#f15b4a55' : '#24262b') + '; background:' + (this.state.errorsOnly ? '#f15b4a18' : 'transparent') + '; color:' + (this.state.errorsOnly ? '#f15b4a' : '#9aa0a8') + ';',
2013
+ p2Open: this.state.p2Open,
2014
+ };
2015
+ }
2016
+ }
2017
+ </script>
2018
+ </body>
2019
+ </html>