htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/analytics/pattern_learning.py +771 -0
  3. htmlgraph/api/main.py +56 -23
  4. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  5. htmlgraph/api/templates/dashboard.html +3 -3
  6. htmlgraph/api/templates/partials/work-items.html +613 -0
  7. htmlgraph/builders/track.py +26 -0
  8. htmlgraph/cli/base.py +31 -7
  9. htmlgraph/cli/work/__init__.py +74 -0
  10. htmlgraph/cli/work/browse.py +114 -0
  11. htmlgraph/cli/work/snapshot.py +558 -0
  12. htmlgraph/collections/base.py +34 -0
  13. htmlgraph/collections/todo.py +12 -0
  14. htmlgraph/converter.py +11 -0
  15. htmlgraph/db/schema.py +34 -1
  16. htmlgraph/hooks/orchestrator.py +88 -14
  17. htmlgraph/hooks/session_handler.py +3 -1
  18. htmlgraph/models.py +22 -2
  19. htmlgraph/orchestration/__init__.py +4 -0
  20. htmlgraph/orchestration/plugin_manager.py +1 -2
  21. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  22. htmlgraph/refs.py +343 -0
  23. htmlgraph/sdk.py +162 -1
  24. htmlgraph/session_manager.py +154 -2
  25. htmlgraph/sessions/__init__.py +23 -0
  26. htmlgraph/sessions/handoff.py +755 -0
  27. htmlgraph/track_builder.py +12 -0
  28. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
  29. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
  30. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
  31. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
  32. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  33. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  34. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  35. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
  36. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,613 @@
