@vpxa/aikit 0.1.164 → 0.1.166

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.
@@ -4,69 +4,120 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>AI Kit - Task Plan Static Viewer</title>
7
- <style>
7
+ <style id="viewer-style">
8
8
  :root {
9
- --dt-bg-primary: #020617;
10
- --dt-bg-surface: #0f172a;
11
- --dt-text-primary: #f8fafc;
12
- --dt-text-secondary: #94a3b8;
13
- --dt-border: #334155;
14
- --dt-cyan: #06b6d4;
15
- --dt-emerald: #10b981;
16
- --dt-violet: #8b5cf6;
17
- --dt-amber: #f59e0b;
18
- --dt-rose: #f43f5e;
19
- --dt-slate: #64748b;
20
- --dt-grid: #1e293b;
21
- --dt-shadow: rgba(2, 6, 23, 0.38);
22
- --dt-toolbar-bg: rgba(15, 23, 42, 0.82);
23
- --dt-button-bg: rgba(15, 23, 42, 0.88);
24
- --dt-button-hover: rgba(30, 41, 59, 0.95);
25
- --dt-panel-bg: linear-gradient(180deg, rgba(15, 23, 42, 0.94), rgba(2, 6, 23, 0.98));
26
- --dt-badge-bg: rgba(15, 23, 42, 0.88);
27
- --dt-node-fill: rgba(15, 23, 42, 0.92);
28
- --tp-indigo: #6366f1;
29
- --tp-pink: #ec4899;
30
- --tp-orange: #f97316;
31
- --tp-teal: #14b8a6;
32
- --tp-purple: #a855f7;
33
- --tp-blue: #3b82f6;
34
- --tp-green: #22c55e;
35
- --tp-red: #ef4444;
36
- --tp-gray: #94a3b8;
9
+ --font-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
10
+ --font-mono: "JetBrains Mono", "Geist Mono", "Fira Code", "SF Mono", Consolas, monospace;
11
+
12
+ --space-1: 4px;
13
+ --space-2: 8px;
14
+ --space-3: 12px;
15
+ --space-4: 16px;
16
+ --space-5: 20px;
17
+ --space-6: 24px;
18
+ --space-8: 32px;
19
+ --space-10: 40px;
20
+ --space-12: 48px;
21
+
22
+ --radius-sm: 10px;
23
+ --radius-md: 14px;
24
+ --radius-xl: 22px;
25
+ --radius-pill: 999px;
26
+
27
+ --tp-agent-researcher: #818cf8;
28
+ --tp-agent-implementer: #34d399;
29
+ --tp-agent-frontend: #f472b6;
30
+ --tp-agent-reviewer: #fbbf24;
31
+ --tp-agent-debugger: #f87171;
32
+ --tp-agent-security: #fb923c;
33
+ --tp-agent-explorer: #22d3ee;
34
+ --tp-agent-documenter: #a78bfa;
35
+ --tp-agent-refactor: #2dd4bf;
36
+ --tp-agent-planner: #c084fc;
37
+ --tp-agent-orchestrator: #94a3b8;
38
+
39
+ --tp-status-pending: #94a3b8;
40
+ --tp-status-in-progress: #60a5fa;
41
+ --tp-status-done: #34d399;
42
+ --tp-status-blocked: #f87171;
43
+
44
+ --tp-phase-1: #06b6d4;
45
+ --tp-phase-2: #6366f1;
46
+ --tp-phase-3: #f59e0b;
47
+ --tp-phase-4: #ec4899;
48
+ --tp-phase-5: #14b8a6;
49
+ --tp-phase-6: #8b5cf6;
50
+
51
+ --viewer-bg: #020617;
52
+ --viewer-surface: #0f172a;
53
+ --viewer-surface-2: #111c32;
54
+ --viewer-card: rgba(15, 23, 42, 0.92);
55
+ --viewer-card-strong: rgba(20, 31, 56, 0.98);
56
+ --viewer-toolbar: rgba(8, 15, 31, 0.84);
57
+ --viewer-text: #f8fafc;
58
+ --viewer-text-muted: #94a3b8;
59
+ --viewer-text-dim: #64748b;
60
+ --viewer-border: rgba(148, 163, 184, 0.22);
61
+ --viewer-border-strong: rgba(148, 163, 184, 0.34);
62
+ --viewer-shadow: 0 30px 70px rgba(2, 6, 23, 0.42);
63
+ --viewer-grid: rgba(51, 65, 85, 0.38);
64
+ --viewer-badge-bg: rgba(15, 23, 42, 0.88);
65
+ --viewer-button-bg: rgba(15, 23, 42, 0.78);
66
+ --viewer-button-hover: rgba(30, 41, 59, 0.96);
67
+ --viewer-button-active: rgba(37, 99, 235, 0.14);
68
+ --viewer-empty-bg: rgba(15, 23, 42, 0.72);
69
+ --viewer-toast-bg: rgba(127, 29, 29, 0.92);
70
+ --viewer-toast-border: rgba(248, 113, 113, 0.55);
71
+ --viewer-backdrop: radial-gradient(circle at top left, rgba(6, 182, 212, 0.14), transparent 24%), radial-gradient(circle at bottom right, rgba(139, 92, 246, 0.12), transparent 28%), linear-gradient(180deg, rgba(15, 23, 42, 0.2), rgba(2, 6, 23, 0));
37
72
  }
38
73
 
39
74
  [data-theme="light"] {
40
- --dt-bg-primary: #ffffff;
41
- --dt-bg-surface: #f8fafc;
42
- --dt-text-primary: #0f172a;
43
- --dt-text-secondary: #64748b;
44
- --dt-border: #e2e8f0;
45
- --dt-grid: rgba(148, 163, 184, 0.12);
46
- --dt-shadow: rgba(148, 163, 184, 0.24);
47
- --dt-toolbar-bg: rgba(255, 255, 255, 0.88);
48
- --dt-button-bg: rgba(248, 250, 252, 0.96);
49
- --dt-button-hover: rgba(226, 232, 240, 0.96);
50
- --dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
51
- --dt-badge-bg: rgba(255, 255, 255, 0.9);
52
- --dt-node-fill: rgba(255, 255, 255, 0.96);
75
+ --viewer-bg: #ffffff;
76
+ --viewer-surface: #f8fafc;
77
+ --viewer-surface-2: #eef4fb;
78
+ --viewer-card: rgba(255, 255, 255, 0.96);
79
+ --viewer-card-strong: rgba(248, 250, 252, 0.98);
80
+ --viewer-toolbar: rgba(255, 255, 255, 0.9);
81
+ --viewer-text: #0f172a;
82
+ --viewer-text-muted: #475569;
83
+ --viewer-text-dim: #64748b;
84
+ --viewer-border: rgba(148, 163, 184, 0.28);
85
+ --viewer-border-strong: rgba(100, 116, 139, 0.36);
86
+ --viewer-shadow: 0 22px 48px rgba(148, 163, 184, 0.2);
87
+ --viewer-grid: rgba(203, 213, 225, 0.55);
88
+ --viewer-badge-bg: rgba(255, 255, 255, 0.92);
89
+ --viewer-button-bg: rgba(255, 255, 255, 0.78);
90
+ --viewer-button-hover: rgba(241, 245, 249, 0.98);
91
+ --viewer-button-active: rgba(59, 130, 246, 0.1);
92
+ --viewer-empty-bg: rgba(255, 255, 255, 0.82);
93
+ --viewer-toast-bg: rgba(254, 242, 242, 0.96);
94
+ --viewer-toast-border: rgba(248, 113, 113, 0.42);
95
+ --viewer-backdrop: radial-gradient(circle at top left, rgba(99, 102, 241, 0.08), transparent 24%), radial-gradient(circle at bottom right, rgba(20, 184, 166, 0.08), transparent 28%), linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(248, 250, 252, 0));
53
96
  }
54
97
 
55
98
  @media (prefers-color-scheme: light) {
56
99
  :root:not([data-theme="dark"]) {
57
- --dt-bg-primary: #ffffff;
58
- --dt-bg-surface: #f8fafc;
59
- --dt-text-primary: #0f172a;
60
- --dt-text-secondary: #64748b;
61
- --dt-border: #e2e8f0;
62
- --dt-grid: rgba(148, 163, 184, 0.12);
63
- --dt-shadow: rgba(148, 163, 184, 0.24);
64
- --dt-toolbar-bg: rgba(255, 255, 255, 0.88);
65
- --dt-button-bg: rgba(248, 250, 252, 0.96);
66
- --dt-button-hover: rgba(226, 232, 240, 0.96);
67
- --dt-panel-bg: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 1));
68
- --dt-badge-bg: rgba(255, 255, 255, 0.9);
69
- --dt-node-fill: rgba(255, 255, 255, 0.96);
100
+ --viewer-bg: #ffffff;
101
+ --viewer-surface: #f8fafc;
102
+ --viewer-surface-2: #eef4fb;
103
+ --viewer-card: rgba(255, 255, 255, 0.96);
104
+ --viewer-card-strong: rgba(248, 250, 252, 0.98);
105
+ --viewer-toolbar: rgba(255, 255, 255, 0.9);
106
+ --viewer-text: #0f172a;
107
+ --viewer-text-muted: #475569;
108
+ --viewer-text-dim: #64748b;
109
+ --viewer-border: rgba(148, 163, 184, 0.28);
110
+ --viewer-border-strong: rgba(100, 116, 139, 0.36);
111
+ --viewer-shadow: 0 22px 48px rgba(148, 163, 184, 0.2);
112
+ --viewer-grid: rgba(203, 213, 225, 0.55);
113
+ --viewer-badge-bg: rgba(255, 255, 255, 0.92);
114
+ --viewer-button-bg: rgba(255, 255, 255, 0.78);
115
+ --viewer-button-hover: rgba(241, 245, 249, 0.98);
116
+ --viewer-button-active: rgba(59, 130, 246, 0.1);
117
+ --viewer-empty-bg: rgba(255, 255, 255, 0.82);
118
+ --viewer-toast-bg: rgba(254, 242, 242, 0.96);
119
+ --viewer-toast-border: rgba(248, 113, 113, 0.42);
120
+ --viewer-backdrop: radial-gradient(circle at top left, rgba(99, 102, 241, 0.08), transparent 24%), radial-gradient(circle at bottom right, rgba(20, 184, 166, 0.08), transparent 28%), linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(248, 250, 252, 0));
70
121
  }
71
122
  }
72
123
 
@@ -76,998 +127,1325 @@
76
127
 
77
128
  html,
78
129
  body {
79
- height: 100%;
130
+ min-height: 100%;
80
131
  margin: 0;
81
132
  }
82
133
 
83
134
  body {
84
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
85
- color: var(--dt-text-primary);
86
- background:
87
- radial-gradient(circle at top left, rgba(99, 102, 241, 0.13), transparent 28%),
88
- radial-gradient(circle at bottom right, rgba(20, 184, 166, 0.12), transparent 28%),
89
- var(--dt-bg-primary);
135
+ overflow: hidden;
136
+ font-family: var(--font-sans);
137
+ color: var(--viewer-text);
138
+ background: var(--viewer-backdrop), var(--viewer-bg);
90
139
  }
91
140
 
