patchwarden 0.6.1 → 0.6.4

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 (65) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +106 -18
  6. package/README.md +30 -19
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/controlCenter.d.ts +14 -0
  10. package/dist/controlCenter.js +2002 -0
  11. package/dist/doctor.js +3 -3
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/docs/control-center/README.md +33 -0
  15. package/docs/control-center/control-center-daily-driver.md +211 -0
  16. package/docs/control-center/control-center-mvp.md +205 -0
  17. package/docs/control-center/control-center-phase2.md +159 -0
  18. package/docs/demo.md +3 -0
  19. package/docs/release-v0.6.4.md +45 -0
  20. package/examples/openai-tunnel/README.md +5 -5
  21. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  22. package/package.json +23 -15
  23. package/scripts/README.md +47 -0
  24. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  25. package/scripts/checks/control-center-smoke.js +1098 -0
  26. package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
  27. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
  28. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  29. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +12 -12
  30. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +2 -2
  31. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +4 -2
  32. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  33. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  34. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  35. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
  36. package/scripts/control/control-center-tray.ps1 +281 -0
  37. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  38. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  39. package/scripts/control/restart-control-center.ps1 +173 -0
  40. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  41. package/scripts/control/start-control-center.ps1 +263 -0
  42. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  43. package/scripts/control/stop-patchwarden.ps1 +114 -0
  44. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  45. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  46. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  47. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  48. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  49. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  50. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  51. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  52. package/src/controlCenter.ts +2152 -0
  53. package/src/doctor.ts +4 -4
  54. package/src/version.ts +1 -1
  55. package/ui/colors_and_type.css +141 -0
  56. package/ui/pages/audit.html +743 -0
  57. package/ui/pages/dashboard.html +1154 -0
  58. package/ui/pages/direct-sessions.html +652 -0
  59. package/ui/pages/logs.html +502 -0
  60. package/ui/pages/task-detail.html +1229 -0
  61. package/ui/pages/tasks.html +702 -0
  62. package/ui/pages/workspace.html +947 -0
  63. package/ui/partials/project-shell.html +362 -0
  64. package/ui/vendor/lucide.js +12 -0
  65. package/ui/vendor/tailwindcss-browser.js +947 -0