1
+ <div class="view-container work-items-view">
2
+ <div class="view-header">
3
+ <h2>Work Items - Smart Kanban</h2>
4
+ <div class="view-description">
5
+ All work items (features, bugs, spikes, tasks) organized by status. Drag cards between visible columns.
6
+ </div>
7
+ <div class="view-filters">
8
+ <div class="filter-group">
9
+ <label>Quick Filter:</label>
10
+ <select class="filter-select"
11
+ hx-get="/views/work-items"
12
+ hx-target="#content-area"
13
+ hx-trigger="change"
14
+ name="status">
15
+ <option value="all">All Status</option>
16
+ <option value="todo">To Do Only</option>
17
+ <option value="in_progress">In Progress Only</option>
18
+ <option value="blocked">Blocked Only</option>
19
+ <option value="done">Done Only</option>
20
+ </select>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="kanban-board">
26
+ <!-- TO DO Column -->
27
+ <div class="kanban-column todo-column" data-status="todo">
28
+ <div class="column-header">
29
+ <div>
30
+ <h3><span class="column-collapse-indicator">▼</span> To Do</h3>
31
+ </div>
32
+ <span class="column-count">{{ work_items_by_status.todo|length }}</span>
33
+ </div>
34
+ <div class="column-cards">
35
+ {% for item in work_items_by_status.todo %}
36
+ <div class="work-item-card todo-card" draggable="true" data-work-item-id="{{ item.id }}">
37
+ <div class="card-header">
38
+ <span class="work-item-type-badge type-{{ item.type }}">{{ item.type }}</span>
39
+ <span class="priority-badge priority-{{ item.priority }}">{{ item.priority }}</span>
40
+ </div>
41
+ <h4 class="card-title">{{ item.title }}</h4>
42
+ {% if item.description %}
43
+ <p class="card-description">{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}</p>
44
+ {% endif %}
45
+ <div class="card-footer">
46
+ <span class="work-item-id" title="{{ item.id }}">{{ item.id[:8] }}</span>
47
+ <div class="contributors">
48
+ {% if item.contributors %}
49
+ {% for agent in item.contributors %}
50
+ <span class="contributor-tag agent-{{ agent|lower|replace(' ', '-') }}" title="{{ agent }}">
51
+ {{ agent[:1]|upper }}
52
+ </span>
53
+ {% endfor %}
54
+ {% endif %}
55
+ {% if item.assigned_to %}
56
+ <span class="assigned-to" title="Assigned to {{ item.assigned_to }}">👤 {{ item.assigned_to[:8] }}</span>
57
+ {% endif %}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ {% endfor %}
62
+ {% if not work_items_by_status.todo %}
63
+ <div class="empty-column">No tasks</div>
64
+ {% endif %}
65
+ </div>
66
+ </div>
67
+
68
+ <!-- IN PROGRESS Column -->
69
+ <div class="kanban-column in-progress-column" data-status="in_progress">
70
+ <div class="column-header">
71
+ <div>
72
+ <h3><span class="column-collapse-indicator">▼</span> In Progress</h3>
73
+ </div>
74
+ <span class="column-count">{{ work_items_by_status.in_progress|length }}</span>
75
+ </div>
76
+ <div class="column-cards">
77
+ {% for item in work_items_by_status.in_progress %}
78
+ <div class="work-item-card in-progress-card" draggable="true" data-work-item-id="{{ item.id }}">
79
+ <div class="card-header">
80
+ <span class="work-item-type-badge type-{{ item.type }}">{{ item.type }}</span>
81
+ <span class="priority-badge priority-{{ item.priority }}">{{ item.priority }}</span>
82
+ </div>
83
+ <h4 class="card-title">{{ item.title }}</h4>
84
+ {% if item.description %}
85
+ <p class="card-description">{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}</p>
86
+ {% endif %}
87
+ <div class="card-footer">
88
+ <span class="work-item-id" title="{{ item.id }}">{{ item.id[:8] }}</span>
89
+ <div class="contributors">
90
+ {% if item.contributors %}
91
+ {% for agent in item.contributors %}
92
+ <span class="contributor-tag agent-{{ agent|lower|replace(' ', '-') }}" title="{{ agent }}">
93
+ {{ agent[:1]|upper }}
94
+ </span>
95
+ {% endfor %}
96
+ {% endif %}
97
+ {% if item.assigned_to %}
98
+ <span class="assigned-to" title="Assigned to {{ item.assigned_to }}">👤 {{ item.assigned_to[:8] }}</span>
99
+ {% endif %}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ {% endfor %}
104
+ {% if not work_items_by_status.in_progress %}
105
+ <div class="empty-column">No tasks</div>
106
+ {% endif %}
107
+ </div>
108
+ </div>
109
+
110
+ <!-- BLOCKED Column (Collapsed by default) -->
111
+ <div class="kanban-column blocked-column collapsed" data-status="blocked">
112
+ <div class="column-header">
113
+ <div>
114
+ <h3><span class="column-collapse-indicator">▶</span> Blocked</h3>
115
+ </div>
116
+ <span class="column-count">{{ work_items_by_status.blocked|length }}</span>
117
+ </div>
118
+ <div class="column-cards">
119
+ {% for item in work_items_by_status.blocked %}
120
+ <div class="work-item-card blocked-card" draggable="true" data-work-item-id="{{ item.id }}">
121
+ <div class="card-header">
122
+ <span class="work-item-type-badge type-{{ item.type }}">{{ item.type }}</span>
123
+ <span class="priority-badge priority-{{ item.priority }}">{{ item.priority }}</span>
124
+ </div>
125
+ <h4 class="card-title">{{ item.title }}</h4>
126
+ {% if item.description %}
127
+ <p class="card-description">{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}</p>
128
+ {% endif %}
129
+ <div class="card-footer">
130
+ <span class="work-item-id" title="{{ item.id }}">{{ item.id[:8] }}</span>
131
+ <div class="contributors">
132
+ {% if item.contributors %}
133
+ {% for agent in item.contributors %}
134
+ <span class="contributor-tag agent-{{ agent|lower|replace(' ', '-') }}" title="{{ agent }}">
135
+ {{ agent[:1]|upper }}
136
+ </span>
137
+ {% endfor %}
138
+ {% endif %}
139
+ {% if item.assigned_to %}
140
+ <span class="assigned-to" title="Assigned to {{ item.assigned_to }}">👤 {{ item.assigned_to[:8] }}</span>
141
+ {% endif %}
142
+ </div>
143
+ </div>
144
+ </div>
145
+ {% endfor %}
146
+ {% if not work_items_by_status.blocked %}
147
+ <div class="empty-column">No tasks</div>
148
+ {% endif %}
149
+ </div>
150
+ </div>
151
+
152
+ <!-- DONE Column (Collapsed by default) -->
153
+ <div class="kanban-column done-column collapsed" data-status="done">
154
+ <div class="column-header">
155
+ <div>
156
+ <h3><span class="column-collapse-indicator">▶</span> Done</h3>
157
+ </div>
158
+ <span class="column-count">{{ work_items_by_status.done|length }}</span>
159
+ </div>
160
+ <div class="column-cards">
161
+ {% for item in work_items_by_status.done %}
162
+ <div class="work-item-card done-card" draggable="true" data-work-item-id="{{ item.id }}">
163
+ <div class="card-header">
164
+ <span class="work-item-type-badge type-{{ item.type }}">{{ item.type }}</span>
165
+ <span class="priority-badge priority-{{ item.priority }}">{{ item.priority }}</span>
166
+ </div>
167
+ <h4 class="card-title">{{ item.title }}</h4>
168
+ {% if item.description %}
169
+ <p class="card-description">{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}</p>
170
+ {% endif %}
171
+ <div class="card-footer">
172
+ <span class="work-item-id" title="{{ item.id }}">{{ item.id[:8] }}</span>
173
+ <div class="contributors">
174
+ {% if item.contributors %}
175
+ {% for agent in item.contributors %}
176
+ <span class="contributor-tag agent-{{ agent|lower|replace(' ', '-') }}" title="{{ agent }}">
177
+ {{ agent[:1]|upper }}
178
+ </span>
179
+ {% endfor %}
180
+ {% endif %}
181
+ {% if item.assigned_to %}
182
+ <span class="assigned-to" title="Assigned to {{ item.assigned_to }}">👤 {{ item.assigned_to[:8] }}</span>
183
+ {% endif %}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ {% endfor %}
188
+ {% if not work_items_by_status.done %}
189
+ <div class="empty-column">No tasks</div>
190
+ {% endif %}
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <script>
197
+ // Smart Kanban Column Management
198
+ class SmartKanban {
199
+ constructor() {
200
+ this.visibleColumns = ['todo', 'in_progress']; // Default visible columns
201
+ this.openOrder = []; // Track which column was opened first
202
+ this.init();
203
+ }
204
+
205
+ init() {
206
+ // Load saved state from localStorage
207
+ const saved = localStorage.getItem('kanban-visible-columns');
208
+ if (saved) {
209
+ this.visibleColumns = JSON.parse(saved);
210
+ this.renderColumns();
211
+ }
212
+
213
+ // Setup column header click handlers
214
+ document.querySelectorAll('.column-header').forEach(header => {
215
+ header.addEventListener('click', (e) => {
216
+ const column = header.closest('.kanban-column');
217
+ const status = column.dataset.status;
218
+ this.toggleColumn(status);
219
+ });
220
+ });
221
+
222
+ // Setup drag and drop
223
+ this.setupDragDrop();
224
+ }
225
+
226
+ toggleColumn(status) {
227
+ if (this.visibleColumns.includes(status)) {
228
+ // Column is visible, hide it
229
+ this.visibleColumns = this.visibleColumns.filter(s => s !== status);
230
+ this.openOrder = this.openOrder.filter(s => s !== status);
231
+ } else {
232
+ // Column is hidden, show it
233
+ this.visibleColumns.push(status);
234
+ this.openOrder.push(status);
235
+
236
+ // Keep only 2 visible columns
237
+ if (this.visibleColumns.length > 2) {
238
+ // Collapse oldest opened column (if not 'done' or 'blocked')
239
+ const toCollapse = this.openOrder.shift();
240
+ if (toCollapse && toCollapse !== 'done' && toCollapse !== 'blocked') {
241
+ this.visibleColumns = this.visibleColumns.filter(s => s !== toCollapse);
242
+ } else if (toCollapse) {
243
+ // If we need to collapse 'done' or 'blocked', find next oldest
244
+ for (let col of this.openOrder) {
245
+ if (col !== 'done' && col !== 'blocked') {
246
+ this.visibleColumns = this.visibleColumns.filter(s => s !== col);
247
+ this.openOrder = this.openOrder.filter(s => s !== col);
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ this.saveState();
256
+ this.renderColumns();
257
+ }
258
+
259
+ renderColumns() {
260
+ document.querySelectorAll('.kanban-column').forEach(column => {
261
+ const status = column.dataset.status;
262
+ const isVisible = this.visibleColumns.includes(status);
263
+
264
+ column.classList.toggle('collapsed', !isVisible);
265
+ });
266
+ }
267
+
268
+ saveState() {
269
+ localStorage.setItem('kanban-visible-columns', JSON.stringify(this.visibleColumns));
270
+ }
271
+
272
+ setupDragDrop() {
273
+ document.addEventListener('dragstart', (e) => {
274
+ if (e.target.classList.contains('work-item-card')) {
275
+ e.target.classList.add('dragging');
276
+ e.dataTransfer.effectAllowed = 'move';
277
+ e.dataTransfer.setData('card-id', e.target.dataset.workItemId);
278
+ }
279
+ });
280
+
281
+ document.addEventListener('dragend', (e) => {
282
+ document.querySelectorAll('.work-item-card').forEach(card => {
283
+ card.classList.remove('dragging');
284
+ });
285
+ });
286
+
287
+ document.addEventListener('dragover', (e) => {
288
+ e.preventDefault();
289
+ e.dataTransfer.dropEffect = 'move';
290
+
291
+ // Only allow drops in visible columns
292
+ const cardsList = e.target.closest('.column-cards');
293
+ if (cardsList) {
294
+ const column = cardsList.closest('.kanban-column');
295
+ if (!column.classList.contains('collapsed')) {
296
+ cardsList.style.background = 'rgba(205, 255, 0, 0.1)';
297
+ }
298
+ }
299
+ });
300
+
301
+ document.addEventListener('dragleave', (e) => {
302
+ const cardsList = e.target.closest('.column-cards');
303
+ if (cardsList) {
304
+ cardsList.style.background = '';
305
+ }
306
+ });
307
+
308
+ document.addEventListener('drop', (e) => {
309
+ e.preventDefault();
310
+ const cardsList = e.target.closest('.column-cards');
311
+ if (cardsList) {
312
+ const column = cardsList.closest('.kanban-column');
313
+ const cardId = e.dataTransfer.getData('card-id');
314
+ const card = document.querySelector(`[data-work-item-id="${cardId}"]`);
315
+
316
+ if (card && !column.classList.contains('collapsed')) {
317
+ cardsList.appendChild(card);
318
+ cardsList.style.background = '';
319
+
320
+ // TODO: Send update to server
321
+ const newStatus = column.dataset.status;
322
+ console.log(`Move feature ${cardId} to ${newStatus}`);
323
+ }
324
+ }
325
+ });
326
+ }
327
+ }
328
+
329
+ // Initialize when DOM is ready
330
+ document.addEventListener('htmx:afterSettle', function(evt) {
331
+ if (evt.detail.target.id === 'content-area' &&
332
+ document.querySelector('.work-items-view')) {
333
+ window.kanban = new SmartKanban();
334
+ }
335
+ });
336
+
337
+ // Also initialize on page load if work items are already visible
338
+ if (document.querySelector('.work-items-view')) {
339
+ window.kanban = new SmartKanban();
340
+ }
341
+ </script>
342
+
343
+ <style>
344
+ /* Kanban specific styles */
345
+ .kanban-board {
346
+ display: grid;
347
+ grid-template-columns: repeat(4, 1fr);
348
+ gap: var(--spacing-lg);
349
+ height: calc(100% - 100px);
350
+ }
351
+
352
+ .kanban-column {
353
+ display: flex;
354
+ flex-direction: column;
355
+ background: var(--bg-card);
356
+ border: 1px solid var(--border-subtle);
357
+ border-radius: 2px;
358
+ overflow: hidden;
359
+ transition: all var(--transition-base);
360
+ min-height: 100px;
361
+ }
362
+
363
+ .kanban-column.collapsed {
364
+ background: var(--bg-darker);
365
+ max-height: 60px;
366
+ }
367
+
368
+ .column-header {
369
+ display: flex;
370
+ justify-content: space-between;
371
+ align-items: center;
372
+ padding: var(--spacing-lg);
373
+ background: var(--bg-darker);
374
+ border-bottom: 1px solid var(--border-subtle);
375
+ cursor: pointer;
376
+ transition: all var(--transition-base);
377
+ user-select: none;
378
+ }
379
+
380
+ .column-header:hover {
381
+ background: var(--bg-hover);
382
+ }
383
+
384
+ .column-header h3 {
385
+ margin: 0;
386
+ font-size: 1rem;
387
+ color: var(--accent-lime);
388
+ text-transform: uppercase;
389
+ letter-spacing: 0.05em;
390
+ }
391
+
392
+ .column-count {
393
+ background: var(--accent-lime);
394
+ color: var(--bg-dark);
395
+ padding: var(--spacing-xs) var(--spacing-sm);
396
+ border-radius: 2px;
397
+ font-weight: 700;
398
+ font-size: 0.8rem;
399
+ min-width: 32px;
400
+ text-align: center;
401
+ }
402
+
403
+ .column-cards {
404
+ flex: 1;
405
+ overflow-y: auto;
406
+ padding: var(--spacing-md);
407
+ display: flex;
408
+ flex-direction: column;
409
+ gap: var(--spacing-md);
410
+ transition: background-color var(--transition-base);
411
+ }
412
+
413
+ .kanban-column.collapsed .column-cards {
414
+ display: none;
415
+ }
416
+
417
+ .work-item-card {
418
+ background: var(--bg-darker);
419
+ border: 1px solid var(--border-subtle);
420
+ border-radius: 2px;
421
+ padding: var(--spacing-lg);
422
+ cursor: grab;
423
+ transition: all var(--transition-base);
424
+ flex-shrink: 0;
425
+ user-select: none;
426
+ }
427
+
428
+ .work-item-card:hover {
429
+ border-color: var(--accent-lime);
430
+ box-shadow: var(--shadow-md);
431
+ transform: translateY(-2px);
432
+ }
433
+
434
+ .work-item-card.dragging {
435
+ opacity: 0.5;
436
+ cursor: grabbing;
437
+ transform: rotate(-5deg) scale(0.95);
438
+ }
439
+
440
+ .card-header {
441
+ display: flex;
442
+ gap: var(--spacing-md);
443
+ margin-bottom: var(--spacing-md);
444
+ flex-wrap: wrap;
445
+ }
446
+
447
+ .work-item-type-badge {
448
+ padding: var(--spacing-xs) var(--spacing-sm);
449
+ border-radius: 2px;
450
+ font-size: 0.7rem;
451
+ font-weight: 600;
452
+ text-transform: uppercase;
453
+ letter-spacing: 0.05em;
454
+ border: 1px solid;
455
+ }
456
+
457
+ /* Type-specific colors */
458
+ .work-item-type-badge.type-feature {
459
+ background: rgba(205, 255, 0, 0.15);
460
+ color: var(--accent-lime);
461
+ border-color: var(--accent-lime);
462
+ }
463
+
464
+ .work-item-type-badge.type-bug {
465
+ background: rgba(239, 68, 68, 0.15);
466
+ color: #ef4444;
467
+ border-color: #ef4444;
468
+ }
469
+
470
+ .work-item-type-badge.type-spike {
471
+ background: rgba(251, 191, 36, 0.15);
472
+ color: #fbbf24;
473
+ border-color: #fbbf24;
474
+ }
475
+
476
+ .work-item-type-badge.type-task {
477
+ background: rgba(139, 92, 246, 0.15);
478
+ color: #8b5cf6;
479
+ border-color: #8b5cf6;
480
+ }
481
+
482
+ .work-item-type-badge.type-chore {
483
+ background: rgba(107, 114, 128, 0.15);
484
+ color: #9ca3af;
485
+ border-color: #9ca3af;
486
+ }
487
+
488
+ .work-item-type-badge.type-epic {
489
+ background: rgba(34, 197, 94, 0.15);
490
+ color: #22c55e;
491
+ border-color: #22c55e;
492
+ }
493
+
494
+ .priority-badge {
495
+ padding: var(--spacing-xs) var(--spacing-sm);
496
+ border-radius: 2px;
497
+ font-size: 0.7rem;
498
+ font-weight: 600;
499
+ text-transform: uppercase;
500
+ letter-spacing: 0.05em;
501
+ border: 1px solid;
502
+ }
503
+
504
+ .priority-badge.high {
505
+ background: rgba(239, 68, 68, 0.15);
506
+ color: var(--status-blocked);
507
+ border-color: var(--status-blocked);
508
+ }
509
+
510
+ .priority-badge.medium {
511
+ background: rgba(251, 191, 36, 0.15);
512
+ color: #FBBF24;
513
+ border-color: #FBBF24;
514
+ }
515
+
516
+ .priority-badge.low {
517
+ background: rgba(34, 197, 94, 0.15);
518
+ color: var(--status-success);
519
+ border-color: var(--status-success);
520
+ }
521
+
522
+ .card-title {
523
+ color: var(--text-primary);
524
+ font-size: 0.95rem;
525
+ margin: 0;
526
+ line-height: 1.4;
527
+ font-weight: 600;
528
+ }
529
+
530
+ .card-description {
531
+ color: var(--text-secondary);
532
+ font-size: 0.85rem;
533
+ margin: var(--spacing-md) 0 0 0;
534
+ line-height: 1.5;
535
+ }
536
+
537
+ .card-footer {
538
+ display: flex;
539
+ justify-content: space-between;
540
+ align-items: center;
541
+ margin-top: var(--spacing-md);
542
+ padding-top: var(--spacing-md);
543
+ border-top: 1px solid var(--border-subtle);
544
+ font-size: 0.8rem;
545
+ color: var(--text-secondary);
546
+ }
547
+
548
+ .work-item-id {
549
+ font-family: 'JetBrains Mono', monospace;
550
+ color: var(--accent-lime);
551
+ font-weight: 600;
552
+ }
553
+
554
+ .contributors {
555
+ display: flex;
556
+ gap: 4px;
557
+ align-items: center;
558
+ }
559
+
560
+ .contributor-tag {
561
+ display: inline-flex;
562
+ align-items: center;
563
+ justify-content: center;
564
+ width: 18px;
565
+ height: 18px;
566
+ border-radius: 50%;
567
+ background: rgba(255, 255, 255, 0.1);
568
+ color: var(--text-primary);
569
+ font-size: 0.6rem;
570
+ font-weight: 700;
571
+ border: 1px solid var(--border-subtle);
572
+ }
573
+
574
+ .contributor-tag.agent-claude-code, .contributor-tag.agent-claude {
575
+ background: rgba(200, 255, 0, 0.2);
576
+ color: #c8ff00;
577
+ border-color: rgba(200, 255, 0, 0.4);
578
+ }
579
+
580
+ .contributor-tag.agent-gemini {
581
+ background: rgba(74, 222, 128, 0.2);
582
+ color: #4ade80;
583
+ border-color: rgba(74, 222, 128, 0.4);
584
+ }
585
+
586
+ .assigned-to {
587
+ color: var(--text-muted);
588
+ margin-left: 4px;
589
+ }
590
+
591
+ .empty-column {
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ height: 100px;
596
+ color: var(--text-muted);
597
+ font-size: 0.85rem;
598
+ text-align: center;
599
+ padding: var(--spacing-lg);
600
+ }
601
+
602
+ @media (max-width: 1024px) {
603
+ .kanban-board {
604
+ grid-template-columns: repeat(2, 1fr);
605
+ }
606
+ }
607
+
608
+ @media (max-width: 768px) {
609
+ .kanban-board {
610
+ grid-template-columns: 1fr;
611
+ }
612
+ }
613
+ </style>
@@ -617,6 +617,30 @@ class TrackBuilder:
617
617
  Phase(id=f"phase-{i + 1}", name=phase_name, tasks=phase_tasks)
618
618
  )
619
619
 
620
+ # Persist features to database from plan phases
621
+ features_created = 0
622
+ if phases:
623
+ for phase in phases:
624
+ for task in phase.tasks:
625
+ # Generate feature ID from task description
626
+ feature_id = generate_id(node_type="feat", title=task.description)
627
+
628
+ # Insert feature into database
629
+ success = self.sdk._db.insert_feature(
630
+ feature_id=feature_id,
631
+ feature_type="task", # Tasks from tracks are features of type "task"
632
+ title=task.description,
633
+ status="todo", # All new tasks start as "todo"
634
+ priority=self._priority, # Inherit priority from track
635
+ assigned_to=None, # No assignment initially
636
+ track_id=track_id,
637
+ description=f"Task from {phase.name}",
638
+ steps_total=0,
639
+ tags=None,
640
+ )
641
+ if success:
642
+ features_created += 1
643
+
620
644
  if self._consolidated:
621
645
  # Single-file format: everything in one index.html
622
646
  track_file = self.sdk._directory / "tracks" / f"{track_id}.html"
@@ -677,6 +701,8 @@ class TrackBuilder:
677
701
  if self._plan_phases:
678
702
  total_tasks = sum(len(tasks) for _, tasks in self._plan_phases)
679
703
  print(f" - Plan with {len(self._plan_phases)} phases, {total_tasks} tasks")
704
+ if features_created > 0:
705
+ print(f" - Persisted {features_created} features to database")
680
706
 
681
707
  return track
682
708