92
- .shell {
93
- min-height: 100%;
141
+ button {
142
+ appearance: none;
143
+ border: 1px solid var(--viewer-border);
144
+ border-radius: var(--radius-pill);
145
+ background: var(--viewer-button-bg);
146
+ color: var(--viewer-text);
147
+ cursor: pointer;
148
+ font: inherit;
149
+ font-size: 13px;
150
+ font-weight: 600;
151
+ line-height: 1;
152
+ padding: 12px 16px;
153
+ transition: transform 160ms ease, border-color 160ms ease, background-color 160ms ease, color 160ms ease;
154
+ }
155
+
156
+ button:hover,
157
+ button:focus-visible {
158
+ outline: none;
159
+ transform: translateY(-1px);
160
+ border-color: var(--viewer-border-strong);
161
+ background: var(--viewer-button-hover);
162
+ }
163
+
164
+ button:focus-visible {
165
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
166
+ }
167
+
168
+ .viewer-shell {
169
+ min-height: 100vh;
94
170
  display: grid;
95
171
  grid-template-rows: auto 1fr;
96
172
  }
97
173
 
98
- .toolbar {
174
+ .viewer-toolbar {
99
175
  position: sticky;
100
176
  top: 0;
101
- z-index: 2;
177
+ z-index: 10;
102
178
  display: flex;
103
- align-items: center;
179
+ align-items: flex-start;
104
180
  justify-content: space-between;
105
- gap: 1rem;
106
- padding: 1rem 1.25rem;
107
- border-bottom: 1px solid var(--dt-border);
108
- background: var(--dt-toolbar-bg);
109
- backdrop-filter: blur(16px);
181
+ gap: var(--space-4);
182
+ padding: var(--space-5) var(--space-6);
183
+ border-bottom: 1px solid var(--viewer-border);
184
+ background: var(--viewer-toolbar);
185
+ backdrop-filter: blur(18px);
186
+ }
187
+
188
+ .viewer-title {
189
+ min-width: 0;
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 6px;
110
193
  }
111
194
 
112
- .title h1 {
195
+ .viewer-title__eyebrow {
196
+ color: var(--viewer-text-dim);
197
+ font-family: var(--font-mono);
198
+ font-size: 11px;
199
+ letter-spacing: 0.1em;
200
+ text-transform: uppercase;
201
+ }
202
+
203
+ .viewer-title h1 {
113
204
  margin: 0;
114
- font-size: 1rem;
115
- font-weight: 700;
116
- letter-spacing: 0.02em;
205
+ font-size: clamp(20px, 2.6vw, 30px);
206
+ font-weight: 600;
207
+ letter-spacing: -0.03em;
208
+ line-height: 1.05;
117
209
  }
118
210
 
119
- .title p {
120
- margin: 0.2rem 0 0;
121
- font-size: 0.82rem;
122
- color: var(--dt-text-secondary);
211
+ .viewer-title p {
212
+ max-width: 76ch;
213
+ margin: 0;
214
+ color: var(--viewer-text-muted);
215
+ font-size: 14px;
216
+ line-height: 1.55;
123
217
  }
124
218
 
125
- .actions {
219
+ .viewer-actions {
126
220
  display: flex;
127
221
  flex-wrap: wrap;
128
222
  justify-content: flex-end;
129
- gap: 0.6rem;
223
+ gap: var(--space-2);
224
+ padding-top: 2px;
130
225
  }
131
226
 
132
- button {
133
- appearance: none;
134
- border: 1px solid var(--dt-border);
135
- background: var(--dt-button-bg);
136
- color: var(--dt-text-primary);
227
+ .viewer-stage {
228
+ overflow: auto;
229
+ padding: var(--space-6);
230
+ }
231
+
232
+ .task-plan__shell {
233
+ position: relative;
234
+ max-width: 1280px;
235
+ margin: 0 auto;
236
+ }
237
+
238
+ .task-plan__surface {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: var(--space-6);
242
+ min-height: calc(100vh - 180px);
243
+ padding: clamp(18px, 2vw, 28px);
244
+ border: 1px solid var(--viewer-border);
245
+ border-radius: calc(var(--radius-xl) + 4px);
246
+ background:
247
+ linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 28%),
248
+ linear-gradient(90deg, transparent 0, transparent calc(100% - 1px), var(--viewer-grid) calc(100% - 1px), var(--viewer-grid) 100%),
249
+ var(--viewer-card-strong);
250
+ box-shadow: var(--viewer-shadow);
251
+ isolation: isolate;
252
+ }
253
+
254
+ .task-plan__pipeline {
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: 0;
258
+ }
259
+
260
+ .task-plan__phase-block {
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 0;
264
+ }
265
+
266
+ .task-plan-phase {
267
+ display: flex;
268
+ flex-direction: column;
269
+ }
270
+
271
+ .task-plan-phase__content {
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: var(--space-4);
275
+ padding: clamp(18px, 2vw, 24px);
276
+ border: 1px solid var(--viewer-border);
277
+ border-left: 4px solid var(--phase-accent);
278
+ border-radius: var(--radius-xl);
279
+ background:
280
+ linear-gradient(135deg, rgba(255, 255, 255, 0.04), transparent 34%),
281
+ linear-gradient(180deg, rgba(2, 6, 23, 0.05), transparent 44%),
282
+ var(--viewer-card);
283
+ }
284
+
285
+ .task-plan-phase__header {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 14px;
289
+ }
290
+
291
+ .task-plan-phase__number {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ width: 34px;
296
+ height: 34px;
297
+ border: 1px solid color-mix(in srgb, var(--phase-accent), var(--viewer-border) 45%);
137
298
  border-radius: 999px;
138
- padding: 0.7rem 1rem;
139
- font: inherit;
140
- cursor: pointer;
141
- transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
299
+ background: color-mix(in srgb, var(--phase-accent), transparent 88%);
300
+ color: var(--phase-accent);
301
+ font-family: var(--font-mono);
302
+ font-size: 12px;
303
+ font-weight: 700;
304
+ flex-shrink: 0;
142
305
  }
143
306
 
144
- button:hover {
145
- background: var(--dt-button-hover);
146
- transform: translateY(-1px);
307
+ .task-plan-phase__label {
308
+ flex: 1;
309
+ min-width: 0;
310
+ margin: 0;
311
+ color: var(--viewer-text);
312
+ font-size: clamp(18px, 2vw, 22px);
313
+ font-weight: 600;
314
+ letter-spacing: -0.02em;
315
+ line-height: 1.2;
316
+ }
317
+
318
+ .task-plan-phase__badge {
319
+ flex-shrink: 0;
320
+ border: 1px solid var(--viewer-border);
321
+ border-radius: var(--radius-pill);
322
+ background: color-mix(in srgb, var(--phase-accent), transparent 92%);
323
+ color: var(--viewer-text-dim);
324
+ font-family: var(--font-mono);
325
+ font-size: 11px;
326
+ letter-spacing: 0.08em;
327
+ padding: 7px 10px;
328
+ text-transform: uppercase;
329
+ white-space: nowrap;
147
330
  }
148
331
 
149
- .stage {
150
- padding: 1rem;
332
+ .task-plan-phase__outcome {
333
+ margin: 0;
334
+ padding-bottom: var(--space-4);
335
+ border-bottom: 1px solid rgba(148, 163, 184, 0.14);
336
+ color: var(--viewer-text-muted);
337
+ font-size: 14px;
338
+ line-height: 1.6;
151
339
  }
152
340
 
153
- .canvas-shell {
341
+ .task-plan-phase__batches {
342
+ display: flex;
343
+ flex-direction: column;
344
+ gap: var(--space-4);
345
+ }
346
+
347
+ .task-plan-batch {
348
+ display: flex;
349
+ flex-direction: column;
350
+ gap: 10px;
351
+ }
352
+
353
+ .task-plan-batch + .task-plan-batch {
354
+ margin-top: 2px;
355
+ padding-top: var(--space-4);
356
+ border-top: 1px dashed rgba(148, 163, 184, 0.2);
357
+ }
358
+
359
+ .task-plan-batch__label {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 8px;
363
+ color: var(--viewer-text-dim);
364
+ font-family: var(--font-mono);
365
+ font-size: 11px;
366
+ letter-spacing: 0.08em;
367
+ text-transform: uppercase;
368
+ }
369
+
370
+ .task-plan-batch__icon {
371
+ display: inline-flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ width: 18px;
375
+ height: 18px;
376
+ border-radius: 999px;
377
+ background: rgba(148, 163, 184, 0.12);
378
+ color: var(--viewer-text-muted);
379
+ font-size: 12px;
380
+ line-height: 1;
381
+ }
382
+
383
+ .task-plan-batch__tasks {
384
+ display: grid;
385
+ grid-template-columns: 1fr;
386
+ gap: 12px;
387
+ width: 100%;
388
+ }
389
+
390
+ .task-plan-batch__tasks--parallel {
391
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
392
+ }
393
+
394
+ .task-plan-batch__tasks--sequential-compact {
395
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
396
+ }
397
+
398
+ .task-plan-task {
154
399
  position: relative;
155
- min-height: calc(100vh - 5.5rem);
156
- border: 1px solid var(--dt-border);
157
- border-radius: 24px;
158
- background:
159
- linear-gradient(var(--dt-grid) 1px, transparent 1px),
160
- linear-gradient(90deg, var(--dt-grid) 1px, transparent 1px),
161
- var(--dt-panel-bg);
162
- background-size: 40px 40px, 40px 40px, 100% 100%;
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 10px;
403
+ min-width: 0;
404
+ min-height: 148px;
405
+ padding: 16px 16px 16px 20px;
406
+ border: 1px solid var(--viewer-border);
407
+ border-radius: var(--radius-md);
408
+ background: color-mix(in srgb, var(--viewer-card), rgba(255, 255, 255, 0.02) 3%);
409
+ transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
410
+ }
411
+
412
+ .task-plan-task::before {
413
+ content: "";
414
+ position: absolute;
415
+ top: 0;
416
+ bottom: 0;
417
+ left: 0;
418
+ width: 4px;
419
+ border-radius: 4px 0 0 4px;
420
+ background: var(--card-accent);
421
+ }
422
+
423
+ .task-plan-task:hover {
424
+ transform: translateY(-1px);
425
+ border-color: color-mix(in srgb, var(--card-accent), var(--viewer-border) 44%);
426
+ box-shadow: 0 16px 28px rgba(2, 6, 23, 0.16);
427
+ }
428
+
429
+ .task-plan-task__topline {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: space-between;
433
+ gap: 12px;
434
+ }
435
+
436
+ .task-plan-task__agent {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 8px;
440
+ min-width: 0;
441
+ color: var(--card-accent);
442
+ font-family: var(--font-mono);
443
+ font-size: 11px;
444
+ font-weight: 700;
445
+ letter-spacing: 0.06em;
446
+ text-transform: uppercase;
447
+ }
448
+
449
+ .task-plan-task__agent-icon {
450
+ display: inline-flex;
451
+ align-items: center;
452
+ justify-content: center;
453
+ width: 22px;
454
+ height: 22px;
455
+ border-radius: 999px;
456
+ background: color-mix(in srgb, var(--card-accent), transparent 82%);
457
+ font-size: 12px;
458
+ flex-shrink: 0;
459
+ }
460
+
461
+ .task-plan-task__agent-label {
163
462
  overflow: hidden;
164
- box-shadow: 0 24px 60px var(--dt-shadow);
463
+ text-overflow: ellipsis;
464
+ white-space: nowrap;
165
465
  }
166
466
 
167
- svg {
168
- display: block;
169
- width: 100%;
170
- height: auto;
171
- min-height: 600px;
467
+ .task-plan-task__status {
468
+ display: inline-flex;
469
+ align-items: center;
470
+ gap: 6px;
471
+ flex-shrink: 0;
472
+ color: var(--status-color);
473
+ font-family: var(--font-mono);
474
+ font-size: 11px;
475
+ letter-spacing: 0.06em;
476
+ text-transform: uppercase;
477
+ white-space: nowrap;
478
+ }
479
+
480
+ .task-plan-task__status-dot {
481
+ width: 8px;
482
+ height: 8px;
483
+ border-radius: 999px;
484
+ background: currentColor;
485
+ box-shadow: 0 0 0 4px color-mix(in srgb, currentColor, transparent 82%);
486
+ flex-shrink: 0;
487
+ }
488
+
489
+ .task-plan-task__status[data-status="in-progress"] .task-plan-task__status-dot {
490
+ animation: task-plan-pulse 2s ease-in-out infinite;
491
+ }
492
+
493
+ .task-plan-task__title {
494
+ color: var(--viewer-text);
495
+ font-size: 15px;
496
+ font-weight: 700;
497
+ line-height: 1.4;
498
+ }
499
+
500
+ .task-plan-task__description {
501
+ margin: 0;
502
+ color: var(--viewer-text-muted);
503
+ font-size: 13px;
504
+ line-height: 1.55;
505
+ }
506
+
507
+ .task-plan-task__meta {
508
+ display: flex;
509
+ flex-wrap: wrap;
510
+ gap: 8px;
511
+ margin-top: auto;
512
+ }
513
+
514
+ .task-plan-task__chip {
515
+ display: inline-flex;
516
+ align-items: center;
517
+ gap: 6px;
518
+ max-width: 100%;
519
+ overflow: hidden;
520
+ border: 1px solid rgba(148, 163, 184, 0.2);
521
+ border-radius: var(--radius-pill);
522
+ background: color-mix(in srgb, var(--viewer-surface-2), transparent 16%);
523
+ color: var(--viewer-text-dim);
524
+ font-family: var(--font-mono);
525
+ font-size: 11px;
526
+ line-height: 1.1;
527
+ padding: 6px 10px;
528
+ text-overflow: ellipsis;
529
+ white-space: nowrap;
530
+ }
531
+
532
+ .task-plan__connector {
533
+ position: relative;
534
+ display: flex;
535
+ align-items: center;
536
+ justify-content: center;
537
+ height: 46px;
538
+ }
539
+
540
+ .task-plan__connector::before {
541
+ content: "";
542
+ position: absolute;
543
+ top: 0;
544
+ bottom: 12px;
545
+ left: 50%;
546
+ width: 2px;
547
+ background: repeating-linear-gradient(180deg, var(--viewer-border-strong) 0, var(--viewer-border-strong) 6px, transparent 6px, transparent 12px);
548
+ transform: translateX(-50%);
172
549
  }
173
550
 
174
- .empty-state {
551
+ .task-plan__connector::after {
552
+ content: "";
175
553
  position: absolute;
176
- inset: 0;
177
- display: none;
554
+ bottom: 0;
555
+ left: 50%;
556
+ width: 0;
557
+ height: 0;
558
+ border-left: 6px solid transparent;
559
+ border-right: 6px solid transparent;
560
+ border-top: 9px solid var(--viewer-border-strong);
561
+ transform: translateX(-50%);
562
+ }
563
+
564
+ .task-plan__summary {
565
+ display: flex;
566
+ flex-wrap: wrap;
567
+ gap: 18px 26px;
568
+ padding: 18px 20px;
569
+ border: 1px solid var(--viewer-border);
570
+ border-radius: var(--radius-xl);
571
+ background: color-mix(in srgb, var(--viewer-card), rgba(255, 255, 255, 0.02) 3%);
572
+ }
573
+
574
+ .task-plan-summary__stat {
575
+ display: flex;
576
+ flex-direction: column;
577
+ gap: 4px;
578
+ min-width: 78px;
579
+ }
580
+
581
+ .task-plan-summary__value {
582
+ color: var(--viewer-text);
583
+ font-size: 24px;
584
+ font-weight: 700;
585
+ letter-spacing: -0.03em;
586
+ line-height: 1;
587
+ }
588
+
589
+ .task-plan-summary__label {
590
+ color: var(--viewer-text-dim);
591
+ font-family: var(--font-mono);
592
+ font-size: 11px;
593
+ letter-spacing: 0.08em;
594
+ text-transform: uppercase;
595
+ }
596
+
597
+ .task-plan__legend {
598
+ display: flex;
599
+ flex-wrap: wrap;
600
+ align-items: center;
601
+ gap: 12px;
602
+ }
603
+
604
+ .task-plan__legend-title {
605
+ color: var(--viewer-text-dim);
606
+ font-family: var(--font-mono);
607
+ font-size: 11px;
608
+ letter-spacing: 0.08em;
609
+ text-transform: uppercase;
610
+ }
611
+
612
+ .task-plan__legend-item {
613
+ display: inline-flex;
614
+ align-items: center;
615
+ gap: 8px;
616
+ padding: 8px 12px;
617
+ border: 1px solid var(--viewer-border);
618
+ border-radius: var(--radius-pill);
619
+ background: rgba(148, 163, 184, 0.08);
620
+ color: var(--viewer-text-muted);
621
+ font-size: 12px;
622
+ line-height: 1;
623
+ }
624
+
625
+ .task-plan__legend-dot {
626
+ width: 8px;
627
+ height: 8px;
628
+ border-radius: 999px;
629
+ flex-shrink: 0;
630
+ }
631
+
632
+ .task-plan__legend-icon {
633
+ opacity: 0.95;
634
+ }
635
+
636
+ .task-plan__empty {
637
+ display: grid;
178
638
  place-items: center;
179
- padding: 2rem;
639
+ gap: 8px;
640
+ min-height: 320px;
641
+ padding: 48px 24px;
642
+ border: 1px dashed rgba(148, 163, 184, 0.3);
643
+ border-radius: var(--radius-xl);
644
+ background: var(--viewer-empty-bg);
645
+ color: var(--viewer-text-muted);
180
646
  text-align: center;
181
- color: var(--dt-text-secondary);
182
647
  }
183
648
 
184
- .empty-state.is-visible {
185
- display: grid;
649
+ .task-plan__empty strong {
650
+ color: var(--viewer-text);
651
+ font-size: 16px;
186
652
  }
187
653
 
188
- .empty-state strong {
189
- display: block;
190
- margin-bottom: 0.4rem;
191
- color: var(--dt-text-primary);
192
- font-size: 1rem;
654
+ .task-plan__empty span {
655
+ max-width: 56ch;
656
+ font-size: 14px;
657
+ line-height: 1.6;
658
+ }
659
+
660
+ .task-plan__export-error {
661
+ position: fixed;
662
+ left: 50%;
663
+ bottom: 24px;
664
+ z-index: 20;
665
+ transform: translateX(-50%);
666
+ max-width: min(92vw, 560px);
667
+ padding: 12px 16px;
668
+ border: 1px solid var(--viewer-toast-border);
669
+ border-radius: var(--radius-pill);
670
+ background: var(--viewer-toast-bg);
671
+ color: var(--viewer-text);
672
+ font-size: 13px;
673
+ line-height: 1.4;
674
+ box-shadow: 0 18px 32px rgba(15, 23, 42, 0.22);
193
675
  }
194
676
 
195
- .badge {
677
+ .viewer-badge {
196
678
  position: fixed;
197
- right: 1rem;
198
- bottom: 1rem;
199
- z-index: 3;
679
+ right: 16px;
680
+ bottom: 16px;
681
+ z-index: 12;
200
682
  display: inline-flex;
201
683
  align-items: center;
202
- gap: 0.55rem;
203
- padding: 0.55rem 0.8rem;
204
- border: 1px solid var(--dt-border);
205
- border-radius: 999px;
206
- background: var(--dt-badge-bg);
207
- color: var(--dt-text-secondary);
684
+ gap: 10px;
685
+ padding: 10px 14px;
686
+ border: 1px solid var(--viewer-border);
687
+ border-radius: var(--radius-pill);
688
+ background: var(--viewer-badge-bg);
689
+ color: var(--viewer-text-dim);
208
690
  backdrop-filter: blur(14px);
209
- font-size: 0.74rem;
210
- letter-spacing: 0.04em;
691
+ font-family: var(--font-mono);
692
+ font-size: 11px;
693
+ letter-spacing: 0.08em;
211
694
  text-transform: uppercase;
212
695
  }
213
696
 
214
- .badge-mark {
215
- width: 0.55rem;
216
- height: 0.55rem;
697
+ .viewer-badge__mark {
698
+ width: 9px;
699
+ height: 9px;
217
700
  border-radius: 999px;
218
- background: var(--tp-indigo);
219
- box-shadow: 0 0 0 0.18rem rgba(99, 102, 241, 0.18);
701
+ background: var(--tp-phase-2);
702
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.18);
703
+ }
704
+
705
+ [hidden] {
706
+ display: none !important;
707
+ }
708
+
709
+ @keyframes task-plan-pulse {
710
+ 0%,
711
+ 100% {
712
+ opacity: 1;
713
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--tp-status-in-progress), transparent 58%);
714
+ }
715
+
716
+ 50% {
717
+ opacity: 0.55;
718
+ box-shadow: 0 0 0 8px color-mix(in srgb, var(--tp-status-in-progress), transparent 100%);
719
+ }
720
+ }
721
+
722
+ @media (max-width: 960px) {
723
+ .viewer-toolbar {
724
+ flex-direction: column;
725
+ }
726
+
727
+ .viewer-actions {
728
+ justify-content: flex-start;
729
+ }
730
+
731
+ .viewer-stage {
732
+ padding: var(--space-4);
733
+ }
220
734
  }
221
735
 
222
736
  @media (max-width: 720px) {
223
- .toolbar {
737
+ body {
738
+ overflow: auto;
739
+ }
740
+
741
+ .viewer-toolbar {
742
+ padding: var(--space-4);
743
+ }
744
+
745
+ .viewer-title h1 {
746
+ font-size: 22px;
747
+ }
748
+
749
+ .task-plan__surface {
750
+ min-height: 0;
751
+ padding: 16px;
752
+ }
753
+
754
+ .task-plan-phase__header,
755
+ .task-plan-task__topline {
224
756
  align-items: flex-start;
225
757
  flex-direction: column;
226
758
  }
227
759
 
228
- .actions {
229
- width: 100%;
230
- justify-content: flex-start;
760
+ .task-plan-phase__badge,
761
+ .task-plan-task__status {
762
+ white-space: normal;
763
+ }
764
+
765
+ .task-plan__summary {
766
+ gap: 16px;
231
767
  }
232
768
 
233
- .stage {
234
- padding: 0.75rem;
769
+ .viewer-badge {
770
+ bottom: 12px;
771
+ right: 12px;
772
+ padding: 9px 12px;
235
773
  }
774
+ }
236
775
 
237
- svg {
238
- min-height: 500px;
776
+ @media (prefers-reduced-motion: reduce) {
777
+ *,
778
+ *::before,
779
+ *::after {
780
+ animation: none !important;
781
+ transition: none !important;
782
+ scroll-behavior: auto !important;
239
783
  }
240
784
  }
241
785
  </style>
242
786
  </head>
243
787
  <body>
244
- <div class="shell">
245
- <header class="toolbar">
246
- <div class="title">
247
- <h1>Task Execution Plan</h1>
248
- <p>Phase-based plan with dependency tracking.</p>
788
+ <div class="viewer-shell">
789
+ <header class="viewer-toolbar">
790
+ <div class="viewer-title">
791
+ <span class="viewer-title__eyebrow">Task Plan Viewer</span>
792
+ <h1 id="viewer-title">Task Plan</h1>
793
+ <p id="viewer-description">Phase-based pipeline for agent work, dependencies, and delivery state.</p>
249
794
  </div>
250
- <div class="actions">
795
+ <div class="viewer-actions" role="toolbar" aria-label="Task plan viewer actions">
251
796
  <button id="theme-button" type="button">Theme: Auto</button>
252
- <button id="export-button" type="button">Export SVG</button>
797
+ <button id="export-png-button" type="button">Export PNG</button>
798
+ <button id="export-svg-button" type="button">Export SVG</button>
253
799
  </div>
254
800
  </header>
255
- <main class="stage">
256
- <div class="canvas-shell">
257
- <div class="empty-state" id="empty-state" aria-live="polite"></div>
258
- <svg id="diagram" role="img" aria-labelledby="diagram-title diagram-description"></svg>
801
+
802
+ <main class="viewer-stage">
803
+ <div class="task-plan__shell">
804
+ <div class="task-plan__surface" id="pipeline-surface">
805
+ <div class="task-plan__pipeline" id="pipeline" aria-label="Task plan pipeline"></div>
806
+
807
+ <section class="task-plan__summary" id="summary" aria-label="Task plan summary" hidden></section>
808
+
809
+ <section class="task-plan__legend" id="legend" aria-label="Agent legend" hidden></section>
810
+
811
+ <div class="task-plan__empty" id="empty-state" role="status" aria-live="polite" hidden>
812
+ <strong>No tasks to render</strong>
813
+ <span>Add phases, batches, and tasks to populate the plan.</span>
814
+ </div>
815
+ </div>
259
816
  </div>
260
817
  </main>
261
818
  </div>
262
- <div class="badge"><span class="badge-mark"></span>AI Kit static viewer</div>
819
+
820
+ <div class="viewer-badge"><span class="viewer-badge__mark"></span>AI Kit static viewer</div>
821
+ <div class="task-plan__export-error" id="export-error" role="status" aria-live="polite" hidden></div>
822
+
263
823
  <script type="application/json" id="diagram-data">{}</script>
264
824
  <script>
265
825
  (() => {
266
- const svgNs = 'http://www.w3.org/2000/svg';
267
826
  const root = document.documentElement;
268
- const svg = document.getElementById('diagram');
269
- const emptyState = document.getElementById('empty-state');
270
- const exportButton = document.getElementById('export-button');
827
+ const colorSchemeQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
828
+
829
+ const pipelineEl = document.getElementById('pipeline');
830
+ const summaryEl = document.getElementById('summary');
831
+ const legendEl = document.getElementById('legend');
832
+ const emptyStateEl = document.getElementById('empty-state');
833
+ const exportErrorEl = document.getElementById('export-error');
834
+ const surfaceEl = document.getElementById('pipeline-surface');
835
+ const viewerTitleEl = document.getElementById('viewer-title');
836
+ const viewerDescriptionEl = document.getElementById('viewer-description');
271
837
  const themeButton = document.getElementById('theme-button');
838
+ const exportPngButton = document.getElementById('export-png-button');
839
+ const exportSvgButton = document.getElementById('export-svg-button');
840
+ const styleTag = document.getElementById('viewer-style');
841
+
272
842
  const themes = ['auto', 'dark', 'light'];
273
- const monoFont = '"JetBrains Mono", "SFMono-Regular", Consolas, ui-monospace, monospace';
274
- const phaseGap = 300;
275
- const batchGap = 120;
276
- const taskGap = 100;
277
- const sequentialTaskGap = 140;
278
- const taskWidth = 260;
279
- const taskHeight = 88;
280
- const phaseHeaderHeight = 72;
281
- const canvasPadding = 80;
282
- const phasePaddingX = 36;
283
- const phasePaddingBottom = 44;
284
- const batchLabelHeight = 32;
285
- const phasePalette = [
286
- '--dt-cyan',
287
- '--tp-indigo',
288
- '--dt-amber',
289
- '--tp-pink',
290
- '--tp-teal',
291
- '--dt-violet',
292
- ];
293
-
294
- function node(tag, attributes, text) {
295
- const element = document.createElementNS(svgNs, tag);
296
- if (attributes) {
297
- Object.entries(attributes).forEach(([name, value]) => {
298
- if (value !== undefined && value !== null) {
299
- element.setAttribute(name, String(value));
300
- }
301
- });
302
- }
303
- if (text !== undefined) {
304
- element.innerHTML = escapeHtml(text);
305
- }
306
- return element;
843
+ const phasePalette = ['#06b6d4', '#6366f1', '#f59e0b', '#ec4899', '#14b8a6', '#8b5cf6'];
844
+ const statusLabels = {
845
+ pending: 'Pending',
846
+ 'in-progress': 'In Progress',
847
+ done: 'Done',
848
+ blocked: 'Blocked'
849
+ };
850
+ const agentIcons = {
851
+ researcher: '🔬',
852
+ implementer: '🛠',
853
+ frontend: '🎨',
854
+ reviewer: '🧪',
855
+ debugger: '🐞',
856
+ security: '🛡',
857
+ explorer: '🧭',
858
+ documenter: '📝',
859
+ refactor: '',
860
+ planner: '🧠',
861
+ orchestrator: '🎛'
862
+ };
863
+
864
+ function getTrimmedString(value) {
865
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
307
866
  }
308
867
 
309
- function cssValue() {
310
- for (let index = 0; index < arguments.length; index += 1) {
311
- const value = getComputedStyle(root).getPropertyValue(arguments[index]).trim();
312
- if (value) {
313
- return value;
314
- }
315
- }
316
- return '';
868
+ function isTaskStatus(value) {
869
+ return value === 'pending' || value === 'in-progress' || value === 'done' || value === 'blocked';
317
870
  }
318
871
 
319
- function rgba(color, alpha) {
320
- const cleaned = String(color || '').replace('#', '').trim();
321
- const value = cleaned.length === 3
322
- ? cleaned.split('').map((char) => char + char).join('')
323
- : cleaned;
324
- const number = Number.parseInt(value, 16);
325
- if (Number.isNaN(number)) {
326
- return color;
872
+ function classifyAgent(agent) {
873
+ if (!agent) return 'orchestrator';
874
+ if (agent.startsWith('Researcher-')) return 'researcher';
875
+ if (agent.startsWith('Code-Reviewer-') || agent.startsWith('Architect-Reviewer-')) return 'reviewer';
876
+ switch (agent) {
877
+ case 'Implementer': return 'implementer';
878
+ case 'Frontend': return 'frontend';
879
+ case 'Refactor': return 'refactor';
880
+ case 'Debugger': return 'debugger';
881
+ case 'Security': return 'security';
882
+ case 'Explorer': return 'explorer';
883
+ case 'Documenter': return 'documenter';
884
+ case 'Planner': return 'planner';
885
+ default: return 'orchestrator';
327
886
  }
328
- const red = (number >> 16) & 255;
329
- const green = (number >> 8) & 255;
330
- const blue = number & 255;
331
- return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
332
887
  }
333
888
 
334
- function wrapText(value, maxChars, maxLines) {
335
- const words = String(value || '').trim().split(/\s+/).filter(Boolean);
336
- if (!words.length) {
337
- return [];
338
- }
339
- const lines = [];
340
- let current = '';
341
- for (const word of words) {
342
- const next = current ? current + ' ' + word : word;
343
- if (next.length > maxChars && current) {
344
- lines.push(current);
345
- current = word;
346
- } else {
347
- current = next;
348
- }
889
+ function getAgentIcon(agent) {
890
+ return agentIcons[classifyAgent(agent)] || agentIcons.orchestrator;
891
+ }
892
+
893
+ function getAgentColor(agent) {
894
+ return 'var(--tp-agent-' + classifyAgent(agent) + ')';
895
+ }
896
+
897
+ function clearElement(element) {
898
+ while (element.firstChild) {
899
+ element.removeChild(element.firstChild);
349
900
  }
350
- if (current) {
351
- lines.push(current);
901
+ }
902
+
903
+ function createElement(tagName, className, text) {
904
+ const element = document.createElement(tagName);
905
+ if (className) {
906
+ element.className = className;
352
907
  }
353
- if (lines.length <= maxLines) {
354
- return lines;
908
+ if (text !== undefined) {
909
+ element.textContent = text;
355
910
  }
356
- const clipped = lines.slice(0, maxLines);
357
- clipped[maxLines - 1] = clipped[maxLines - 1].replace(/[\s.]+$/, '') + '...';
358
- return clipped;
911
+ return element;
359
912
  }
360
913
 
361
- function escapeHtml(value) {
362
- return String(value)
363
- .replace(/&/g, '&amp;')
364
- .replace(/</g, '&lt;')
365
- .replace(/>/g, '&gt;')
366
- .replace(/"/g, '&quot;')
367
- .replace(/'/g, '&#39;');
914
+ function normalizeTask(task, taskIndex) {
915
+ const id = getTrimmedString(task && task.id) || 'task-' + (taskIndex + 1);
916
+ const files = Array.isArray(task && task.files)
917
+ ? task.files.filter((file) => typeof file === 'string' && file.trim().length > 0)
918
+ : [];
919
+ const dependsOn = Array.isArray(task && task.dependsOn)
920
+ ? Array.from(new Set(task.dependsOn.filter((dependency) => typeof dependency === 'string' && dependency.trim().length > 0)))
921
+ .filter((dependency) => dependency !== id)
922
+ : [];
923
+
924
+ return {
925
+ id,
926
+ title: getTrimmedString(task && task.title) || 'Task ' + (taskIndex + 1),
927
+ agent: getTrimmedString(task && task.agent),
928
+ files,
929
+ status: isTaskStatus(task && task.status) ? task.status : 'pending',
930
+ description: getTrimmedString(task && task.description),
931
+ dependsOn
932
+ };
368
933
  }
369
934
 
370
- function showEmpty(title, message) {
371
- emptyState.classList.add('is-visible');
372
- emptyState.innerHTML = '<div><strong>' + escapeHtml(title) + '</strong><span>' + escapeHtml(message) + '</span></div>';
373
- svg.innerHTML = '';
935
+ function normalizeBatch(batch, batchIndex) {
936
+ const order = typeof (batch && batch.order) === 'number' && Number.isFinite(batch.order)
937
+ ? batch.order
938
+ : batchIndex + 1;
939
+
940
+ return {
941
+ id: getTrimmedString(batch && batch.id) || 'batch-' + (batchIndex + 1),
942
+ order,
943
+ parallel: Boolean(batch && batch.parallel),
944
+ label: getTrimmedString(batch && batch.label),
945
+ tasks: Array.isArray(batch && batch.tasks)
946
+ ? batch.tasks.map((task, taskIndex) => normalizeTask(task, taskIndex))
947
+ : []
948
+ };
374
949
  }
375
950
 
376
- function hideEmpty() {
377
- emptyState.classList.remove('is-visible');
378
- emptyState.textContent = '';
951
+ function normalizePhase(phase, phaseIndex) {
952
+ const batches = Array.isArray(phase && phase.batches)
953
+ ? phase.batches.map((batch, batchIndex) => normalizeBatch(batch, batchIndex)).sort((left, right) => left.order - right.order)
954
+ : [];
955
+
956
+ return {
957
+ id: getTrimmedString(phase && phase.id) || 'phase-' + (phaseIndex + 1),
958
+ label: getTrimmedString(phase && phase.label) || 'Phase ' + (phaseIndex + 1),
959
+ outcome: getTrimmedString(phase && phase.outcome),
960
+ batches
961
+ };
962
+ }
963
+
964
+ function normalizeData(raw) {
965
+ return {
966
+ title: getTrimmedString(raw && raw.title),
967
+ description: getTrimmedString(raw && raw.description),
968
+ phases: Array.isArray(raw && raw.phases)
969
+ ? raw.phases.map((phase, phaseIndex) => normalizePhase(phase, phaseIndex))
970
+ : []
971
+ };
379
972
  }
380
973
 
381
- function parseData() {
974
+ function readData() {
382
975
  const source = document.getElementById('diagram-data');
383
- if (!source) {
384
- return {};
976
+ if (!source || !source.textContent) {
977
+ return normalizeData(undefined);
385
978
  }
979
+
386
980
  try {
387
- return JSON.parse(source.textContent || '{}');
981
+ return normalizeData(JSON.parse(source.textContent));
388
982
  } catch (error) {
389
- showEmpty('Invalid diagram JSON', String(error && error.message ? error.message : error));
983
+ showEmpty('Invalid task plan JSON', error instanceof Error ? error.message : String(error));
390
984
  return null;
391
985
  }
392
986
  }
393
987
 
394
- function phaseColor(index) {
395
- return cssValue(phasePalette[index % phasePalette.length], '--dt-cyan');
988
+ function getTasks(plan) {
989
+ return plan.phases.flatMap((phase) => phase.batches.flatMap((batch) => batch.tasks));
396
990
  }
397
991
 
398
- function agentColor(agent) {
399
- if (!agent) return cssValue('--dt-slate');
400
- const value = agent.toLowerCase();
401
- if (value.includes('researcher')) return cssValue('--tp-indigo');
402
- if (value.includes('implementer')) return cssValue('--dt-emerald');
403
- if (value.includes('frontend')) return cssValue('--tp-pink');
404
- if (value.includes('reviewer') || value.includes('architect')) return cssValue('--dt-amber');
405
- if (value.includes('debugger')) return cssValue('--tp-red', '--dt-rose');
406
- if (value.includes('security')) return cssValue('--tp-orange');
407
- if (value.includes('explorer')) return cssValue('--dt-cyan');
408
- if (value.includes('documenter')) return cssValue('--dt-violet');
409
- if (value.includes('refactor')) return cssValue('--tp-teal');
410
- if (value.includes('planner')) return cssValue('--tp-purple');
411
- if (value.includes('orchestrator')) return cssValue('--dt-slate');
412
- return cssValue('--dt-slate');
992
+ function getPhaseTaskCount(phase) {
993
+ return phase.batches.reduce((count, batch) => count + batch.tasks.length, 0);
413
994
  }
414
995
 
415
- function agentToken(agent) {
416
- const value = String(agent || '').trim();
417
- if (!value) {
418
- return '?';
419
- }
420
- const cleaned = value.replace(/[^A-Za-z0-9\s-]/g, ' ').trim();
421
- const parts = cleaned.split(/[\s-]+/).filter(Boolean);
422
- if (parts.length === 1) {
423
- return parts[0].slice(0, 2).toUpperCase();
996
+ function getPhaseBadge(phase) {
997
+ const taskCount = getPhaseTaskCount(phase);
998
+ if (phase.batches.length <= 1) {
999
+ return taskCount + ' ' + (taskCount === 1 ? 'task' : 'tasks');
424
1000
  }
425
- return (parts[0][0] + parts[1][0]).toUpperCase();
1001
+ return taskCount + ' ' + (taskCount === 1 ? 'task' : 'tasks') + ' · ' + phase.batches.length + ' batches';
426
1002
  }
427
1003
 
428
- function statusMeta(status) {
429
- const value = String(status || 'pending').toLowerCase();
430
- if (value === 'done') {
431
- return { key: 'done', color: cssValue('--tp-green'), label: 'Done' };
432
- }
433
- if (value === 'in-progress') {
434
- return { key: 'in-progress', color: cssValue('--tp-blue'), label: 'In progress' };
1004
+ function getBatchDescriptor(batch) {
1005
+ const mode = batch.parallel ? 'Parallel' : 'Sequential';
1006
+ if (batch.label) {
1007
+ return batch.label + ' - ' + mode;
435
1008
  }
436
- if (value === 'blocked') {
437
- return { key: 'blocked', color: cssValue('--tp-red'), label: 'Blocked' };
438
- }
439
- return { key: 'pending', color: cssValue('--tp-gray', '--dt-slate'), label: 'Pending' };
1009
+ return 'Batch ' + batch.order + ' - ' + mode;
440
1010
  }
441
1011
 
442
- function normalizeTask(task, phase, batch, index) {
443
- return {
444
- id: typeof task.id === 'string' ? task.id : phase.id + '-' + batch.id + '-task-' + index,
445
- title: typeof task.title === 'string' ? task.title : 'Untitled task',
446
- agent: typeof task.agent === 'string' ? task.agent : '',
447
- files: Array.isArray(task.files) ? task.files.filter((file) => typeof file === 'string') : [],
448
- status: typeof task.status === 'string' ? task.status : 'pending',
449
- dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.filter((id) => typeof id === 'string') : [],
450
- phaseId: phase.id,
451
- batchId: batch.id,
452
- };
1012
+ function getBatchIcon(batch) {
1013
+ return batch.parallel ? '⫸' : '→';
453
1014
  }
454
1015
 
455
- function buildModel(raw) {
456
- const phases = Array.isArray(raw && raw.phases) ? raw.phases : [];
457
- const normalized = [];
458
- const taskMap = new Map();
459
- const edges = [];
460
- const edgeKeys = new Set();
1016
+ function getFileCountLabel(fileCount) {
1017
+ return fileCount + ' ' + (fileCount === 1 ? 'file' : 'files');
1018
+ }
461
1019
 
462
- phases.forEach((phase, phaseIndex) => {
463
- if (!phase || typeof phase !== 'object') {
464
- return;
465
- }
466
- const phaseValue = {
467
- id: typeof phase.id === 'string' ? phase.id : 'phase-' + (phaseIndex + 1),
468
- label: typeof phase.label === 'string' ? phase.label : 'Phase ' + (phaseIndex + 1),
469
- outcome: typeof phase.outcome === 'string' ? phase.outcome : '',
470
- batches: [],
471
- };
472
-
473
- const batches = Array.isArray(phase.batches) ? phase.batches : [];
474
- batches.forEach((batch, batchIndex) => {
475
- if (!batch || typeof batch !== 'object') {
476
- return;
477
- }
478
- const batchValue = {
479
- id: typeof batch.id === 'string' ? batch.id : phaseValue.id + '-batch-' + (batchIndex + 1),
480
- order: typeof batch.order === 'number' ? batch.order : batchIndex + 1,
481
- parallel: Boolean(batch.parallel),
482
- label: typeof batch.label === 'string' ? batch.label : 'Batch ' + (batchIndex + 1),
483
- tasks: [],
484
- };
485
-
486
- const tasks = Array.isArray(batch.tasks) ? batch.tasks : [];
487
- tasks.forEach((task, taskIndex) => {
488
- if (!task || typeof task !== 'object') {
489
- return;
490
- }
491
- const normalizedTask = normalizeTask(task, phaseValue, batchValue, taskIndex + 1);
492
- batchValue.tasks.push(normalizedTask);
493
- taskMap.set(normalizedTask.id, normalizedTask);
494
- });
1020
+ function getDependencyLabels(task) {
1021
+ const dependencies = task.dependsOn || [];
1022
+ if (dependencies.length <= 3) {
1023
+ return dependencies.map((dependency) => 'Depends: ' + dependency);
1024
+ }
1025
+ return dependencies.slice(0, 2).map((dependency) => 'Depends: ' + dependency).concat(['+' + (dependencies.length - 2) + ' more']);
1026
+ }
495
1027
 
496
- if (batchValue.tasks.length) {
497
- phaseValue.batches.push(batchValue);
1028
+ function buildLegendItems(tasks) {
1029
+ const seen = new Set();
1030
+ return tasks
1031
+ .filter((task) => Boolean(task.agent))
1032
+ .filter((task) => {
1033
+ if (seen.has(task.agent)) {
1034
+ return false;
498
1035
  }
499
- });
1036
+ seen.add(task.agent);
1037
+ return true;
1038
+ })
1039
+ .map((task) => ({
1040
+ label: task.agent,
1041
+ color: getAgentColor(task.agent),
1042
+ icon: getAgentIcon(task.agent)
1043
+ }));
1044
+ }
500
1045
 
501
- if (phaseValue.batches.length) {
502
- normalized.push(phaseValue);
503
- }
504
- });
1046
+ function getSummary(plan) {
1047
+ const tasks = getTasks(plan);
1048
+ const counts = {
1049
+ pending: 0,
1050
+ 'in-progress': 0,
1051
+ done: 0,
1052
+ blocked: 0
1053
+ };
1054
+ const agents = new Set();
505
1055
 
506
- normalized.forEach((phase) => {
507
- phase.batches.forEach((batch) => {
508
- batch.tasks.forEach((task) => {
509
- task.dependsOn.forEach((dependencyId) => {
510
- if (!taskMap.has(dependencyId)) {
511
- return;
512
- }
513
- const key = dependencyId + '->' + task.id;
514
- if (edgeKeys.has(key)) {
515
- return;
516
- }
517
- edgeKeys.add(key);
518
- edges.push({ source: dependencyId, target: task.id });
519
- });
520
- });
521
- });
1056
+ tasks.forEach((task) => {
1057
+ counts[task.status] += 1;
1058
+ if (task.agent) {
1059
+ agents.add(task.agent);
1060
+ }
522
1061
  });
523
1062
 
524
1063
  return {
525
- title: typeof raw.title === 'string' ? raw.title : 'Task execution plan',
526
- description: typeof raw.description === 'string' ? raw.description : '',
527
- phases: normalized,
528
- taskMap,
529
- edges,
1064
+ totalTasks: tasks.length,
1065
+ totalPhases: plan.phases.length,
1066
+ totalAgents: agents.size,
1067
+ statusCounts: counts
530
1068
  };
531
1069
  }
532
1070
 
533
- function measureTask(task) {
534
- const titleLines = wrapText(task.title || task.id, 28, 2);
535
- const agentLines = wrapText(task.agent || 'Unassigned', 24, 1);
536
- const height = taskHeight + Math.max(0, titleLines.length - 1) * 20;
537
- return {
538
- width: taskWidth,
539
- height,
540
- titleLines,
541
- agentLines,
542
- };
1071
+ function getThemeMode() {
1072
+ return root.getAttribute('data-theme') || 'auto';
543
1073
  }
544
1074
 
545
- function layoutBatch(batch, startX, startY) {
546
- const tasks = batch.tasks.map((task) => ({ task, size: measureTask(task) }));
547
- const boxes = [];
548
- const contentY = startY + batchLabelHeight + 8;
549
-
550
- if (batch.parallel) {
551
- let cursorY = contentY;
552
- let width = taskWidth;
553
- tasks.forEach(({ task, size }) => {
554
- boxes.push({ task, x: startX, y: cursorY, width: size.width, height: size.height, size });
555
- width = Math.max(width, size.width);
556
- cursorY += size.height + taskGap;
557
- });
558
- return {
559
- boxes,
560
- width,
561
- height: Math.max(batchLabelHeight + 8, cursorY - startY - taskGap),
562
- };
1075
+ function getResolvedTheme() {
1076
+ const mode = getThemeMode();
1077
+ if (mode === 'light' || mode === 'dark') {
1078
+ return mode;
563
1079
  }
564
-
565
- let cursorX = startX;
566
- let maxHeight = 0;
567
- tasks.forEach(({ task, size }) => {
568
- boxes.push({ task, x: cursorX, y: contentY, width: size.width, height: size.height, size });
569
- cursorX += size.width + sequentialTaskGap;
570
- maxHeight = Math.max(maxHeight, size.height);
571
- });
572
- return {
573
- boxes,
574
- width: Math.max(taskWidth, cursorX - startX - sequentialTaskGap),
575
- height: batchLabelHeight + 8 + maxHeight,
576
- };
1080
+ return colorSchemeQuery && colorSchemeQuery.matches ? 'light' : 'dark';
577
1081
  }
578
1082
 
579
- function layout(model) {
580
- const phaseLayouts = [];
581
- const taskBoxes = new Map();
582
- let currentY = canvasPadding;
583
- let maxWidth = 0;
584
-
585
- model.phases.forEach((phase, phaseIndex) => {
586
- const phaseX = canvasPadding;
587
- const phaseY = currentY;
588
- let batchCursorX = phaseX + phasePaddingX;
589
- const batchBaseY = phaseY + phaseHeaderHeight + 18;
590
- let rightEdge = phaseX + 320;
591
- let maxBatchBottom = batchBaseY;
592
- const batchLayouts = [];
593
-
594
- phase.batches.forEach((batch) => {
595
- const batchLayout = layoutBatch(batch, batchCursorX, batchBaseY);
596
- batchLayout.boxes.forEach((box) => {
597
- taskBoxes.set(box.task.id, box);
598
- });
599
- batchLayouts.push({
600
- batch,
601
- x: batchCursorX,
602
- y: batchBaseY,
603
- width: batchLayout.width,
604
- height: batchLayout.height,
605
- boxes: batchLayout.boxes,
606
- });
607
- rightEdge = Math.max(rightEdge, batchCursorX + batchLayout.width + phasePaddingX);
608
- maxBatchBottom = Math.max(maxBatchBottom, batchBaseY + batchLayout.height);
609
- batchCursorX += Math.max(taskWidth, batchLayout.width) + batchGap;
610
- });
1083
+ function updateThemeButton() {
1084
+ const mode = getThemeMode();
1085
+ themeButton.textContent = 'Theme: ' + mode.charAt(0).toUpperCase() + mode.slice(1);
1086
+ }
611
1087
 
612
- const phaseWidth = Math.max(600, rightEdge - phaseX);
613
- const phaseHeight = Math.max(
614
- phaseHeaderHeight + 120,
615
- maxBatchBottom - phaseY + phasePaddingBottom,
616
- );
617
-
618
- phaseLayouts.push({
619
- phase,
620
- x: phaseX,
621
- y: phaseY,
622
- width: phaseWidth,
623
- height: phaseHeight,
624
- color: phaseColor(phaseIndex),
625
- batches: batchLayouts,
626
- });
1088
+ function cycleTheme() {
1089
+ const current = getThemeMode();
1090
+ const next = themes[(themes.indexOf(current) + 1) % themes.length];
1091
+ root.setAttribute('data-theme', next);
1092
+ updateThemeButton();
1093
+ }
627
1094
 
628
- maxWidth = Math.max(maxWidth, phaseX + phaseWidth + canvasPadding);
629
- currentY += phaseHeight + phaseGap;
630
- });
1095
+ let exportErrorTimer = null;
631
1096
 
632
- return {
633
- phases: phaseLayouts,
634
- taskBoxes,
635
- width: Math.max(1200, maxWidth),
636
- height: Math.max(600, currentY - phaseGap + canvasPadding),
637
- };
1097
+ function showExportError(message) {
1098
+ exportErrorEl.textContent = message;
1099
+ exportErrorEl.hidden = false;
1100
+ if (exportErrorTimer) {
1101
+ window.clearTimeout(exportErrorTimer);
1102
+ }
1103
+ exportErrorTimer = window.setTimeout(() => {
1104
+ exportErrorEl.hidden = true;
1105
+ }, 4500);
638
1106
  }
639
1107
 
640
- function routeEdge(edge, source, target) {
641
- const startX = source.x + source.width;
642
- const startY = source.y + source.height / 2;
643
- const endX = target.x;
644
- const endY = target.y + target.height / 2;
645
- const deltaX = endX - startX;
646
- const curve = Math.max(68, Math.abs(deltaX) * 0.38);
647
-
648
- if (deltaX >= 0) {
649
- return {
650
- d: 'M ' + startX + ' ' + startY + ' C ' + (startX + curve) + ' ' + startY + ', ' + (endX - curve) + ' ' + endY + ', ' + endX + ' ' + endY,
651
- labelX: startX + deltaX / 2,
652
- labelY: Math.min(startY, endY) - 16,
653
- };
1108
+ function clearExportError() {
1109
+ exportErrorEl.hidden = true;
1110
+ exportErrorEl.textContent = '';
1111
+ if (exportErrorTimer) {
1112
+ window.clearTimeout(exportErrorTimer);
1113
+ exportErrorTimer = null;
654
1114
  }
1115
+ }
655
1116
 
656
- const midX = Math.max(startX + 72, startX + Math.abs(deltaX) * 0.35);
657
- const liftY = Math.min(startY, endY) - 56;
658
- return {
659
- d: 'M ' + startX + ' ' + startY + ' C ' + midX + ' ' + startY + ', ' + midX + ' ' + liftY + ', ' + (endX - 18) + ' ' + liftY + ' S ' + (endX - 32) + ' ' + endY + ', ' + endX + ' ' + endY,
660
- labelX: midX,
661
- labelY: liftY - 10,
662
- };
1117
+ function showEmpty(title, message) {
1118
+ clearElement(pipelineEl);
1119
+ clearElement(summaryEl);
1120
+ clearElement(legendEl);
1121
+ summaryEl.hidden = true;
1122
+ legendEl.hidden = true;
1123
+
1124
+ clearElement(emptyStateEl);
1125
+ emptyStateEl.appendChild(createElement('strong', '', title));
1126
+ emptyStateEl.appendChild(createElement('span', '', message));
1127
+ emptyStateEl.hidden = false;
663
1128
  }
664
1129
 
665
- function renderDefs() {
666
- const defs = node('defs');
667
-
668
- const marker = node('marker', {
669
- id: 'arrow',
670
- viewBox: '0 0 10 10',
671
- refX: 8,
672
- refY: 5,
673
- markerWidth: 7,
674
- markerHeight: 7,
675
- orient: 'auto-start-reverse',
676
- });
677
- marker.appendChild(node('path', {
678
- d: 'M 0 0 L 10 5 L 0 10 z',
679
- fill: cssValue('--dt-text-secondary'),
680
- }));
681
- defs.appendChild(marker);
682
-
683
- const filter = node('filter', {
684
- id: 'node-shadow',
685
- x: '-20%',
686
- y: '-20%',
687
- width: '140%',
688
- height: '140%',
689
- });
690
- filter.appendChild(node('feDropShadow', {
691
- dx: 0,
692
- dy: 12,
693
- stdDeviation: 12,
694
- 'flood-color': cssValue('--dt-shadow'),
695
- }));
696
- defs.appendChild(filter);
697
-
698
- phasePalette.forEach((token, index) => {
699
- const color = cssValue(token, '--dt-cyan');
700
- const gradient = node('linearGradient', {
701
- id: 'phase-grad-' + index,
702
- x1: '0%',
703
- y1: '0%',
704
- x2: '100%',
705
- y2: '100%',
706
- });
707
- gradient.appendChild(node('stop', { offset: '0%', 'stop-color': rgba(color, 0.14) }));
708
- gradient.appendChild(node('stop', { offset: '100%', 'stop-color': rgba(color, 0.03) }));
709
- defs.appendChild(gradient);
1130
+ function hideEmpty() {
1131
+ emptyStateEl.hidden = true;
1132
+ }
1133
+
1134
+ function renderSummary(summary) {
1135
+ clearElement(summaryEl);
1136
+
1137
+ const statItems = [
1138
+ { label: 'Total Tasks', value: String(summary.totalTasks) },
1139
+ { label: 'Phases', value: String(summary.totalPhases) },
1140
+ { label: 'Pending', value: String(summary.statusCounts.pending), color: 'var(--tp-status-pending)' },
1141
+ { label: 'In Progress', value: String(summary.statusCounts['in-progress']), color: 'var(--tp-status-in-progress)' },
1142
+ { label: 'Done', value: String(summary.statusCounts.done), color: 'var(--tp-status-done)' },
1143
+ { label: 'Blocked', value: String(summary.statusCounts.blocked), color: 'var(--tp-status-blocked)' },
1144
+ { label: 'Agents', value: String(summary.totalAgents) }
1145
+ ];
1146
+
1147
+ statItems.forEach((stat) => {
1148
+ const statEl = createElement('div', 'task-plan-summary__stat');
1149
+ const valueEl = createElement('span', 'task-plan-summary__value', stat.value);
1150
+ if (stat.color) {
1151
+ valueEl.style.color = stat.color;
1152
+ }
1153
+ statEl.appendChild(valueEl);
1154
+ statEl.appendChild(createElement('span', 'task-plan-summary__label', stat.label));
1155
+ summaryEl.appendChild(statEl);
710
1156
  });
711
1157
 
712
- return defs;
1158
+ summaryEl.hidden = false;
713
1159
  }
714
1160
 
715
- function renderTextLines(lines, group, x, y, fontSize, color, fontWeight, lineHeight, anchor) {
716
- lines.forEach((line, index) => {
717
- group.appendChild(node('text', {
718
- x,
719
- y: y + index * lineHeight,
720
- fill: color,
721
- 'font-size': fontSize,
722
- 'font-weight': fontWeight,
723
- 'text-anchor': anchor || 'start',
724
- }, line));
1161
+ function renderLegend(tasks) {
1162
+ clearElement(legendEl);
1163
+ const legendItems = buildLegendItems(tasks);
1164
+
1165
+ if (legendItems.length <= 1) {
1166
+ legendEl.hidden = true;
1167
+ return;
1168
+ }
1169
+
1170
+ legendEl.appendChild(createElement('span', 'task-plan__legend-title', 'Agents'));
1171
+
1172
+ legendItems.forEach((item) => {
1173
+ const legendItemEl = createElement('span', 'task-plan__legend-item');
1174
+ const dotEl = createElement('span', 'task-plan__legend-dot');
1175
+ dotEl.style.backgroundColor = item.color;
1176
+
1177
+ const textWrapEl = createElement('span', '');
1178
+ textWrapEl.appendChild(createElement('span', 'task-plan__legend-icon', item.icon));
1179
+ textWrapEl.appendChild(document.createTextNode(' ' + item.label));
1180
+
1181
+ legendItemEl.appendChild(dotEl);
1182
+ legendItemEl.appendChild(textWrapEl);
1183
+ legendEl.appendChild(legendItemEl);
725
1184
  });
726
- }
727
1185
 
728
- function renderPhase(phaseLayout, layer, index) {
729
- const group = node('g');
730
- const gradientId = 'phase-grad-' + (index % phasePalette.length);
731
-
732
- group.appendChild(node('rect', {
733
- x: phaseLayout.x,
734
- y: phaseLayout.y,
735
- width: phaseLayout.width,
736
- height: phaseLayout.height,
737
- rx: 28,
738
- fill: 'url(#' + gradientId + ')',
739
- stroke: rgba(phaseLayout.color, 0.42),
740
- 'stroke-width': 1.6,
741
- 'stroke-dasharray': '10 10',
742
- }));
743
-
744
- group.appendChild(node('text', {
745
- x: phaseLayout.x + phasePaddingX,
746
- y: phaseLayout.y + 30,
747
- fill: phaseLayout.color,
748
- 'font-size': 10,
749
- 'font-family': monoFont,
750
- 'font-weight': 700,
751
- 'letter-spacing': '0.08em',
752
- }, 'PHASE'));
753
-
754
- group.appendChild(node('text', {
755
- x: phaseLayout.x + phasePaddingX,
756
- y: phaseLayout.y + 50,
757
- fill: cssValue('--dt-text-primary'),
758
- 'font-size': 18,
759
- 'font-weight': 700,
760
- }, phaseLayout.phase.label));
761
-
762
- const outcomeLines = wrapText(phaseLayout.phase.outcome || '', 38, 2);
763
- renderTextLines(
764
- outcomeLines,
765
- group,
766
- phaseLayout.x + phasePaddingX,
767
- phaseLayout.y + 68,
768
- 11.5,
769
- cssValue('--dt-text-secondary'),
770
- 500,
771
- 15,
772
- 'start',
773
- );
774
-
775
- layer.appendChild(group);
1186
+ legendEl.hidden = false;
776
1187
  }
777
1188
 
778
- function renderBatch(batchLayout, layer) {
779
- const label = (batchLayout.batch.parallel ? '⫘ Parallel' : '→ Sequential') + ' · ' + batchLayout.batch.label;
780
- const group = node('g');
781
-
782
- group.appendChild(node('text', {
783
- x: batchLayout.x,
784
- y: batchLayout.y + 14,
785
- fill: cssValue('--dt-text-secondary'),
786
- 'font-size': 10.5,
787
- 'font-family': monoFont,
788
- 'font-weight': 600,
789
- 'letter-spacing': '0.03em',
790
- }, label));
791
-
792
- const orderText = '#' + String(batchLayout.batch.order);
793
- const badgeWidth = Math.max(30, orderText.length * 7 + 14);
794
- group.appendChild(node('rect', {
795
- x: batchLayout.x + Math.max(120, Math.min(batchLayout.width - badgeWidth, label.length * 5.9 + 16)),
796
- y: batchLayout.y,
797
- width: badgeWidth,
798
- height: 18,
799
- rx: 9,
800
- fill: rgba(cssValue('--dt-slate'), 0.14),
801
- stroke: rgba(cssValue('--dt-slate'), 0.26),
802
- }));
803
- group.appendChild(node('text', {
804
- x: batchLayout.x + Math.max(120, Math.min(batchLayout.width - badgeWidth, label.length * 5.9 + 16)) + badgeWidth / 2,
805
- y: batchLayout.y + 12,
806
- fill: cssValue('--dt-text-secondary'),
807
- 'font-size': 10,
808
- 'font-family': monoFont,
809
- 'font-weight': 700,
810
- 'text-anchor': 'middle',
811
- }, orderText));
812
-
813
- layer.appendChild(group);
814
- }
1189
+ function createTaskCard(task) {
1190
+ const status = task.status || 'pending';
1191
+ const article = createElement('article', 'task-plan-task');
1192
+ article.dataset.status = status;
1193
+ article.style.setProperty('--card-accent', getAgentColor(task.agent));
1194
+ article.style.setProperty('--status-color', 'var(--tp-status-' + status + ')');
815
1195
 
816
- function renderStatus(group, box, meta) {
817
- const cx = box.x + 18;
818
- const cy = box.y + box.height - 18;
819
-
820
- if (meta.key === 'in-progress') {
821
- const pulse = node('circle', {
822
- cx,
823
- cy,
824
- r: 10,
825
- fill: 'none',
826
- stroke: rgba(meta.color, 0.4),
827
- 'stroke-width': 2,
828
- });
829
- pulse.appendChild(node('animate', {
830
- attributeName: 'r',
831
- values: '6;12;6',
832
- dur: '1.6s',
833
- repeatCount: 'indefinite',
834
- }));
835
- pulse.appendChild(node('animate', {
836
- attributeName: 'opacity',
837
- values: '0.9;0.2;0.9',
838
- dur: '1.6s',
839
- repeatCount: 'indefinite',
840
- }));
841
- group.appendChild(pulse);
1196
+ const topLineEl = createElement('div', 'task-plan-task__topline');
1197
+
1198
+ const agentEl = createElement('span', 'task-plan-task__agent');
1199
+ agentEl.title = task.agent || 'Orchestrator';
1200
+ agentEl.appendChild(createElement('span', 'task-plan-task__agent-icon', getAgentIcon(task.agent)));
1201
+ agentEl.appendChild(createElement('span', 'task-plan-task__agent-label', task.agent || 'Orchestrator'));
1202
+
1203
+ const statusEl = createElement('span', 'task-plan-task__status');
1204
+ statusEl.dataset.status = status;
1205
+ statusEl.appendChild(createElement('span', 'task-plan-task__status-dot'));
1206
+ statusEl.appendChild(createElement('span', '', statusLabels[status] || statusLabels.pending));
1207
+
1208
+ topLineEl.appendChild(agentEl);
1209
+ topLineEl.appendChild(statusEl);
1210
+ article.appendChild(topLineEl);
1211
+
1212
+ article.appendChild(createElement('div', 'task-plan-task__title', task.title));
1213
+
1214
+ if (task.description) {
1215
+ article.appendChild(createElement('p', 'task-plan-task__description', task.description));
842
1216
  }
843
1217
 
844
- group.appendChild(node('circle', {
845
- cx,
846
- cy,
847
- r: 6,
848
- fill: meta.color,
849
- }));
850
-
851
- if (meta.key === 'done') {
852
- group.appendChild(node('path', {
853
- d: 'M ' + (cx - 3) + ' ' + cy + ' L ' + (cx - 1) + ' ' + (cy + 3) + ' L ' + (cx + 4) + ' ' + (cy - 3),
854
- fill: 'none',
855
- stroke: '#ffffff',
856
- 'stroke-width': 1.6,
857
- 'stroke-linecap': 'round',
858
- 'stroke-linejoin': 'round',
859
- }));
1218
+ const metaEl = createElement('div', 'task-plan-task__meta');
1219
+ const fileCount = task.files ? task.files.length : 0;
1220
+ if (fileCount > 0) {
1221
+ metaEl.appendChild(createElement('span', 'task-plan-task__chip', '📁 ' + getFileCountLabel(fileCount)));
860
1222
  }
861
1223
 
862
- if (meta.key === 'blocked') {
863
- group.appendChild(node('path', {
864
- d: 'M ' + (cx - 3) + ' ' + (cy - 3) + ' L ' + (cx + 3) + ' ' + (cy + 3) + ' M ' + (cx + 3) + ' ' + (cy - 3) + ' L ' + (cx - 3) + ' ' + (cy + 3),
865
- fill: 'none',
866
- stroke: '#ffffff',
867
- 'stroke-width': 1.5,
868
- 'stroke-linecap': 'round',
869
- }));
1224
+ getDependencyLabels(task).forEach((label) => {
1225
+ const chipEl = createElement('span', 'task-plan-task__chip', '🔗 ' + label);
1226
+ chipEl.title = label;
1227
+ metaEl.appendChild(chipEl);
1228
+ });
1229
+
1230
+ if (metaEl.childNodes.length > 0) {
1231
+ article.appendChild(metaEl);
870
1232
  }
871
1233
 
872
- group.appendChild(node('text', {
873
- x: box.x + 30,
874
- y: box.y + box.height - 14,
875
- fill: cssValue('--dt-text-secondary'),
876
- 'font-size': 10.5,
877
- 'font-weight': 600,
878
- }, meta.label));
1234
+ return article;
879
1235
  }
880
1236
 
881
- function renderFileBadge(group, box, files) {
882
- if (!files.length) {
883
- return;
884
- }
885
- const label = files.length + (files.length === 1 ? ' file' : ' files');
886
- const width = Math.max(46, label.length * 6.6 + 14);
887
- const x = box.x + box.width - width - 14;
888
- const y = box.y + box.height - 27;
889
-
890
- group.appendChild(node('rect', {
891
- x,
892
- y,
893
- width,
894
- height: 18,
895
- rx: 9,
896
- fill: rgba(cssValue('--dt-cyan'), 0.12),
897
- stroke: rgba(cssValue('--dt-cyan'), 0.22),
898
- }));
899
- group.appendChild(node('text', {
900
- x: x + width / 2,
901
- y: y + 12,
902
- fill: cssValue('--dt-text-secondary'),
903
- 'font-size': 10,
904
- 'font-family': monoFont,
905
- 'font-weight': 600,
906
- 'text-anchor': 'middle',
907
- }, label));
1237
+ function renderPipeline(plan) {
1238
+ clearElement(pipelineEl);
1239
+
1240
+ plan.phases.forEach((phase, phaseIndex) => {
1241
+ const phaseBlockEl = createElement('div', 'task-plan__phase-block');
1242
+ const phaseSectionEl = createElement('section', 'task-plan-phase');
1243
+ const headingId = 'task-plan-phase-' + phase.id;
1244
+
1245
+ phaseSectionEl.setAttribute('aria-labelledby', headingId);
1246
+
1247
+ const contentEl = createElement('div', 'task-plan-phase__content');
1248
+ contentEl.style.setProperty('--phase-accent', phasePalette[phaseIndex % phasePalette.length]);
1249
+
1250
+ const headerEl = createElement('div', 'task-plan-phase__header');
1251
+ headerEl.appendChild(createElement('span', 'task-plan-phase__number', String(phaseIndex + 1)));
1252
+
1253
+ const labelEl = createElement('h2', 'task-plan-phase__label', phase.label);
1254
+ labelEl.id = headingId;
1255
+ headerEl.appendChild(labelEl);
1256
+
1257
+ headerEl.appendChild(createElement('span', 'task-plan-phase__badge', getPhaseBadge(phase)));
1258
+ contentEl.appendChild(headerEl);
1259
+
1260
+ if (phase.outcome) {
1261
+ contentEl.appendChild(createElement('p', 'task-plan-phase__outcome', phase.outcome));
1262
+ }
1263
+
1264
+ const batchesEl = createElement('div', 'task-plan-phase__batches');
1265
+ phase.batches.forEach((batch) => {
1266
+ const batchEl = createElement('section', 'task-plan-batch');
1267
+ batchEl.setAttribute('aria-label', getBatchDescriptor(batch));
1268
+
1269
+ const batchLabelEl = createElement('div', 'task-plan-batch__label');
1270
+ batchLabelEl.appendChild(createElement('span', 'task-plan-batch__icon', getBatchIcon(batch)));
1271
+ batchLabelEl.appendChild(createElement('span', '', getBatchDescriptor(batch)));
1272
+ batchEl.appendChild(batchLabelEl);
1273
+
1274
+ const tasksClass = ['task-plan-batch__tasks'];
1275
+ if (batch.parallel) {
1276
+ tasksClass.push('task-plan-batch__tasks--parallel');
1277
+ } else if (batch.tasks.length <= 2) {
1278
+ tasksClass.push('task-plan-batch__tasks--sequential-compact');
1279
+ }
1280
+
1281
+ const tasksEl = createElement('div', tasksClass.join(' '));
1282
+ batch.tasks.forEach((task) => {
1283
+ tasksEl.appendChild(createTaskCard(task));
1284
+ });
1285
+ batchEl.appendChild(tasksEl);
1286
+ batchesEl.appendChild(batchEl);
1287
+ });
1288
+
1289
+ contentEl.appendChild(batchesEl);
1290
+ phaseSectionEl.appendChild(contentEl);
1291
+ phaseBlockEl.appendChild(phaseSectionEl);
1292
+
1293
+ if (phaseIndex < plan.phases.length - 1) {
1294
+ phaseBlockEl.appendChild(createElement('div', 'task-plan__connector'));
1295
+ }
1296
+
1297
+ pipelineEl.appendChild(phaseBlockEl);
1298
+ });
1299
+ }
1300
+
1301
+ function updateHeader(plan) {
1302
+ const title = plan.title || 'Task Plan';
1303
+ const description = plan.description || 'Phase-based pipeline for agent work, dependencies, and delivery state.';
1304
+
1305
+ viewerTitleEl.textContent = title;
1306
+ viewerDescriptionEl.textContent = description;
1307
+ document.title = 'AI Kit - ' + title;
908
1308
  }
909
1309
 
910
- function renderTask(box, layer) {
911
- const task = box.task;
912
- const accent = agentColor(task.agent);
913
- const meta = statusMeta(task.status);
914
- const group = node('g');
915
- const titleX = box.x + 44;
916
- const token = agentToken(task.agent);
917
-
918
- group.appendChild(node('rect', {
919
- x: box.x,
920
- y: box.y,
921
- width: box.width,
922
- height: box.height,
923
- rx: 18,
924
- fill: rgba(accent, 0.08),
925
- stroke: rgba(accent, 0.28),
926
- 'stroke-width': 1.25,
927
- filter: 'url(#node-shadow)',
928
- }));
929
-
930
- group.appendChild(node('rect', {
931
- x: box.x + 1,
932
- y: box.y + 1,
933
- width: box.width - 2,
934
- height: box.height - 2,
935
- rx: 17,
936
- fill: cssValue('--dt-node-fill'),
937
- }));
938
-
939
- group.appendChild(node('rect', {
940
- x: box.x,
941
- y: box.y,
942
- width: 4,
943
- height: box.height,
944
- rx: 4,
945
- fill: accent,
946
- }));
947
-
948
- group.appendChild(node('circle', {
949
- cx: box.x + 22,
950
- cy: box.y + 19,
951
- r: 11,
952
- fill: rgba(accent, 0.18),
953
- stroke: rgba(accent, 0.4),
954
- }));
955
- group.appendChild(node('text', {
956
- x: box.x + 22,
957
- y: box.y + 23,
958
- fill: accent,
959
- 'font-size': 10,
960
- 'font-family': monoFont,
961
- 'font-weight': 700,
962
- 'text-anchor': 'middle',
963
- }, token));
964
-
965
- renderTextLines(box.size.agentLines, group, titleX, box.y + 21, 10.5, accent, 700, 14, 'start');
966
- renderTextLines(box.size.titleLines, group, titleX, box.y + 40, 13.5, cssValue('--dt-text-primary'), 700, 17, 'start');
967
-
968
- renderStatus(group, box, meta);
969
- renderFileBadge(group, box, task.files);
970
-
971
- layer.appendChild(group);
1310
+ function buildExportSvgMarkup() {
1311
+ const resolvedTheme = getResolvedTheme();
1312
+ const exportClone = surfaceEl.cloneNode(true);
1313
+ const padding = 24;
1314
+ const contentWidth = Math.ceil(surfaceEl.scrollWidth);
1315
+ const contentHeight = Math.ceil(surfaceEl.scrollHeight);
1316
+ const width = contentWidth + padding * 2;
1317
+ const height = contentHeight + padding * 2;
1318
+
1319
+ exportClone.removeAttribute('id');
1320
+ exportClone.style.width = contentWidth + 'px';
1321
+ exportClone.style.minHeight = '0';
1322
+ exportClone.style.boxShadow = 'none';
1323
+
1324
+ let stylesheet = styleTag && styleTag.textContent ? styleTag.textContent : '';
1325
+ stylesheet = stylesheet.replace(/:root\b/g, '.tp-export-root');
1326
+ stylesheet = stylesheet.replace(/\]\]>/g, ']]&gt;');
1327
+ stylesheet += '\n.tp-export-root{width:' + contentWidth + 'px;padding:' + padding + 'px;background:transparent;font-family:var(--font-sans);color:var(--viewer-text);}\n';
1328
+ stylesheet += '.tp-export-root .task-plan__surface{min-height:0;box-shadow:none;}\n';
1329
+
1330
+ const serializedSurface = exportClone.outerHTML;
1331
+ const svg = [
1332
+ '<?xml version="1.0" encoding="UTF-8"?>',
1333
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + ' ' + height + '">',
1334
+ '<foreignObject width="100%" height="100%">',
1335
+ '<div xmlns="http://www.w3.org/1999/xhtml" class="tp-export-root" data-theme="' + resolvedTheme + '">',
1336
+ '<style><![CDATA[' + stylesheet + ']]></style>',
1337
+ serializedSurface,
1338
+ '</div>',
1339
+ '</foreignObject>',
1340
+ '</svg>'
1341
+ ].join('');
1342
+
1343
+ return { markup: svg, width, height };
972
1344
  }
973
1345
 
974
- function renderEdge(edge, taskBoxes, layer) {
975
- const source = taskBoxes.get(edge.source);
976
- const target = taskBoxes.get(edge.target);
977
- if (!source || !target) {
978
- return;
1346
+ function downloadBlob(blob, filename) {
1347
+ const url = URL.createObjectURL(blob);
1348
+ const anchor = document.createElement('a');
1349
+ anchor.href = url;
1350
+ anchor.download = filename;
1351
+ document.body.appendChild(anchor);
1352
+ anchor.click();
1353
+ anchor.remove();
1354
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
1355
+ }
1356
+
1357
+ async function exportSvg() {
1358
+ clearExportError();
1359
+ try {
1360
+ const exported = buildExportSvgMarkup();
1361
+ const blob = new Blob([exported.markup], { type: 'image/svg+xml;charset=utf-8' });
1362
+ downloadBlob(blob, 'task-plan.svg');
1363
+ } catch (error) {
1364
+ showExportError(error instanceof Error ? error.message : 'SVG export failed.');
979
1365
  }
1366
+ }
1367
+
1368
+ async function exportPng() {
1369
+ clearExportError();
1370
+ try {
1371
+ const exported = buildExportSvgMarkup();
1372
+ const blob = new Blob([exported.markup], { type: 'image/svg+xml;charset=utf-8' });
1373
+ const url = URL.createObjectURL(blob);
1374
+ const image = new Image();
1375
+ const scale = Math.max(window.devicePixelRatio || 1, 2);
1376
+
1377
+ await new Promise((resolve, reject) => {
1378
+ image.onload = resolve;
1379
+ image.onerror = () => reject(new Error('PNG export is unavailable in this browser. Use SVG export instead.'));
1380
+ image.src = url;
1381
+ });
1382
+
1383
+ const canvas = document.createElement('canvas');
1384
+ canvas.width = Math.ceil(exported.width * scale);
1385
+ canvas.height = Math.ceil(exported.height * scale);
1386
+
1387
+ const context = canvas.getContext('2d');
1388
+ if (!context) {
1389
+ URL.revokeObjectURL(url);
1390
+ throw new Error('Canvas export is unavailable in this browser.');
1391
+ }
980
1392
 
981
- const route = routeEdge(edge, source, target);
982
- layer.appendChild(node('path', {
983
- d: route.d,
984
- fill: 'none',
985
- stroke: cssValue('--dt-text-secondary'),
986
- 'stroke-width': 2,
987
- 'stroke-linecap': 'round',
988
- 'stroke-linejoin': 'round',
989
- 'marker-end': 'url(#arrow)',
990
- }));
1393
+ context.scale(scale, scale);
1394
+ context.drawImage(image, 0, 0, exported.width, exported.height);
1395
+
1396
+ const pngBlob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
1397
+ URL.revokeObjectURL(url);
1398
+
1399
+ if (!pngBlob) {
1400
+ throw new Error('PNG export failed.');
1401
+ }
1402
+
1403
+ downloadBlob(pngBlob, 'task-plan.png');
1404
+ } catch (error) {
1405
+ showExportError(error instanceof Error ? error.message : 'PNG export failed.');
1406
+ }
991
1407
  }
992
1408
 
993
1409
  function render() {
994
- const raw = parseData();
995
- if (raw === null) {
1410
+ const plan = readData();
1411
+ if (!plan) {
996
1412
  return;
997
1413
  }
998
1414
 
999
- const model = buildModel(raw);
1000
- const totalTasks = model.phases.reduce((count, phase) => {
1001
- return count + phase.batches.reduce((batchCount, batch) => batchCount + batch.tasks.length, 0);
1002
- }, 0);
1415
+ clearExportError();
1416
+ updateHeader(plan);
1003
1417
 
1004
- if (!totalTasks) {
1005
- showEmpty('No task plan provided', 'Populate #diagram-data with phases, batches, and tasks to render the plan.');
1418
+ const tasks = getTasks(plan);
1419
+ if (tasks.length === 0) {
1420
+ showEmpty('No tasks to render', 'Add phases, batches, and tasks to populate the plan.');
1006
1421
  return;
1007
1422
  }
1008
1423
 
1009
1424
  hideEmpty();
1010
- const layoutResult = layout(model);
1011
- svg.innerHTML = '';
1012
- svg.setAttribute('viewBox', '0 0 ' + layoutResult.width + ' ' + layoutResult.height);
1013
- svg.style.height = layoutResult.height + 'px';
1014
- svg.setAttribute('aria-label', model.title + ' with ' + totalTasks + ' tasks');
1015
- svg.appendChild(renderDefs());
1016
- svg.appendChild(node('title', { id: 'diagram-title' }, model.title || 'Task execution plan'));
1017
- svg.appendChild(node('desc', { id: 'diagram-description' }, model.description || 'Static task execution plan rendered with SVG'));
1018
-
1019
- const phaseLayer = node('g', { 'aria-hidden': 'true' });
1020
- const batchLayer = node('g', { 'aria-hidden': 'true' });
1021
- const edgeLayer = node('g', { 'aria-hidden': 'true' });
1022
- const nodeLayer = node('g');
1023
-
1024
- layoutResult.phases.forEach((phaseLayout, index) => {
1025
- renderPhase(phaseLayout, phaseLayer, index);
1026
- phaseLayout.batches.forEach((batchLayout) => {
1027
- renderBatch(batchLayout, batchLayer);
1028
- batchLayout.boxes.forEach((box) => renderTask(box, nodeLayer));
1029
- });
1030
- });
1031
-
1032
- model.edges.forEach((edge) => renderEdge(edge, layoutResult.taskBoxes, edgeLayer));
1033
-
1034
- svg.appendChild(phaseLayer);
1035
- svg.appendChild(edgeLayer);
1036
- svg.appendChild(batchLayer);
1037
- svg.appendChild(nodeLayer);
1038
- }
1039
-
1040
- function updateThemeButton() {
1041
- const value = root.getAttribute('data-theme') || 'auto';
1042
- themeButton.textContent = 'Theme: ' + value.charAt(0).toUpperCase() + value.slice(1);
1043
- }
1044
-
1045
- function cycleTheme() {
1046
- const current = root.getAttribute('data-theme') || 'auto';
1047
- const next = themes[(themes.indexOf(current) + 1) % themes.length];
1048
- root.setAttribute('data-theme', next);
1049
- updateThemeButton();
1050
- render();
1425
+ renderPipeline(plan);
1426
+ renderSummary(getSummary(plan));
1427
+ renderLegend(tasks);
1051
1428
  }
1052
1429
 
1053
- function exportSvg() {
1054
- const clone = svg.cloneNode(true);
1055
- clone.setAttribute('xmlns', svgNs);
1056
- const payload = '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
1057
- const blob = new Blob([payload], { type: 'image/svg+xml;charset=utf-8' });
1058
- const url = URL.createObjectURL(blob);
1059
- const anchorEl = document.createElement('a');
1060
- anchorEl.href = url;
1061
- anchorEl.download = 'task-execution-plan.svg';
1062
- anchorEl.click();
1063
- setTimeout(() => URL.revokeObjectURL(url), 1000);
1430
+ themeButton.addEventListener('click', cycleTheme);
1431
+ exportSvgButton.addEventListener('click', () => {
1432
+ void exportSvg();
1433
+ });
1434
+ exportPngButton.addEventListener('click', () => {
1435
+ void exportPng();
1436
+ });
1437
+
1438
+ if (colorSchemeQuery && typeof colorSchemeQuery.addEventListener === 'function') {
1439
+ colorSchemeQuery.addEventListener('change', () => {
1440
+ if (getThemeMode() === 'auto') {
1441
+ clearExportError();
1442
+ }
1443
+ });
1064
1444
  }
1065
1445
 
1066
- themeButton.addEventListener('click', cycleTheme);
1067
- exportButton.addEventListener('click', exportSvg);
1068
1446
  updateThemeButton();
1069
1447
  render();
1070
1448
  })();
1071
1449
  </script>
1072
1450
  </body>
1073
- </html>
1451
+ </html>