@vpxa/aikit 0.1.160 → 0.1.161

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.
@@ -0,0 +1,1081 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="auto">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Kit - Task Plan Static Viewer</title>
7
+ <style>
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;
37
+ }
38
+
39
+ [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: #f1f5f9;
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);
53
+ }
54
+
55
+ @media (prefers-color-scheme: light) {
56
+ :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: #f1f5f9;
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);
70
+ }
71
+ }
72
+
73
+ * {
74
+ box-sizing: border-box;
75
+ }
76
+
77
+ html,
78
+ body {
79
+ height: 100%;
80
+ margin: 0;
81
+ }
82
+
83
+ 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);
90
+ }
91
+
92
+ .shell {
93
+ min-height: 100%;
94
+ display: grid;
95
+ grid-template-rows: auto 1fr;
96
+ }
97
+
98
+ .toolbar {
99
+ position: sticky;
100
+ top: 0;
101
+ z-index: 2;
102
+ display: flex;
103
+ align-items: center;
104
+ 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);
110
+ }
111
+
112
+ .title h1 {
113
+ margin: 0;
114
+ font-size: 1rem;
115
+ font-weight: 700;
116
+ letter-spacing: 0.02em;
117
+ }
118
+
119
+ .title p {
120
+ margin: 0.2rem 0 0;
121
+ font-size: 0.82rem;
122
+ color: var(--dt-text-secondary);
123
+ }
124
+
125
+ .actions {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ justify-content: flex-end;
129
+ gap: 0.6rem;
130
+ }
131
+
132
+ button {
133
+ appearance: none;
134
+ border: 1px solid var(--dt-border);
135
+ background: var(--dt-button-bg);
136
+ color: var(--dt-text-primary);
137
+ 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;
142
+ }
143
+
144
+ button:hover {
145
+ background: var(--dt-button-hover);
146
+ transform: translateY(-1px);
147
+ }
148
+
149
+ .stage {
150
+ padding: 1rem;
151
+ }
152
+
153
+ .canvas-shell {
154
+ position: relative;
155
+ min-height: calc(100vh - 5.5rem);
156
+ border: 1px solid var(--dt-border);
157
+ border-radius: 24px;
158
+ background: var(--dt-panel-bg);
159
+ overflow: hidden;
160
+ box-shadow: 0 24px 60px var(--dt-shadow);
161
+ }
162
+
163
+ svg {
164
+ display: block;
165
+ width: 100%;
166
+ height: min(78vh, 980px);
167
+ min-height: 480px;
168
+ }
169
+
170
+ .empty-state {
171
+ position: absolute;
172
+ inset: 0;
173
+ display: none;
174
+ place-items: center;
175
+ padding: 2rem;
176
+ text-align: center;
177
+ color: var(--dt-text-secondary);
178
+ }
179
+
180
+ .empty-state.is-visible {
181
+ display: grid;
182
+ }
183
+
184
+ .empty-state strong {
185
+ display: block;
186
+ margin-bottom: 0.4rem;
187
+ color: var(--dt-text-primary);
188
+ font-size: 1rem;
189
+ }
190
+
191
+ .badge {
192
+ position: fixed;
193
+ right: 1rem;
194
+ bottom: 1rem;
195
+ z-index: 3;
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 0.55rem;
199
+ padding: 0.55rem 0.8rem;
200
+ border: 1px solid var(--dt-border);
201
+ border-radius: 999px;
202
+ background: var(--dt-badge-bg);
203
+ color: var(--dt-text-secondary);
204
+ backdrop-filter: blur(14px);
205
+ font-size: 0.74rem;
206
+ letter-spacing: 0.04em;
207
+ text-transform: uppercase;
208
+ }
209
+
210
+ .badge-mark {
211
+ width: 0.55rem;
212
+ height: 0.55rem;
213
+ border-radius: 999px;
214
+ background: var(--tp-indigo);
215
+ box-shadow: 0 0 0 0.18rem rgba(99, 102, 241, 0.18);
216
+ }
217
+
218
+ @media (max-width: 720px) {
219
+ .toolbar {
220
+ align-items: flex-start;
221
+ flex-direction: column;
222
+ }
223
+
224
+ .actions {
225
+ width: 100%;
226
+ justify-content: flex-start;
227
+ }
228
+
229
+ .stage {
230
+ padding: 0.75rem;
231
+ }
232
+
233
+ svg {
234
+ min-height: 420px;
235
+ }
236
+ }
237
+ </style>
238
+ </head>
239
+ <body>
240
+ <div class="shell">
241
+ <header class="toolbar">
242
+ <div class="title">
243
+ <h1>Task Execution Plan</h1>
244
+ <p>Phase-based plan with dependency tracking.</p>
245
+ </div>
246
+ <div class="actions">
247
+ <button id="theme-button" type="button">Theme: Auto</button>
248
+ <button id="export-button" type="button">Export SVG</button>
249
+ </div>
250
+ </header>
251
+ <main class="stage">
252
+ <div class="canvas-shell">
253
+ <div class="empty-state" id="empty-state" aria-live="polite"></div>
254
+ <svg id="diagram" role="img" aria-labelledby="diagram-title diagram-description"></svg>
255
+ </div>
256
+ </main>
257
+ </div>
258
+ <div class="badge"><span class="badge-mark"></span>AI Kit static viewer</div>
259
+ <script type="application/json" id="diagram-data">{}</script>
260
+ <script>
261
+ (() => {
262
+ const svgNs = 'http://www.w3.org/2000/svg';
263
+ const root = document.documentElement;
264
+ const svg = document.getElementById('diagram');
265
+ const emptyState = document.getElementById('empty-state');
266
+ const exportButton = document.getElementById('export-button');
267
+ const themeButton = document.getElementById('theme-button');
268
+ const themes = ['auto', 'dark', 'light'];
269
+ const monoFont = '"JetBrains Mono", "SFMono-Regular", Consolas, ui-monospace, monospace';
270
+ const phaseGap = 260;
271
+ const batchGap = 100;
272
+ const taskGap = 80;
273
+ const sequentialTaskGap = 112;
274
+ const taskWidth = 200;
275
+ const taskHeight = 72;
276
+ const phaseHeaderHeight = 60;
277
+ const canvasPadding = 64;
278
+ const phasePaddingX = 28;
279
+ const phasePaddingBottom = 34;
280
+ const batchLabelHeight = 26;
281
+ const phasePalette = [
282
+ '--dt-cyan',
283
+ '--tp-indigo',
284
+ '--dt-amber',
285
+ '--tp-pink',
286
+ '--tp-teal',
287
+ '--dt-violet',
288
+ ];
289
+
290
+ function node(tag, attributes, text) {
291
+ const element = document.createElementNS(svgNs, tag);
292
+ if (attributes) {
293
+ Object.entries(attributes).forEach(([name, value]) => {
294
+ if (value !== undefined && value !== null) {
295
+ element.setAttribute(name, String(value));
296
+ }
297
+ });
298
+ }
299
+ if (text !== undefined) {
300
+ element.innerHTML = escapeHtml(text);
301
+ }
302
+ return element;
303
+ }
304
+
305
+ function cssValue() {
306
+ for (let index = 0; index < arguments.length; index += 1) {
307
+ const value = getComputedStyle(root).getPropertyValue(arguments[index]).trim();
308
+ if (value) {
309
+ return value;
310
+ }
311
+ }
312
+ return '';
313
+ }
314
+
315
+ function rgba(color, alpha) {
316
+ const cleaned = String(color || '').replace('#', '').trim();
317
+ const value = cleaned.length === 3
318
+ ? cleaned.split('').map((char) => char + char).join('')
319
+ : cleaned;
320
+ const number = Number.parseInt(value, 16);
321
+ if (Number.isNaN(number)) {
322
+ return color;
323
+ }
324
+ const red = (number >> 16) & 255;
325
+ const green = (number >> 8) & 255;
326
+ const blue = number & 255;
327
+ return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
328
+ }
329
+
330
+ function wrapText(value, maxChars, maxLines) {
331
+ const words = String(value || '').trim().split(/\s+/).filter(Boolean);
332
+ if (!words.length) {
333
+ return [];
334
+ }
335
+ const lines = [];
336
+ let current = '';
337
+ for (const word of words) {
338
+ const next = current ? current + ' ' + word : word;
339
+ if (next.length > maxChars && current) {
340
+ lines.push(current);
341
+ current = word;
342
+ } else {
343
+ current = next;
344
+ }
345
+ }
346
+ if (current) {
347
+ lines.push(current);
348
+ }
349
+ if (lines.length <= maxLines) {
350
+ return lines;
351
+ }
352
+ const clipped = lines.slice(0, maxLines);
353
+ clipped[maxLines - 1] = clipped[maxLines - 1].replace(/[\s.]+$/, '') + '...';
354
+ return clipped;
355
+ }
356
+
357
+ function escapeHtml(value) {
358
+ return String(value)
359
+ .replace(/&/g, '&amp;')
360
+ .replace(/</g, '&lt;')
361
+ .replace(/>/g, '&gt;')
362
+ .replace(/"/g, '&quot;')
363
+ .replace(/'/g, '&#39;');
364
+ }
365
+
366
+ function showEmpty(title, message) {
367
+ emptyState.classList.add('is-visible');
368
+ emptyState.innerHTML = '<div><strong>' + escapeHtml(title) + '</strong><span>' + escapeHtml(message) + '</span></div>';
369
+ svg.innerHTML = '';
370
+ }
371
+
372
+ function hideEmpty() {
373
+ emptyState.classList.remove('is-visible');
374
+ emptyState.textContent = '';
375
+ }
376
+
377
+ function parseData() {
378
+ const source = document.getElementById('diagram-data');
379
+ if (!source) {
380
+ return {};
381
+ }
382
+ try {
383
+ return JSON.parse(source.textContent || '{}');
384
+ } catch (error) {
385
+ showEmpty('Invalid diagram JSON', String(error && error.message ? error.message : error));
386
+ return null;
387
+ }
388
+ }
389
+
390
+ function phaseColor(index) {
391
+ return cssValue(phasePalette[index % phasePalette.length], '--dt-cyan');
392
+ }
393
+
394
+ function agentColor(agent) {
395
+ if (!agent) return cssValue('--dt-slate');
396
+ const value = agent.toLowerCase();
397
+ if (value.includes('researcher')) return cssValue('--tp-indigo');
398
+ if (value.includes('implementer')) return cssValue('--dt-emerald');
399
+ if (value.includes('frontend')) return cssValue('--tp-pink');
400
+ if (value.includes('reviewer') || value.includes('architect')) return cssValue('--dt-amber');
401
+ if (value.includes('debugger')) return cssValue('--tp-red', '--dt-rose');
402
+ if (value.includes('security')) return cssValue('--tp-orange');
403
+ if (value.includes('explorer')) return cssValue('--dt-cyan');
404
+ if (value.includes('documenter')) return cssValue('--dt-violet');
405
+ if (value.includes('refactor')) return cssValue('--tp-teal');
406
+ if (value.includes('planner')) return cssValue('--tp-purple');
407
+ if (value.includes('orchestrator')) return cssValue('--dt-slate');
408
+ return cssValue('--dt-slate');
409
+ }
410
+
411
+ function agentToken(agent) {
412
+ const value = String(agent || '').trim();
413
+ if (!value) {
414
+ return '?';
415
+ }
416
+ const cleaned = value.replace(/[^A-Za-z0-9\s-]/g, ' ').trim();
417
+ const parts = cleaned.split(/[\s-]+/).filter(Boolean);
418
+ if (parts.length === 1) {
419
+ return parts[0].slice(0, 2).toUpperCase();
420
+ }
421
+ return (parts[0][0] + parts[1][0]).toUpperCase();
422
+ }
423
+
424
+ function statusMeta(status) {
425
+ const value = String(status || 'pending').toLowerCase();
426
+ if (value === 'done') {
427
+ return { key: 'done', color: cssValue('--tp-green'), label: 'Done' };
428
+ }
429
+ if (value === 'in-progress') {
430
+ return { key: 'in-progress', color: cssValue('--tp-blue'), label: 'In progress' };
431
+ }
432
+ if (value === 'blocked') {
433
+ return { key: 'blocked', color: cssValue('--tp-red'), label: 'Blocked' };
434
+ }
435
+ return { key: 'pending', color: cssValue('--tp-gray', '--dt-slate'), label: 'Pending' };
436
+ }
437
+
438
+ function normalizeTask(task, phase, batch, index) {
439
+ return {
440
+ id: typeof task.id === 'string' ? task.id : phase.id + '-' + batch.id + '-task-' + index,
441
+ title: typeof task.title === 'string' ? task.title : 'Untitled task',
442
+ agent: typeof task.agent === 'string' ? task.agent : '',
443
+ files: Array.isArray(task.files) ? task.files.filter((file) => typeof file === 'string') : [],
444
+ status: typeof task.status === 'string' ? task.status : 'pending',
445
+ dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.filter((id) => typeof id === 'string') : [],
446
+ phaseId: phase.id,
447
+ batchId: batch.id,
448
+ };
449
+ }
450
+
451
+ function buildModel(raw) {
452
+ const phases = Array.isArray(raw && raw.phases) ? raw.phases : [];
453
+ const normalized = [];
454
+ const taskMap = new Map();
455
+ const edges = [];
456
+ const edgeKeys = new Set();
457
+
458
+ phases.forEach((phase, phaseIndex) => {
459
+ if (!phase || typeof phase !== 'object') {
460
+ return;
461
+ }
462
+ const phaseValue = {
463
+ id: typeof phase.id === 'string' ? phase.id : 'phase-' + (phaseIndex + 1),
464
+ label: typeof phase.label === 'string' ? phase.label : 'Phase ' + (phaseIndex + 1),
465
+ outcome: typeof phase.outcome === 'string' ? phase.outcome : '',
466
+ batches: [],
467
+ };
468
+
469
+ const batches = Array.isArray(phase.batches) ? phase.batches : [];
470
+ batches.forEach((batch, batchIndex) => {
471
+ if (!batch || typeof batch !== 'object') {
472
+ return;
473
+ }
474
+ const batchValue = {
475
+ id: typeof batch.id === 'string' ? batch.id : phaseValue.id + '-batch-' + (batchIndex + 1),
476
+ order: typeof batch.order === 'number' ? batch.order : batchIndex + 1,
477
+ parallel: Boolean(batch.parallel),
478
+ label: typeof batch.label === 'string' ? batch.label : 'Batch ' + (batchIndex + 1),
479
+ tasks: [],
480
+ };
481
+
482
+ const tasks = Array.isArray(batch.tasks) ? batch.tasks : [];
483
+ tasks.forEach((task, taskIndex) => {
484
+ if (!task || typeof task !== 'object') {
485
+ return;
486
+ }
487
+ const normalizedTask = normalizeTask(task, phaseValue, batchValue, taskIndex + 1);
488
+ batchValue.tasks.push(normalizedTask);
489
+ taskMap.set(normalizedTask.id, normalizedTask);
490
+ });
491
+
492
+ if (batchValue.tasks.length) {
493
+ phaseValue.batches.push(batchValue);
494
+ }
495
+ });
496
+
497
+ if (phaseValue.batches.length) {
498
+ normalized.push(phaseValue);
499
+ }
500
+ });
501
+
502
+ normalized.forEach((phase) => {
503
+ phase.batches.forEach((batch) => {
504
+ batch.tasks.forEach((task) => {
505
+ task.dependsOn.forEach((dependencyId) => {
506
+ if (!taskMap.has(dependencyId)) {
507
+ return;
508
+ }
509
+ const key = dependencyId + '->' + task.id;
510
+ if (edgeKeys.has(key)) {
511
+ return;
512
+ }
513
+ edgeKeys.add(key);
514
+ edges.push({ source: dependencyId, target: task.id });
515
+ });
516
+ });
517
+ });
518
+ });
519
+
520
+ return {
521
+ title: typeof raw.title === 'string' ? raw.title : 'Task execution plan',
522
+ description: typeof raw.description === 'string' ? raw.description : '',
523
+ phases: normalized,
524
+ taskMap,
525
+ edges,
526
+ };
527
+ }
528
+
529
+ function measureTask(task) {
530
+ const titleLines = wrapText(task.title || task.id, 22, 2);
531
+ const agentLines = wrapText(task.agent || 'Unassigned', 18, 1);
532
+ const height = taskHeight + Math.max(0, titleLines.length - 1) * 18;
533
+ return {
534
+ width: taskWidth,
535
+ height,
536
+ titleLines,
537
+ agentLines,
538
+ };
539
+ }
540
+
541
+ function layoutBatch(batch, startX, startY) {
542
+ const tasks = batch.tasks.map((task) => ({ task, size: measureTask(task) }));
543
+ const boxes = [];
544
+ const contentY = startY + batchLabelHeight + 8;
545
+
546
+ if (batch.parallel) {
547
+ let cursorY = contentY;
548
+ let width = taskWidth;
549
+ tasks.forEach(({ task, size }) => {
550
+ boxes.push({ task, x: startX, y: cursorY, width: size.width, height: size.height, size });
551
+ width = Math.max(width, size.width);
552
+ cursorY += size.height + taskGap;
553
+ });
554
+ return {
555
+ boxes,
556
+ width,
557
+ height: Math.max(batchLabelHeight + 8, cursorY - startY - taskGap),
558
+ };
559
+ }
560
+
561
+ let cursorX = startX;
562
+ let maxHeight = 0;
563
+ tasks.forEach(({ task, size }) => {
564
+ boxes.push({ task, x: cursorX, y: contentY, width: size.width, height: size.height, size });
565
+ cursorX += size.width + sequentialTaskGap;
566
+ maxHeight = Math.max(maxHeight, size.height);
567
+ });
568
+ return {
569
+ boxes,
570
+ width: Math.max(taskWidth, cursorX - startX - sequentialTaskGap),
571
+ height: batchLabelHeight + 8 + maxHeight,
572
+ };
573
+ }
574
+
575
+ function layout(model) {
576
+ const phaseLayouts = [];
577
+ const taskBoxes = new Map();
578
+ let currentX = canvasPadding;
579
+ let maxBottom = 0;
580
+
581
+ model.phases.forEach((phase, phaseIndex) => {
582
+ const phaseX = currentX;
583
+ const phaseY = canvasPadding;
584
+ let batchX = phaseX + phasePaddingX;
585
+ let batchY = phaseY + phaseHeaderHeight + 18;
586
+ let rightEdge = phaseX + 320;
587
+ const batchLayouts = [];
588
+
589
+ phase.batches.forEach((batch) => {
590
+ const batchLayout = layoutBatch(batch, batchX, batchY);
591
+ batchLayout.boxes.forEach((box) => {
592
+ taskBoxes.set(box.task.id, box);
593
+ });
594
+ batchLayouts.push({
595
+ batch,
596
+ x: batchX,
597
+ y: batchY,
598
+ width: batchLayout.width,
599
+ height: batchLayout.height,
600
+ boxes: batchLayout.boxes,
601
+ });
602
+ rightEdge = Math.max(rightEdge, batchX + batchLayout.width + phasePaddingX);
603
+ batchY += batchLayout.height + batchGap;
604
+ batchX += Math.max(taskWidth, batchLayout.width) + 108;
605
+ });
606
+
607
+ const phaseHeight = Math.max(
608
+ phaseHeaderHeight + 120,
609
+ batchY - phaseY - batchGap + phasePaddingBottom,
610
+ );
611
+
612
+ phaseLayouts.push({
613
+ phase,
614
+ x: phaseX,
615
+ y: phaseY,
616
+ width: Math.max(360, rightEdge - phaseX),
617
+ height: phaseHeight,
618
+ color: phaseColor(phaseIndex),
619
+ batches: batchLayouts,
620
+ });
621
+
622
+ currentX += Math.max(360, rightEdge - phaseX) + phaseGap;
623
+ maxBottom = Math.max(maxBottom, phaseY + phaseHeight);
624
+ });
625
+
626
+ return {
627
+ phases: phaseLayouts,
628
+ taskBoxes,
629
+ width: Math.max(1160, currentX - phaseGap + canvasPadding),
630
+ height: Math.max(700, maxBottom + canvasPadding),
631
+ };
632
+ }
633
+
634
+ function routeEdge(edge, source, target) {
635
+ const startX = source.x + source.width;
636
+ const startY = source.y + source.height / 2;
637
+ const endX = target.x;
638
+ const endY = target.y + target.height / 2;
639
+ const deltaX = endX - startX;
640
+ const curve = Math.max(68, Math.abs(deltaX) * 0.38);
641
+
642
+ if (deltaX >= 0) {
643
+ return {
644
+ d: 'M ' + startX + ' ' + startY + ' C ' + (startX + curve) + ' ' + startY + ', ' + (endX - curve) + ' ' + endY + ', ' + endX + ' ' + endY,
645
+ labelX: startX + deltaX / 2,
646
+ labelY: Math.min(startY, endY) - 16,
647
+ };
648
+ }
649
+
650
+ const midX = Math.max(startX + 72, startX + Math.abs(deltaX) * 0.35);
651
+ const liftY = Math.min(startY, endY) - 56;
652
+ return {
653
+ d: 'M ' + startX + ' ' + startY + ' C ' + midX + ' ' + startY + ', ' + midX + ' ' + liftY + ', ' + (endX - 18) + ' ' + liftY + ' S ' + (endX - 32) + ' ' + endY + ', ' + endX + ' ' + endY,
654
+ labelX: midX,
655
+ labelY: liftY - 10,
656
+ };
657
+ }
658
+
659
+ function renderDefs() {
660
+ const defs = node('defs');
661
+
662
+ const pattern = node('pattern', {
663
+ id: 'grid',
664
+ width: 40,
665
+ height: 40,
666
+ patternUnits: 'userSpaceOnUse',
667
+ });
668
+ pattern.appendChild(node('path', {
669
+ d: 'M 40 0 L 0 0 0 40',
670
+ fill: 'none',
671
+ stroke: cssValue('--dt-grid'),
672
+ 'stroke-width': 1,
673
+ }));
674
+ defs.appendChild(pattern);
675
+
676
+ const marker = node('marker', {
677
+ id: 'arrow',
678
+ viewBox: '0 0 10 10',
679
+ refX: 8,
680
+ refY: 5,
681
+ markerWidth: 7,
682
+ markerHeight: 7,
683
+ orient: 'auto-start-reverse',
684
+ });
685
+ marker.appendChild(node('path', {
686
+ d: 'M 0 0 L 10 5 L 0 10 z',
687
+ fill: cssValue('--dt-text-secondary'),
688
+ }));
689
+ defs.appendChild(marker);
690
+
691
+ const filter = node('filter', {
692
+ id: 'node-shadow',
693
+ x: '-20%',
694
+ y: '-20%',
695
+ width: '140%',
696
+ height: '140%',
697
+ });
698
+ filter.appendChild(node('feDropShadow', {
699
+ dx: 0,
700
+ dy: 12,
701
+ stdDeviation: 12,
702
+ 'flood-color': cssValue('--dt-shadow'),
703
+ }));
704
+ defs.appendChild(filter);
705
+
706
+ phasePalette.forEach((token, index) => {
707
+ const color = cssValue(token, '--dt-cyan');
708
+ const gradient = node('linearGradient', {
709
+ id: 'phase-grad-' + index,
710
+ x1: '0%',
711
+ y1: '0%',
712
+ x2: '100%',
713
+ y2: '100%',
714
+ });
715
+ gradient.appendChild(node('stop', { offset: '0%', 'stop-color': rgba(color, 0.14) }));
716
+ gradient.appendChild(node('stop', { offset: '100%', 'stop-color': rgba(color, 0.03) }));
717
+ defs.appendChild(gradient);
718
+ });
719
+
720
+ return defs;
721
+ }
722
+
723
+ function renderTextLines(lines, group, x, y, fontSize, color, fontWeight, lineHeight, anchor) {
724
+ lines.forEach((line, index) => {
725
+ group.appendChild(node('text', {
726
+ x,
727
+ y: y + index * lineHeight,
728
+ fill: color,
729
+ 'font-size': fontSize,
730
+ 'font-weight': fontWeight,
731
+ 'text-anchor': anchor || 'start',
732
+ }, line));
733
+ });
734
+ }
735
+
736
+ function renderPhase(phaseLayout, layer, index) {
737
+ const group = node('g');
738
+ const gradientId = 'phase-grad-' + (index % phasePalette.length);
739
+
740
+ group.appendChild(node('rect', {
741
+ x: phaseLayout.x,
742
+ y: phaseLayout.y,
743
+ width: phaseLayout.width,
744
+ height: phaseLayout.height,
745
+ rx: 28,
746
+ fill: 'url(#' + gradientId + ')',
747
+ stroke: rgba(phaseLayout.color, 0.42),
748
+ 'stroke-width': 1.6,
749
+ 'stroke-dasharray': '10 10',
750
+ }));
751
+
752
+ group.appendChild(node('text', {
753
+ x: phaseLayout.x + phasePaddingX,
754
+ y: phaseLayout.y + 30,
755
+ fill: phaseLayout.color,
756
+ 'font-size': 10,
757
+ 'font-family': monoFont,
758
+ 'font-weight': 700,
759
+ 'letter-spacing': '0.08em',
760
+ }, 'PHASE'));
761
+
762
+ group.appendChild(node('text', {
763
+ x: phaseLayout.x + phasePaddingX,
764
+ y: phaseLayout.y + 50,
765
+ fill: cssValue('--dt-text-primary'),
766
+ 'font-size': 18,
767
+ 'font-weight': 700,
768
+ }, phaseLayout.phase.label));
769
+
770
+ const outcomeLines = wrapText(phaseLayout.phase.outcome || '', 38, 2);
771
+ renderTextLines(
772
+ outcomeLines,
773
+ group,
774
+ phaseLayout.x + phasePaddingX,
775
+ phaseLayout.y + 68,
776
+ 11.5,
777
+ cssValue('--dt-text-secondary'),
778
+ 500,
779
+ 15,
780
+ 'start',
781
+ );
782
+
783
+ layer.appendChild(group);
784
+ }
785
+
786
+ function renderBatch(batchLayout, layer) {
787
+ const label = (batchLayout.batch.parallel ? '⫘ Parallel' : '→ Sequential') + ' · ' + batchLayout.batch.label;
788
+ const group = node('g');
789
+
790
+ group.appendChild(node('text', {
791
+ x: batchLayout.x,
792
+ y: batchLayout.y + 14,
793
+ fill: cssValue('--dt-text-secondary'),
794
+ 'font-size': 10.5,
795
+ 'font-family': monoFont,
796
+ 'font-weight': 600,
797
+ 'letter-spacing': '0.03em',
798
+ }, label));
799
+
800
+ const orderText = '#' + String(batchLayout.batch.order);
801
+ const badgeWidth = Math.max(30, orderText.length * 7 + 14);
802
+ group.appendChild(node('rect', {
803
+ x: batchLayout.x + Math.max(120, Math.min(batchLayout.width - badgeWidth, label.length * 5.9 + 16)),
804
+ y: batchLayout.y,
805
+ width: badgeWidth,
806
+ height: 18,
807
+ rx: 9,
808
+ fill: rgba(cssValue('--dt-slate'), 0.14),
809
+ stroke: rgba(cssValue('--dt-slate'), 0.26),
810
+ }));
811
+ group.appendChild(node('text', {
812
+ x: batchLayout.x + Math.max(120, Math.min(batchLayout.width - badgeWidth, label.length * 5.9 + 16)) + badgeWidth / 2,
813
+ y: batchLayout.y + 12,
814
+ fill: cssValue('--dt-text-secondary'),
815
+ 'font-size': 10,
816
+ 'font-family': monoFont,
817
+ 'font-weight': 700,
818
+ 'text-anchor': 'middle',
819
+ }, orderText));
820
+
821
+ layer.appendChild(group);
822
+ }
823
+
824
+ function renderStatus(group, box, meta) {
825
+ const cx = box.x + 18;
826
+ const cy = box.y + box.height - 18;
827
+
828
+ if (meta.key === 'in-progress') {
829
+ const pulse = node('circle', {
830
+ cx,
831
+ cy,
832
+ r: 10,
833
+ fill: 'none',
834
+ stroke: rgba(meta.color, 0.4),
835
+ 'stroke-width': 2,
836
+ });
837
+ pulse.appendChild(node('animate', {
838
+ attributeName: 'r',
839
+ values: '6;12;6',
840
+ dur: '1.6s',
841
+ repeatCount: 'indefinite',
842
+ }));
843
+ pulse.appendChild(node('animate', {
844
+ attributeName: 'opacity',
845
+ values: '0.9;0.2;0.9',
846
+ dur: '1.6s',
847
+ repeatCount: 'indefinite',
848
+ }));
849
+ group.appendChild(pulse);
850
+ }
851
+
852
+ group.appendChild(node('circle', {
853
+ cx,
854
+ cy,
855
+ r: 6,
856
+ fill: meta.color,
857
+ }));
858
+
859
+ if (meta.key === 'done') {
860
+ group.appendChild(node('path', {
861
+ d: 'M ' + (cx - 3) + ' ' + cy + ' L ' + (cx - 1) + ' ' + (cy + 3) + ' L ' + (cx + 4) + ' ' + (cy - 3),
862
+ fill: 'none',
863
+ stroke: '#ffffff',
864
+ 'stroke-width': 1.6,
865
+ 'stroke-linecap': 'round',
866
+ 'stroke-linejoin': 'round',
867
+ }));
868
+ }
869
+
870
+ if (meta.key === 'blocked') {
871
+ group.appendChild(node('path', {
872
+ d: 'M ' + (cx - 3) + ' ' + (cy - 3) + ' L ' + (cx + 3) + ' ' + (cy + 3) + ' M ' + (cx + 3) + ' ' + (cy - 3) + ' L ' + (cx - 3) + ' ' + (cy + 3),
873
+ fill: 'none',
874
+ stroke: '#ffffff',
875
+ 'stroke-width': 1.5,
876
+ 'stroke-linecap': 'round',
877
+ }));
878
+ }
879
+
880
+ group.appendChild(node('text', {
881
+ x: box.x + 30,
882
+ y: box.y + box.height - 14,
883
+ fill: cssValue('--dt-text-secondary'),
884
+ 'font-size': 10.5,
885
+ 'font-weight': 600,
886
+ }, meta.label));
887
+ }
888
+
889
+ function renderFileBadge(group, box, files) {
890
+ if (!files.length) {
891
+ return;
892
+ }
893
+ const label = files.length + (files.length === 1 ? ' file' : ' files');
894
+ const width = Math.max(46, label.length * 6.6 + 14);
895
+ const x = box.x + box.width - width - 14;
896
+ const y = box.y + box.height - 27;
897
+
898
+ group.appendChild(node('rect', {
899
+ x,
900
+ y,
901
+ width,
902
+ height: 18,
903
+ rx: 9,
904
+ fill: rgba(cssValue('--dt-cyan'), 0.12),
905
+ stroke: rgba(cssValue('--dt-cyan'), 0.22),
906
+ }));
907
+ group.appendChild(node('text', {
908
+ x: x + width / 2,
909
+ y: y + 12,
910
+ fill: cssValue('--dt-text-secondary'),
911
+ 'font-size': 10,
912
+ 'font-family': monoFont,
913
+ 'font-weight': 600,
914
+ 'text-anchor': 'middle',
915
+ }, label));
916
+ }
917
+
918
+ function renderTask(box, layer) {
919
+ const task = box.task;
920
+ const accent = agentColor(task.agent);
921
+ const meta = statusMeta(task.status);
922
+ const group = node('g');
923
+ const titleX = box.x + 44;
924
+ const token = agentToken(task.agent);
925
+
926
+ group.appendChild(node('rect', {
927
+ x: box.x,
928
+ y: box.y,
929
+ width: box.width,
930
+ height: box.height,
931
+ rx: 18,
932
+ fill: rgba(accent, 0.08),
933
+ stroke: rgba(accent, 0.28),
934
+ 'stroke-width': 1.25,
935
+ filter: 'url(#node-shadow)',
936
+ }));
937
+
938
+ group.appendChild(node('rect', {
939
+ x: box.x + 1,
940
+ y: box.y + 1,
941
+ width: box.width - 2,
942
+ height: box.height - 2,
943
+ rx: 17,
944
+ fill: cssValue('--dt-node-fill'),
945
+ }));
946
+
947
+ group.appendChild(node('rect', {
948
+ x: box.x,
949
+ y: box.y,
950
+ width: 4,
951
+ height: box.height,
952
+ rx: 4,
953
+ fill: accent,
954
+ }));
955
+
956
+ group.appendChild(node('circle', {
957
+ cx: box.x + 22,
958
+ cy: box.y + 19,
959
+ r: 11,
960
+ fill: rgba(accent, 0.18),
961
+ stroke: rgba(accent, 0.4),
962
+ }));
963
+ group.appendChild(node('text', {
964
+ x: box.x + 22,
965
+ y: box.y + 23,
966
+ fill: accent,
967
+ 'font-size': 10,
968
+ 'font-family': monoFont,
969
+ 'font-weight': 700,
970
+ 'text-anchor': 'middle',
971
+ }, token));
972
+
973
+ renderTextLines(box.size.agentLines, group, titleX, box.y + 21, 10.5, accent, 700, 14, 'start');
974
+ renderTextLines(box.size.titleLines, group, titleX, box.y + 40, 13.5, cssValue('--dt-text-primary'), 700, 17, 'start');
975
+
976
+ renderStatus(group, box, meta);
977
+ renderFileBadge(group, box, task.files);
978
+
979
+ layer.appendChild(group);
980
+ }
981
+
982
+ function renderEdge(edge, taskBoxes, layer) {
983
+ const source = taskBoxes.get(edge.source);
984
+ const target = taskBoxes.get(edge.target);
985
+ if (!source || !target) {
986
+ return;
987
+ }
988
+
989
+ const route = routeEdge(edge, source, target);
990
+ layer.appendChild(node('path', {
991
+ d: route.d,
992
+ fill: 'none',
993
+ stroke: cssValue('--dt-text-secondary'),
994
+ 'stroke-width': 2,
995
+ 'stroke-linecap': 'round',
996
+ 'stroke-linejoin': 'round',
997
+ 'marker-end': 'url(#arrow)',
998
+ }));
999
+ }
1000
+
1001
+ function render() {
1002
+ const raw = parseData();
1003
+ if (raw === null) {
1004
+ return;
1005
+ }
1006
+
1007
+ const model = buildModel(raw);
1008
+ const totalTasks = model.phases.reduce((count, phase) => {
1009
+ return count + phase.batches.reduce((batchCount, batch) => batchCount + batch.tasks.length, 0);
1010
+ }, 0);
1011
+
1012
+ if (!totalTasks) {
1013
+ showEmpty('No task plan provided', 'Populate #diagram-data with phases, batches, and tasks to render the plan.');
1014
+ return;
1015
+ }
1016
+
1017
+ hideEmpty();
1018
+ const layoutResult = layout(model);
1019
+ svg.innerHTML = '';
1020
+ svg.setAttribute('viewBox', '0 0 ' + layoutResult.width + ' ' + layoutResult.height);
1021
+ svg.setAttribute('aria-label', model.title + ' with ' + totalTasks + ' tasks');
1022
+ svg.appendChild(renderDefs());
1023
+ svg.appendChild(node('rect', { x: 0, y: 0, width: layoutResult.width, height: layoutResult.height, fill: 'url(#grid)' }));
1024
+ svg.appendChild(node('title', { id: 'diagram-title' }, model.title || 'Task execution plan'));
1025
+ svg.appendChild(node('desc', { id: 'diagram-description' }, model.description || 'Static task execution plan rendered with SVG'));
1026
+
1027
+ const phaseLayer = node('g', { 'aria-hidden': 'true' });
1028
+ const batchLayer = node('g', { 'aria-hidden': 'true' });
1029
+ const edgeLayer = node('g', { 'aria-hidden': 'true' });
1030
+ const nodeLayer = node('g');
1031
+
1032
+ layoutResult.phases.forEach((phaseLayout, index) => {
1033
+ renderPhase(phaseLayout, phaseLayer, index);
1034
+ phaseLayout.batches.forEach((batchLayout) => {
1035
+ renderBatch(batchLayout, batchLayer);
1036
+ batchLayout.boxes.forEach((box) => renderTask(box, nodeLayer));
1037
+ });
1038
+ });
1039
+
1040
+ model.edges.forEach((edge) => renderEdge(edge, layoutResult.taskBoxes, edgeLayer));
1041
+
1042
+ svg.appendChild(phaseLayer);
1043
+ svg.appendChild(edgeLayer);
1044
+ svg.appendChild(batchLayer);
1045
+ svg.appendChild(nodeLayer);
1046
+ }
1047
+
1048
+ function updateThemeButton() {
1049
+ const value = root.getAttribute('data-theme') || 'auto';
1050
+ themeButton.textContent = 'Theme: ' + value.charAt(0).toUpperCase() + value.slice(1);
1051
+ }
1052
+
1053
+ function cycleTheme() {
1054
+ const current = root.getAttribute('data-theme') || 'auto';
1055
+ const next = themes[(themes.indexOf(current) + 1) % themes.length];
1056
+ root.setAttribute('data-theme', next);
1057
+ updateThemeButton();
1058
+ render();
1059
+ }
1060
+
1061
+ function exportSvg() {
1062
+ const clone = svg.cloneNode(true);
1063
+ clone.setAttribute('xmlns', svgNs);
1064
+ const payload = '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
1065
+ const blob = new Blob([payload], { type: 'image/svg+xml;charset=utf-8' });
1066
+ const url = URL.createObjectURL(blob);
1067
+ const anchorEl = document.createElement('a');
1068
+ anchorEl.href = url;
1069
+ anchorEl.download = 'task-execution-plan.svg';
1070
+ anchorEl.click();
1071
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1072
+ }
1073
+
1074
+ themeButton.addEventListener('click', cycleTheme);
1075
+ exportButton.addEventListener('click', exportSvg);
1076
+ updateThemeButton();
1077
+ render();
1078
+ })();
1079
+ </script>
1080
+ </body>
1081
+ </html>