@@ -0,0 +1,702 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title data-zh="任务面板" data-en="Tasks">任务面板</title>
7
+ <style id="theme-vars">
8
+ /* PatchWarden Brand CSS — Dark Security Theme */
9
+ /* Prefix: pw */
10
+
11
+ :root {
12
+ /* ── Primary Palette ── */
13
+ --pw-bg: #0a0e14;
14
+ --pw-bg-elevated: #111820;
15
+ --pw-bg-surface: #161d27;
16
+ --pw-bg-hover: #1c2533;
17
+ --pw-bg-active: #222d3d;
18
+ --pw-border: #1e2a3a;
19
+ --pw-border-subtle: #172030;
20
+ --pw-border-focus: #2dd4a8;
21
+
22
+ /* ── Text ── */
23
+ --pw-text-primary: #e8edf4;
24
+ --pw-text-secondary: #8b99ad;
25
+ --pw-text-tertiary: #5a6a80;
26
+ --pw-text-inverse: #0a0e14;
27
+ --pw-text-accent: #2dd4a8;
28
+
29
+ /* ── Brand Accent — Security Green ── */
30
+ --pw-accent: #2dd4a8;
31
+ --pw-accent-hover: #34eabc;
32
+ --pw-accent-muted: rgba(45, 212, 168, 0.12);
33
+ --pw-accent-subtle: rgba(45, 212, 168, 0.06);
34
+
35
+ /* ── State Colors ── */
36
+ --pw-state-success: #22c55e;
37
+ --pw-state-success-bg: rgba(34, 197, 94, 0.1);
38
+ --pw-state-warning: #f59e0b;
39
+ --pw-state-warning-bg: rgba(245, 158, 11, 0.1);
40
+ --pw-state-error: #ef4444;
41
+ --pw-state-error-bg: rgba(239, 68, 68, 0.1);
42
+ --pw-state-info: #3b82f6;
43
+ --pw-state-info-bg: rgba(59, 130, 246, 0.1);
44
+
45
+ /* ── Radius ── */
46
+ --pw-radius-sm: 4px;
47
+ --pw-radius-md: 8px;
48
+ --pw-radius-lg: 12px;
49
+ --pw-radius-full: 9999px;
50
+
51
+ /* ── Shadows — static surfaces use border only; floating layers use subtle shadow ── */
52
+ --pw-shadow-float: 0 8px 32px rgba(0, 0, 0, 0.4);
53
+ --pw-shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.5);
54
+
55
+ /* ── Typography ── */
56
+ --pw-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
57
+ --pw-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;
58
+
59
+ --pw-text-xs: 0.75rem;
60
+ --pw-text-sm: 0.8125rem;
61
+ --pw-text-base: 0.875rem;
62
+ --pw-text-lg: 1rem;
63
+ --pw-text-xl: 1.25rem;
64
+ --pw-text-2xl: 1.5rem;
65
+ --pw-text-3xl: 2rem;
66
+
67
+ --pw-leading-tight: 1.25;
68
+ --pw-leading-normal: 1.5;
69
+ --pw-leading-relaxed: 1.625;
70
+
71
+ --pw-weight-normal: 400;
72
+ --pw-weight-medium: 500;
73
+ --pw-weight-semibold: 600;
74
+ --pw-weight-bold: 700;
75
+
76
+ /* ── Spacing ── */
77
+ --pw-space-1: 4px;
78
+ --pw-space-2: 8px;
79
+ --pw-space-3: 12px;
80
+ --pw-space-4: 16px;
81
+ --pw-space-5: 20px;
82
+ --pw-space-6: 24px;
83
+ --pw-space-8: 32px;
84
+ --pw-space-10: 40px;
85
+ --pw-space-12: 48px;
86
+
87
+ /* ── Transitions ── */
88
+ --pw-transition-fast: 150ms ease;
89
+ --pw-transition-base: 200ms ease;
90
+ --pw-transition-slow: 300ms ease;
91
+ }
92
+
93
+ html.dark {
94
+ --pw-bg: #0a0e14;
95
+ --pw-bg-elevated: #111820;
96
+ --pw-bg-surface: #161d27;
97
+ --pw-bg-hover: #1c2533;
98
+ --pw-bg-active: #222d3d;
99
+ --pw-border: #1e2a3a;
100
+ --pw-border-subtle: #172030;
101
+ --pw-text-primary: #e8edf4;
102
+ --pw-text-secondary: #8b99ad;
103
+ --pw-text-tertiary: #5a6a80;
104
+ }
105
+
106
+ </style>
107
+ <script src="/vendor/tailwindcss-browser.js"></script>
108
+ <script src="/vendor/lucide.js"></script>
109
+ <style type="text/tailwindcss">
110
+ @theme inline {
111
+ --color-accent: var(--pw-accent);
112
+ --color-border: var(--pw-border);
113
+ }
114
+ @layer base {
115
+ body { background: var(--pw-background); color: var(--pw-text-primary); }
116
+ td, th { @apply break-words; word-break: break-all; word-break: auto-phrase; }
117
+ th { @apply whitespace-nowrap; }
118
+ }
119
+ </style>
120
+ <style>
121
+ .no-scrollbar::-webkit-scrollbar { display: none; }
122
+ .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
123
+ [data-icon] {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ -webkit-mask-size: contain;
128
+ mask-size: contain;
129
+ -webkit-mask-repeat: no-repeat;
130
+ mask-repeat: no-repeat;
131
+ -webkit-mask-position: center;
132
+ mask-position: center;
133
+ background-color: currentColor;
134
+ }
135
+ .pw-spin {
136
+ animation: pw-spin 0.9s linear infinite;
137
+ }
138
+ @keyframes pw-spin {
139
+ from { transform: rotate(0deg); }
140
+ to { transform: rotate(360deg); }
141
+ }
142
+ </style>
143
+ </head>
144
+ <body class="flex h-screen overflow-hidden">
145
+
146
+ <!-- ========== SIDEBAR ========== -->
147
+ <aside class="flex flex-col w-60 shrink-0 h-screen" style="background-color: var(--pw-bg); border-right: 1px solid var(--pw-border);">
148
+
149
+ <!-- Logo Area -->
150
+ <div class="flex items-center gap-3 px-5 py-5" style="border-bottom: 1px solid var(--pw-border-subtle);">
151
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
152
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
153
+ <path d="M9 12l2 2 4-4"/>
154
+ </svg>
155
+ <span class="truncate text-lg font-semibold tracking-tight" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">PatchWarden</span>
156
+ </div>
157
+
158
+ <!-- Navigation -->
159
+ <nav class="flex-1 flex flex-col gap-1 px-3 pt-4 overflow-y-auto">
160
+
161
+ <a href="/pages/dashboard.html" data-nav-key="dashboard" data-dom-id="nav-dashboard" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
162
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
163
+ <rect x="3" y="3" width="7" height="7" rx="1"/>
164
+ <rect x="14" y="3" width="7" height="7" rx="1"/>
165
+ <rect x="3" y="14" width="7" height="7" rx="1"/>
166
+ <rect x="14" y="14" width="7" height="7" rx="1"/>
167
+ </svg>
168
+ <span class="truncate" data-zh="控制台" data-en="Dashboard">控制台</span>
169
+ </a>
170
+
171
+ <a href="/pages/tasks.html" data-nav-key="tasks" data-active="true" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
172
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
173
+ <path d="M9 11l3 3L22 4"/>
174
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
175
+ </svg>
176
+ <span class="truncate" data-zh="任务面板" data-en="Tasks">任务面板</span>
177
+ </a>
178
+
179
+ <a href="/pages/workspace.html" data-nav-key="workspace" data-dom-id="nav-workspace" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
180
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
181
+ <path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2z"/>
182
+ </svg>
183
+ <span class="truncate" data-zh="工作区" data-en="Workspace">工作区</span>
184
+ </a>
185
+
186
+ <a href="/pages/audit.html" data-nav-key="audit" data-dom-id="nav-audit" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
187
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
188
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
189
+ <polyline points="14 2 14 8 20 8"/>
190
+ <line x1="16" y1="13" x2="8" y2="13"/>
191
+ <line x1="16" y1="17" x2="8" y2="17"/>
192
+ <polyline points="10 9 9 9 8 9"/>
193
+ </svg>
194
+ <span class="truncate" data-zh="审计日志" data-en="Audit Log">审计日志</span>
195
+ </a>
196
+
197
+ <a href="/pages/direct-sessions.html" data-nav-key="direct-sessions" data-dom-id="nav-direct-sessions" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
198
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
199
+ <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
200
+ <line x1="4" y1="22" x2="4" y2="15"/>
201
+ </svg>
202
+ <span class="truncate" data-zh="Direct 会话" data-en="Direct Sessions">Direct 会话</span>
203
+ </a>
204
+
205
+ <a href="/pages/logs.html" data-nav-key="logs" data-dom-id="nav-logs" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
206
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
207
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
208
+ <polyline points="14 2 14 8 20 8"/>
209
+ <line x1="16" y1="13" x2="8" y2="13"/>
210
+ <line x1="16" y1="17" x2="8" y2="17"/>
211
+ </svg>
212
+ <span class="truncate" data-zh="日志" data-en="Logs">日志</span>
213
+ </a>
214
+
215
+ </nav>
216
+
217
+ <!-- Bottom Section -->
218
+ <div class="px-5 py-4" style="border-top: 1px solid var(--pw-border-subtle);">
219
+ <div class="flex items-center gap-2 mb-3">
220
+ <span class="text-xs font-medium px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-mono);">Core v0.6.0</span>
221
+ </div>
222
+ <div class="flex items-center gap-2">
223
+ <span class="shrink-0 w-2 h-2 rounded-full" style="background-color: var(--pw-state-success);"></span>
224
+ <span class="text-xs truncate" style="color: var(--pw-text-tertiary);">Connected</span>
225
+ </div>
226
+ </div>
227
+
228
+ </aside>
229
+
230
+ <!-- ========== MAIN AREA ========== -->
231
+ <div class="flex flex-col flex-1 min-w-0 h-screen overflow-hidden">
232
+
233
+ <!-- Top Bar -->
234
+ <header class="flex items-center justify-between shrink-0 px-6 py-3" style="border-bottom: 1px solid var(--pw-border); background-color: var(--pw-bg);">
235
+ <h1 class="text-lg font-semibold truncate min-w-0" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); text-wrap: balance; word-break: keep-all; overflow-wrap: break-word;" data-zh="任务面板" data-en="Tasks">
236
+ 任务面板
237
+ </h1>
238
+ <div class="flex items-center gap-4 shrink-0">
239
+ <div id="watcherStatus" class="flex items-center gap-1.5 text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);">
240
+ <span class="shrink-0 w-1.5 h-1.5 rounded-full" style="background-color: var(--pw-text-tertiary);"></span>
241
+ <span>Watcher: --</span>
242
+ </div>
243
+ <div class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); font-family: var(--pw-font-mono);">
244
+ PW
245
+ </div>
246
+ </div>
247
+ </header>
248
+
249
+ <!-- Page Content Area -->
250
+ <main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
251
+
252
+ <!-- ===== Filter Bar ===== -->
253
+ <div class="flex flex-wrap items-center gap-3 mb-5">
254
+ <!-- Status Filter Dropdown -->
255
+ <div class="relative">
256
+ <select id="statusFilter" class="appearance-none pl-3 pr-8 py-1.5 rounded-lg text-xs font-medium outline-none cursor-pointer transition-colors" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border-subtle); color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
257
+ <option value="" data-zh="全部状态" data-en="All Status">全部状态</option>
258
+ <option value="pending">Pending</option>
259
+ <option value="running">Running</option>
260
+ <option value="done">Done</option>
261
+ <option value="failed">Failed</option>
262
+ </select>
263
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-tertiary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none">
264
+ <polyline points="6 9 12 15 18 9"/>
265
+ </svg>
266
+ </div>
267
+
268
+ <!-- Refresh Button -->
269
+ <button id="refreshBtn" class="flex items-center gap-2 px-4 py-1.5 rounded-lg text-xs font-semibold whitespace-nowrap transition-colors" style="background-color: var(--pw-bg-surface); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
270
+ <svg id="refreshIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
271
+ <polyline points="23 4 23 10 17 10"/>
272
+ <polyline points="1 20 1 14 7 14"/>
273
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
274
+ </svg>
275
+ <span data-zh="刷新" data-en="Refresh">刷新</span>
276
+ </button>
277
+
278
+ <!-- Spacer -->
279
+ <div class="flex-1"></div>
280
+
281
+ <!-- Count Summary -->
282
+ <div id="countSummary" class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);">--</div>
283
+ </div>
284
+
285
+ <!-- ===== Task Table (border-led card) ===== -->
286
+ <div class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-left: 3px solid var(--pw-accent);">
287
+
288
+ <!-- Loading State -->
289
+ <div id="loadingState" class="py-16 text-center">
290
+ <svg class="pw-spin inline-block mb-3" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-tertiary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
291
+ <line x1="12" y1="2" x2="12" y2="6"/>
292
+ <line x1="12" y1="18" x2="12" y2="22"/>
293
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
294
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
295
+ <line x1="2" y1="12" x2="6" y2="12"/>
296
+ <line x1="18" y1="12" x2="22" y2="12"/>
297
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
298
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
299
+ </svg>
300
+ <p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="加载中..." data-en="Loading...">加载中...</p>
301
+ </div>
302
+
303
+ <!-- Error State -->
304
+ <div id="errorState" class="hidden py-16 text-center">
305
+ <svg class="inline-block mb-3" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-state-error)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
306
+ <circle cx="12" cy="12" r="10"/>
307
+ <line x1="12" y1="8" x2="12" y2="12"/>
308
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
309
+ </svg>
310
+ <p class="text-sm font-medium" style="color: var(--pw-state-error); font-family: var(--pw-font-sans);" data-zh="加载失败" data-en="Load Failed">加载失败</p>
311
+ <p id="errorMessage" class="text-xs mt-2 px-6 break-words" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);"></p>
312
+ <button id="retryBtn" class="mt-4 px-4 py-1.5 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" data-zh="重试" data-en="Retry">重试</button>
313
+ </div>
314
+
315
+ <!-- Empty State -->
316
+ <div id="emptyState" class="hidden py-16 text-center">
317
+ <svg class="inline-block mb-3" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-tertiary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
318
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
319
+ <polyline points="14 2 14 8 20 8"/>
320
+ <line x1="9" y1="13" x2="15" y2="13"/>
321
+ </svg>
322
+ <p class="text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无任务" data-en="No Tasks">暂无任务</p>
323
+ </div>
324
+
325
+ <!-- Table -->
326
+ <div id="tableWrapper" class="hidden overflow-x-auto">
327
+ <table class="w-full text-left" style="min-width: 960px;">
328
+ <thead>
329
+ <tr style="border-bottom: 1px solid var(--pw-border);">
330
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="任务ID" data-en="Task ID">任务ID</th>
331
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="标题" data-en="Title">标题</th>
332
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);">Agent</th>
333
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="状态" data-en="Status">状态</th>
334
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="阶段" data-en="Phase">阶段</th>
335
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="创建时间" data-en="Created">创建时间</th>
336
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="待因" data-en="Pending Reason">待因</th>
337
+ <th class="px-4 py-3 text-xs font-semibold whitespace-nowrap text-right" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="操作" data-en="Actions">操作</th>
338
+ </tr>
339
+ </thead>
340
+ <tbody id="taskTableBody">
341
+ <!-- Rendered by JavaScript -->
342
+ </tbody>
343
+ </table>
344
+ </div>
345
+ </div>
346
+
347
+ </main>
348
+ </div>
349
+
350
+ <!-- Active nav item styling + sidebar hover states -->
351
+ <style>
352
+ [data-nav-key] {
353
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
354
+ }
355
+ [data-nav-key]:hover {
356
+ background-color: var(--pw-bg-hover);
357
+ color: var(--pw-text-primary);
358
+ }
359
+ [data-nav-key][data-active="true"] {
360
+ background-color: var(--pw-accent-subtle);
361
+ color: var(--pw-accent);
362
+ border-left: 3px solid var(--pw-accent);
363
+ padding-left: 9px;
364
+ }
365
+ input[type="text"]:focus,
366
+ select:focus {
367
+ border-color: var(--pw-border-focus);
368
+ background-color: var(--pw-bg-hover);
369
+ }
370
+ input[type="text"]::placeholder {
371
+ color: var(--pw-text-tertiary);
372
+ }
373
+ /* Table row hover */
374
+ tbody tr:hover {
375
+ background-color: var(--pw-bg-active) !important;
376
+ }
377
+ /* Pulsing blue glow for running status */
378
+ .pw-pulse-badge {
379
+ animation: pw-pulse-glow 2s ease-in-out infinite;
380
+ }
381
+ @keyframes pw-pulse-glow {
382
+ 0%, 100% {
383
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.35);
384
+ }
385
+ 50% {
386
+ box-shadow: 0 0 8px 2px rgba(59, 130, 246, 0.18);
387
+ }
388
+ }
389
+ /* Reduced motion */
390
+ @media (prefers-reduced-motion: reduce) {
391
+ .pw-pulse-badge {
392
+ animation-duration: 0.01ms;
393
+ }
394
+ .pw-spin {
395
+ animation-duration: 1.5s;
396
+ }
397
+ }
398
+ /* Button hover states */
399
+ button:hover {
400
+ filter: brightness(1.1);
401
+ }
402
+ button:active {
403
+ filter: brightness(0.95);
404
+ transform: scale(0.98);
405
+ }
406
+ button:focus-visible {
407
+ outline: 2px solid var(--pw-border-focus);
408
+ outline-offset: 1px;
409
+ }
410
+ /* Refresh button hover */
411
+ #refreshBtn:hover {
412
+ background-color: var(--pw-bg-hover) !important;
413
+ color: var(--pw-text-primary) !important;
414
+ border-color: var(--pw-border-focus) !important;
415
+ }
416
+ /* Retry button hover */
417
+ #retryBtn:hover {
418
+ background-color: var(--pw-bg-active) !important;
419
+ color: var(--pw-text-primary) !important;
420
+ }
421
+ </style>
422
+
423
+ <script>
424
+ (function () {
425
+ 'use strict';
426
+
427
+ var API_URL = '/api/tasks';
428
+
429
+ // status -> { label, color, text }
430
+ // color: success (green) | info (blue) | warning (yellow) | error (red) | muted (gray)
431
+ var STATUS_META = {
432
+ pending: { color: 'warning', text: '等待中' },
433
+ running: { color: 'info', text: '运行中' },
434
+ done: { color: 'success', text: '已完成' },
435
+ failed: { color: 'error', text: '失败' },
436
+ failed_scope_violation: { color: 'error', text: '范围违规' },
437
+ canceled: { color: 'muted', text: '已取消' },
438
+ timeout: { color: 'muted', text: '超时' }
439
+ };
440
+
441
+ var PHASE_LABELS = {
442
+ queued: '排队中',
443
+ preparing: '准备中',
444
+ running: '运行中',
445
+ verifying: '验证中',
446
+ collecting_artifacts: '收集产物',
447
+ completed: '已完成'
448
+ };
449
+
450
+ var allTasks = [];
451
+ var currentFilter = '';
452
+
453
+ // ---- DOM helpers ----
454
+ function $(id) { return document.getElementById(id); }
455
+ function show(el) { el && el.classList.remove('hidden'); }
456
+ function hide(el) { el && el.classList.add('hidden'); }
457
+
458
+ function escapeHtml(s) {
459
+ if (s === null || s === undefined) return '';
460
+ return String(s).replace(/[&<>"']/g, function (c) {
461
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
462
+ });
463
+ }
464
+
465
+ function escapeAttr(s) { return escapeHtml(s); }
466
+
467
+ function truncate(s, n) {
468
+ if (!s) return '';
469
+ s = String(s);
470
+ return s.length > n ? s.slice(0, n) + '…' : s;
471
+ }
472
+
473
+ function pad2(n) { return String(n).padStart(2, '0'); }
474
+
475
+ function formatTime(iso) {
476
+ if (!iso) return '—';
477
+ var d = new Date(iso);
478
+ if (isNaN(d.getTime())) return String(iso);
479
+ return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
480
+ + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
481
+ }
482
+
483
+ function getStatusMeta(status) {
484
+ return STATUS_META[status] || { color: 'muted', text: status || '—' };
485
+ }
486
+
487
+ function getPhaseLabel(phase) {
488
+ return PHASE_LABELS[phase] || phase || '—';
489
+ }
490
+
491
+ // ---- State rendering ----
492
+ function showLoading() {
493
+ show($('loadingState'));
494
+ hide($('errorState'));
495
+ hide($('emptyState'));
496
+ hide($('tableWrapper'));
497
+ $('countSummary').textContent = '--';
498
+ }
499
+
500
+ function showError(msg) {
501
+ hide($('loadingState'));
502
+ show($('errorState'));
503
+ hide($('emptyState'));
504
+ hide($('tableWrapper'));
505
+ $('errorMessage').textContent = msg || '未知错误';
506
+ $('countSummary').textContent = '--';
507
+ }
508
+
509
+ function showEmpty() {
510
+ hide($('loadingState'));
511
+ hide($('errorState'));
512
+ show($('emptyState'));
513
+ hide($('tableWrapper'));
514
+ }
515
+
516
+ function showTable() {
517
+ hide($('loadingState'));
518
+ hide($('errorState'));
519
+ hide($('emptyState'));
520
+ show($('tableWrapper'));
521
+ }
522
+
523
+ // ---- Badge styling ----
524
+ function badgeStyle(color) {
525
+ switch (color) {
526
+ case 'success': return 'background-color: var(--pw-state-success-bg); color: var(--pw-state-success);';
527
+ case 'info': return 'background-color: var(--pw-state-info-bg); color: var(--pw-state-info);';
528
+ case 'warning': return 'background-color: var(--pw-state-warning-bg); color: var(--pw-state-warning);';
529
+ case 'error': return 'background-color: var(--pw-state-error-bg); color: var(--pw-state-error);';
530
+ default: return 'background-color: var(--pw-bg-hover); color: var(--pw-text-secondary);';
531
+ }
532
+ }
533
+
534
+ function agentBadgeStyle(agent) {
535
+ // opencode -> purple tint (matches original design), others -> blue tint
536
+ if (agent === 'opencode') {
537
+ return 'background-color: rgba(168, 85, 247, 0.12); color: #a855f7;';
538
+ }
539
+ return 'background-color: var(--pw-state-info-bg); color: var(--pw-state-info);';
540
+ }
541
+
542
+ // ---- Row rendering ----
543
+ function renderRow(task, idx) {
544
+ var meta = getStatusMeta(task.status);
545
+ var style = badgeStyle(meta.color);
546
+ var pulseClass = (task.status === 'running') ? ' pw-pulse-badge' : '';
547
+ var truncatedId = truncate(task.task_id, 12);
548
+ var createdAt = formatTime(task.created_at);
549
+ var phaseLabel = getPhaseLabel(task.phase);
550
+ var altBg = (idx % 2 === 1) ? ' background-color: var(--pw-bg-hover);' : '';
551
+
552
+ var pendingCell;
553
+ if (task.pending_reason) {
554
+ pendingCell = '<span class="inline-block truncate" style="max-width: 180px; color: var(--pw-state-warning); font-family: var(--pw-font-sans);" title="'
555
+ + escapeAttr(task.pending_reason) + '">'
556
+ + escapeHtml(truncate(task.pending_reason, 28))
557
+ + '</span>';
558
+ } else {
559
+ pendingCell = '<span style="color: var(--pw-text-tertiary);">—</span>';
560
+ }
561
+
562
+ var detailHref = '/pages/task-detail.html?id=' + encodeURIComponent(task.task_id || '');
563
+
564
+ return ''
565
+ + '<tr class="transition-colors" style="border-bottom: 1px solid var(--pw-border-subtle);' + altBg + '">'
566
+ + '<td class="px-4 py-3 text-xs whitespace-nowrap" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);" title="' + escapeAttr(task.task_id || '') + '">' + escapeHtml(truncatedId) + '</td>'
567
+ + '<td class="px-4 py-3 text-sm" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); max-width: 320px;">' + escapeHtml(task.title || '—') + '</td>'
568
+ + '<td class="px-4 py-3">'
569
+ + '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium whitespace-nowrap" style="' + agentBadgeStyle(task.agent) + ' font-family: var(--pw-font-mono);">' + escapeHtml(task.agent || '—') + '</span>'
570
+ + '</td>'
571
+ + '<td class="px-4 py-3">'
572
+ + '<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap' + pulseClass + '" style="' + style + ' font-family: var(--pw-font-sans);" data-status="' + escapeAttr(task.status || '') + '">'
573
+ + '<span class="shrink-0 w-1.5 h-1.5 rounded-full" style="background-color: currentColor;"></span>'
574
+ + escapeHtml(meta.text)
575
+ + '</span>'
576
+ + '</td>'
577
+ + '<td class="px-4 py-3 text-xs whitespace-nowrap" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);" data-phase="' + escapeAttr(task.phase || '') + '">' + escapeHtml(phaseLabel) + '</td>'
578
+ + '<td class="px-4 py-3 text-xs whitespace-nowrap" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);" title="' + escapeAttr(task.created_at || '') + '">' + escapeHtml(createdAt) + '</td>'
579
+ + '<td class="px-4 py-3 text-xs" style="font-family: var(--pw-font-sans);">' + pendingCell + '</td>'
580
+ + '<td class="px-4 py-3 text-right">'
581
+ + '<a href="' + detailHref + '" class="inline-block px-3 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" data-zh="查看详情" data-en="View Details">查看详情</a>'
582
+ + '</td>'
583
+ + '</tr>';
584
+ }
585
+
586
+ function renderTasks() {
587
+ var filtered = currentFilter
588
+ ? allTasks.filter(function (t) { return t && t.status === currentFilter; })
589
+ : allTasks.slice();
590
+
591
+ // Sort by created_at desc (newest first). Falls back gracefully if missing.
592
+ filtered.sort(function (a, b) {
593
+ var ta = a && a.created_at ? Date.parse(a.created_at) : 0;
594
+ var tb = b && b.created_at ? Date.parse(b.created_at) : 0;
595
+ if (isNaN(ta)) ta = 0;
596
+ if (isNaN(tb)) tb = 0;
597
+ return tb - ta;
598
+ });
599
+
600
+ if (filtered.length === 0) {
601
+ showEmpty();
602
+ } else {
603
+ var html = '';
604
+ for (var i = 0; i < filtered.length; i++) {
605
+ html += renderRow(filtered[i], i);
606
+ }
607
+ $('taskTableBody').innerHTML = html;
608
+ showTable();
609
+ }
610
+
611
+ // Count summary
612
+ if (currentFilter) {
613
+ $('countSummary').textContent = '显示 ' + filtered.length + ' / ' + allTasks.length + ' 条';
614
+ } else {
615
+ $('countSummary').textContent = '共 ' + allTasks.length + ' 条';
616
+ }
617
+ }
618
+
619
+ function renderWatcher(watcher) {
620
+ var el = $('watcherStatus');
621
+ if (!watcher) {
622
+ el.innerHTML = '<span class="shrink-0 w-1.5 h-1.5 rounded-full" style="background-color: var(--pw-text-tertiary);"></span><span>Watcher: --</span>';
623
+ return;
624
+ }
625
+ var status = watcher.status || 'unknown';
626
+ var dotColor;
627
+ if (status === 'healthy') {
628
+ dotColor = 'var(--pw-state-success)';
629
+ } else if (status === 'unhealthy' || status === 'error') {
630
+ dotColor = 'var(--pw-state-error)';
631
+ } else {
632
+ dotColor = 'var(--pw-state-warning)';
633
+ }
634
+ el.innerHTML = '<span class="shrink-0 w-1.5 h-1.5 rounded-full" style="background-color: ' + dotColor + ';"></span><span>Watcher: ' + escapeHtml(status) + '</span>';
635
+ }
636
+
637
+ // ---- API call ----
638
+ function setRefreshing(isRefreshing) {
639
+ var icon = $('refreshIcon');
640
+ if (!icon) return;
641
+ if (isRefreshing) {
642
+ icon.classList.add('pw-spin');
643
+ } else {
644
+ icon.classList.remove('pw-spin');
645
+ }
646
+ }
647
+
648
+ function fetchTasks() {
649
+ showLoading();
650
+ setRefreshing(true);
651
+ fetch(API_URL, {
652
+ method: 'GET',
653
+ headers: { 'Accept': 'application/json' },
654
+ cache: 'no-store'
655
+ })
656
+ .then(function (res) {
657
+ if (!res.ok) {
658
+ return res.text().then(function (body) {
659
+ throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
660
+ });
661
+ }
662
+ return res.json();
663
+ })
664
+ .then(function (data) {
665
+ allTasks = (data && Array.isArray(data.tasks)) ? data.tasks : [];
666
+ renderWatcher(data && data.watcher);
667
+ renderTasks();
668
+ })
669
+ .catch(function (err) {
670
+ var msg = (err && err.message) ? err.message : '未知错误';
671
+ // Mask fetch network errors that expose no useful detail
672
+ if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
673
+ msg = '无法连接到 ' + API_URL + '(请确认 PatchWarden 服务正在运行)';
674
+ }
675
+ showError(msg);
676
+ })
677
+ .finally(function () {
678
+ setRefreshing(false);
679
+ });
680
+ }
681
+
682
+ // ---- Event wiring ----
683
+ $('statusFilter').addEventListener('change', function (e) {
684
+ currentFilter = e.target.value;
685
+ renderTasks();
686
+ });
687
+
688
+ $('refreshBtn').addEventListener('click', fetchTasks);
689
+
690
+ var retryBtn = $('retryBtn');
691
+ if (retryBtn) {
692
+ retryBtn.addEventListener('click', fetchTasks);
693
+ }
694
+
695
+ // Initial load
696
+ fetchTasks();
697
+ })();
698
+ </script>
699
+
700
+ <script>lucide.createIcons();</script>
701
+ </body>
702
+ </html>