claude-code-workflow 6.3.29 → 6.3.31

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.
@@ -13,7 +13,11 @@ var issueData = {
13
13
  selectedSolutionIssueId: null,
14
14
  statusFilter: 'all',
15
15
  searchQuery: '',
16
- viewMode: 'issues' // 'issues' | 'queue'
16
+ viewMode: 'issues', // 'issues' | 'queue'
17
+ // Search suggestions state
18
+ searchSuggestions: [],
19
+ showSuggestions: false,
20
+ selectedSuggestion: -1
17
21
  };
18
22
  var issueLoading = false;
19
23
  var issueDragState = {
@@ -21,6 +25,13 @@ var issueDragState = {
21
25
  groupId: null
22
26
  };
23
27
 
28
+ // Multi-queue state
29
+ var queueData = {
30
+ queues: [], // All queue index entries
31
+ activeQueueId: null, // Currently active queue
32
+ expandedQueueId: null // Queue showing execution groups
33
+ };
34
+
24
35
  // ========== Main Render Function ==========
25
36
  async function renderIssueManager() {
26
37
  const container = document.getElementById('mainContent');
@@ -36,7 +47,7 @@ async function renderIssueManager() {
36
47
  '</div>';
37
48
 
38
49
  // Load data
39
- await Promise.all([loadIssueData(), loadQueueData()]);
50
+ await Promise.all([loadIssueData(), loadQueueData(), loadAllQueues()]);
40
51
 
41
52
  // Render the main view
42
53
  renderIssueView();
@@ -82,6 +93,20 @@ async function loadQueueData() {
82
93
  }
83
94
  }
84
95
 
96
+ async function loadAllQueues() {
97
+ try {
98
+ const response = await fetch('/api/queue/history?path=' + encodeURIComponent(projectPath));
99
+ if (!response.ok) throw new Error('Failed to load queue history');
100
+ const data = await response.json();
101
+ queueData.queues = data.queues || [];
102
+ queueData.activeQueueId = data.active_queue_id;
103
+ } catch (err) {
104
+ console.error('Failed to load all queues:', err);
105
+ queueData.queues = [];
106
+ queueData.activeQueueId = null;
107
+ }
108
+ }
109
+
85
110
  async function loadIssueDetail(issueId) {
86
111
  try {
87
112
  const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath));
@@ -124,11 +149,24 @@ function renderIssueView() {
124
149
 
125
150
  if (issueData.searchQuery) {
126
151
  const query = issueData.searchQuery.toLowerCase();
127
- filteredIssues = filteredIssues.filter(i =>
128
- i.id.toLowerCase().includes(query) ||
129
- (i.title && i.title.toLowerCase().includes(query)) ||
130
- (i.context && i.context.toLowerCase().includes(query))
131
- );
152
+ filteredIssues = filteredIssues.filter(i => {
153
+ // Basic field search
154
+ const basicMatch =
155
+ i.id.toLowerCase().includes(query) ||
156
+ (i.title && i.title.toLowerCase().includes(query)) ||
157
+ (i.context && i.context.toLowerCase().includes(query));
158
+
159
+ if (basicMatch) return true;
160
+
161
+ // Search in solutions
162
+ if (i.solutions && i.solutions.length > 0) {
163
+ return i.solutions.some(sol =>
164
+ (sol.description && sol.description.toLowerCase().includes(query)) ||
165
+ (sol.approach && sol.approach.toLowerCase().includes(query))
166
+ );
167
+ }
168
+ return false;
169
+ });
132
170
  }
133
171
 
134
172
  container.innerHTML = `
@@ -273,12 +311,18 @@ function renderIssueListSection(issues) {
273
311
  id="issueSearchInput"
274
312
  placeholder="${t('issues.searchPlaceholder') || 'Search issues...'}"
275
313
  value="${issueData.searchQuery}"
276
- oninput="handleIssueSearch(this.value)" />
314
+ oninput="handleIssueSearch(this.value)"
315
+ onkeydown="handleSearchKeydown(event)"
316
+ onfocus="showSearchSuggestions()"
317
+ autocomplete="off" />
277
318
  ${issueData.searchQuery ? `
278
319
  <button class="issue-search-clear" onclick="clearIssueSearch()">
279
320
  <i data-lucide="x" class="w-3 h-3"></i>
280
321
  </button>
281
322
  ` : ''}
323
+ <div class="search-suggestions ${issueData.showSuggestions && issueData.searchSuggestions.length > 0 ? 'show' : ''}" id="searchSuggestions">
324
+ ${renderSearchSuggestions()}
325
+ </div>
282
326
  </div>
283
327
 
284
328
  <div class="issue-filters">
@@ -339,7 +383,7 @@ function renderIssueCard(issue) {
339
383
  <div class="issue-card ${isArchived ? 'archived' : ''}" onclick="openIssueDetail('${issue.id}'${isArchived ? ', true' : ''})">
340
384
  <div class="flex items-start justify-between mb-3">
341
385
  <div class="flex items-center gap-2">
342
- <span class="issue-id font-mono text-sm">${issue.id}</span>
386
+ <span class="issue-id font-mono text-sm">${highlightMatch(issue.id, issueData.searchQuery)}</span>
343
387
  <span class="issue-status ${statusColors[issue.status] || ''}">${issue.status || 'unknown'}</span>
344
388
  ${isArchived ? '<span class="issue-archived-badge">' + (t('issues.archived') || 'Archived') + '</span>' : ''}
345
389
  </div>
@@ -348,7 +392,7 @@ function renderIssueCard(issue) {
348
392
  </span>
349
393
  </div>
350
394
 
351
- <h3 class="issue-title text-foreground font-medium mb-2">${issue.title || issue.id}</h3>
395
+ <h3 class="issue-title text-foreground font-medium mb-2">${highlightMatch(issue.title || issue.id, issueData.searchQuery)}</h3>
352
396
 
353
397
  <div class="issue-meta flex items-center gap-4 text-sm text-muted-foreground">
354
398
  <span class="flex items-center gap-1">
@@ -398,25 +442,57 @@ async function filterIssuesByStatus(status) {
398
442
 
399
443
  // ========== Queue Section ==========
400
444
  function renderQueueSection() {
401
- const queue = issueData.queue;
402
- // Support both solution-level and task-level queues
403
- const queueItems = queue.solutions || queue.tasks || [];
404
- const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
405
- const metadata = queue._metadata || {};
406
-
407
- // Check if queue is empty
408
- if (queueItems.length === 0) {
445
+ const queues = queueData.queues || [];
446
+ const activeQueueId = queueData.activeQueueId;
447
+ const expandedQueueId = queueData.expandedQueueId;
448
+
449
+ // If a queue is expanded, show loading then load detail
450
+ if (expandedQueueId) {
451
+ // Show loading state first, then load async
452
+ setTimeout(() => loadAndRenderExpandedQueue(expandedQueueId), 0);
409
453
  return `
410
- <div class="queue-empty-container">
411
- <div class="queue-empty-toolbar">
412
- <button class="btn-secondary" onclick="showQueueHistoryModal()" title="${t('issues.queueHistory') || 'Queue History'}">
413
- <i data-lucide="history" class="w-4 h-4"></i>
414
- <span>${t('issues.history') || 'History'}</span>
454
+ <div id="queueExpandedWrapper" class="queue-expanded-wrapper">
455
+ <div class="queue-detail-header mb-4">
456
+ <button class="btn-secondary" onclick="queueData.expandedQueueId = null; renderIssueView();">
457
+ <i data-lucide="arrow-left" class="w-4 h-4"></i>
458
+ <span>${t('common.back') || 'Back'}</span>
415
459
  </button>
460
+ <div class="queue-detail-title">
461
+ <h3 class="font-mono text-lg">${escapeHtml(expandedQueueId)}</h3>
462
+ </div>
416
463
  </div>
464
+ <div id="expandedQueueContent" class="flex items-center justify-center py-8">
465
+ <i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
466
+ <span class="ml-2">${t('common.loading') || 'Loading...'}</span>
467
+ </div>
468
+ </div>
469
+ `;
470
+ }
471
+
472
+ // Show multi-queue cards view
473
+ return `
474
+ <!-- Queue Cards Header -->
475
+ <div class="queue-cards-header mb-4">
476
+ <div class="flex items-center gap-3">
477
+ <h3 class="text-lg font-semibold">${t('issues.executionQueues') || 'Execution Queues'}</h3>
478
+ <span class="text-sm text-muted-foreground">${queues.length} ${t('issues.queues') || 'queues'}</span>
479
+ </div>
480
+ <div class="flex items-center gap-2">
481
+ <button class="btn-secondary" onclick="loadAllQueues().then(() => renderIssueView())" title="${t('issues.refresh') || 'Refresh'}">
482
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
483
+ </button>
484
+ <button class="btn-primary" onclick="createExecutionQueue()">
485
+ <i data-lucide="plus" class="w-4 h-4"></i>
486
+ <span>${t('issues.createQueue') || 'Create Queue'}</span>
487
+ </button>
488
+ </div>
489
+ </div>
490
+
491
+ ${queues.length === 0 ? `
492
+ <div class="queue-empty-container">
417
493
  <div class="queue-empty">
418
494
  <i data-lucide="git-branch" class="w-16 h-16"></i>
419
- <p class="queue-empty-title">${t('issues.queueEmpty') || 'Queue is empty'}</p>
495
+ <p class="queue-empty-title">${t('issues.noQueues') || 'No queues found'}</p>
420
496
  <p class="queue-empty-hint">${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}</p>
421
497
  <button class="queue-create-btn" onclick="createExecutionQueue()">
422
498
  <i data-lucide="play" class="w-4 h-4"></i>
@@ -424,14 +500,509 @@ function renderQueueSection() {
424
500
  </button>
425
501
  </div>
426
502
  </div>
503
+ ` : `
504
+ <!-- Queue Cards Grid -->
505
+ <div class="queue-cards-grid">
506
+ ${queues.map(q => renderQueueCard(q, q.id === activeQueueId)).join('')}
507
+ </div>
508
+ `}
509
+ `;
510
+ }
511
+
512
+ function renderQueueCard(queue, isActive) {
513
+ const itemCount = queue.total_solutions || queue.total_tasks || 0;
514
+ const completedCount = queue.completed_solutions || queue.completed_tasks || 0;
515
+ const progressPercent = itemCount > 0 ? Math.round((completedCount / itemCount) * 100) : 0;
516
+ const issueCount = queue.issue_ids?.length || 0;
517
+ const statusClass = queue.status === 'merged' ? 'merged' : queue.status || '';
518
+ const safeQueueId = escapeHtml(queue.id || '');
519
+
520
+ return `
521
+ <div class="queue-card ${isActive ? 'active' : ''} ${statusClass}" onclick="toggleQueueExpand('${safeQueueId}')">
522
+ <div class="queue-card-header">
523
+ <span class="queue-card-id font-mono">${safeQueueId}</span>
524
+ <div class="queue-card-badges">
525
+ ${isActive ? '<span class="queue-active-badge">Active</span>' : ''}
526
+ <span class="queue-status-badge ${statusClass}">${queue.status || 'unknown'}</span>
527
+ </div>
528
+ </div>
529
+
530
+ <div class="queue-card-stats">
531
+ <div class="progress-bar">
532
+ <div class="progress-fill ${queue.status === 'completed' ? 'completed' : ''}" style="width: ${progressPercent}%"></div>
533
+ </div>
534
+ <div class="queue-card-progress">
535
+ <span>${completedCount}/${itemCount} ${queue.total_solutions ? 'solutions' : 'tasks'}</span>
536
+ <span class="text-muted-foreground">${progressPercent}%</span>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="queue-card-meta">
541
+ <span class="flex items-center gap-1">
542
+ <i data-lucide="layers" class="w-3 h-3"></i>
543
+ ${issueCount} issues
544
+ </span>
545
+ <span class="flex items-center gap-1">
546
+ <i data-lucide="calendar" class="w-3 h-3"></i>
547
+ ${queue.created_at ? new Date(queue.created_at).toLocaleDateString() : 'N/A'}
548
+ </span>
549
+ </div>
550
+
551
+ <div class="queue-card-actions" onclick="event.stopPropagation()">
552
+ <button class="btn-sm" onclick="toggleQueueExpand('${safeQueueId}')" title="View details">
553
+ <i data-lucide="eye" class="w-3 h-3"></i>
554
+ </button>
555
+ ${!isActive && queue.status !== 'merged' ? `
556
+ <button class="btn-sm btn-primary" onclick="activateQueue('${safeQueueId}')" title="Set as active">
557
+ <i data-lucide="check-circle" class="w-3 h-3"></i>
558
+ </button>
559
+ ` : ''}
560
+ ${queue.status !== 'merged' ? `
561
+ <button class="btn-sm" onclick="showMergeQueueModal('${safeQueueId}')" title="Merge into another queue">
562
+ <i data-lucide="git-merge" class="w-3 h-3"></i>
563
+ </button>
564
+ ` : ''}
565
+ <button class="btn-sm btn-danger" onclick="confirmDeleteQueue('${safeQueueId}')" title="${t('issues.deleteQueue') || 'Delete queue'}">
566
+ <i data-lucide="trash-2" class="w-3 h-3"></i>
567
+ </button>
568
+ </div>
569
+ </div>
570
+ `;
571
+ }
572
+
573
+ function toggleQueueExpand(queueId) {
574
+ if (queueData.expandedQueueId === queueId) {
575
+ queueData.expandedQueueId = null;
576
+ } else {
577
+ queueData.expandedQueueId = queueId;
578
+ }
579
+ renderIssueView();
580
+ }
581
+
582
+ async function activateQueue(queueId) {
583
+ try {
584
+ const response = await fetch('/api/queue/switch?path=' + encodeURIComponent(projectPath), {
585
+ method: 'POST',
586
+ headers: { 'Content-Type': 'application/json' },
587
+ body: JSON.stringify({ queueId })
588
+ });
589
+ const result = await response.json();
590
+ if (result.success) {
591
+ showNotification(t('issues.queueActivated') || 'Queue activated: ' + queueId, 'success');
592
+ await Promise.all([loadQueueData(), loadAllQueues()]);
593
+ renderIssueView();
594
+ } else {
595
+ showNotification(result.error || 'Failed to activate queue', 'error');
596
+ }
597
+ } catch (err) {
598
+ console.error('Failed to activate queue:', err);
599
+ showNotification('Failed to activate queue', 'error');
600
+ }
601
+ }
602
+
603
+ async function deactivateQueue(queueId) {
604
+ try {
605
+ const response = await fetch('/api/queue/deactivate?path=' + encodeURIComponent(projectPath), {
606
+ method: 'POST',
607
+ headers: { 'Content-Type': 'application/json' },
608
+ body: JSON.stringify({ queueId })
609
+ });
610
+ const result = await response.json();
611
+ if (result.success) {
612
+ showNotification(t('issues.queueDeactivated') || 'Queue deactivated', 'success');
613
+ queueData.activeQueueId = null;
614
+ await Promise.all([loadQueueData(), loadAllQueues()]);
615
+ renderIssueView();
616
+ } else {
617
+ showNotification(result.error || 'Failed to deactivate queue', 'error');
618
+ }
619
+ } catch (err) {
620
+ console.error('Failed to deactivate queue:', err);
621
+ showNotification('Failed to deactivate queue', 'error');
622
+ }
623
+ }
624
+
625
+ function confirmDeleteQueue(queueId) {
626
+ const msg = t('issues.confirmDeleteQueue') || 'Are you sure you want to delete this queue? This action cannot be undone.';
627
+ if (confirm(msg)) {
628
+ deleteQueue(queueId);
629
+ }
630
+ }
631
+
632
+ async function deleteQueue(queueId) {
633
+ try {
634
+ const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath), {
635
+ method: 'DELETE'
636
+ });
637
+ const result = await response.json();
638
+ if (result.success) {
639
+ showNotification(t('issues.queueDeleted') || 'Queue deleted successfully', 'success');
640
+ queueData.expandedQueueId = null;
641
+ await Promise.all([loadQueueData(), loadAllQueues()]);
642
+ renderIssueView();
643
+ } else {
644
+ showNotification(result.error || 'Failed to delete queue', 'error');
645
+ }
646
+ } catch (err) {
647
+ console.error('Failed to delete queue:', err);
648
+ showNotification('Failed to delete queue', 'error');
649
+ }
650
+ }
651
+
652
+ async function renderExpandedQueueView(queueId) {
653
+ const safeQueueId = escapeHtml(queueId || '');
654
+ // Fetch queue detail
655
+ let queue;
656
+ try {
657
+ const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath));
658
+ queue = await response.json();
659
+ if (queue.error) throw new Error(queue.error);
660
+ } catch (err) {
661
+ return `
662
+ <div class="queue-error">
663
+ <button class="btn-secondary mb-4" onclick="queueData.expandedQueueId = null; renderIssueView();">
664
+ <i data-lucide="arrow-left" class="w-4 h-4"></i> Back
665
+ </button>
666
+ <p class="text-red-500">Failed to load queue: ${escapeHtml(err.message)}</p>
667
+ </div>
668
+ `;
669
+ }
670
+
671
+ const queueItems = queue.solutions || queue.tasks || [];
672
+ const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
673
+ const metadata = queue._metadata || {};
674
+ const isActive = queueId === queueData.activeQueueId;
675
+
676
+ // Group items by execution_group
677
+ const groupMap = {};
678
+ queueItems.forEach(item => {
679
+ const groupId = item.execution_group || 'default';
680
+ if (!groupMap[groupId]) groupMap[groupId] = [];
681
+ groupMap[groupId].push(item);
682
+ });
683
+
684
+ const groups = queue.execution_groups || Object.keys(groupMap).map(groupId => ({
685
+ id: groupId,
686
+ type: groupId.startsWith('P') ? 'parallel' : 'sequential',
687
+ solution_count: groupMap[groupId]?.length || 0
688
+ }));
689
+ const groupedItems = queue.grouped_items || groupMap;
690
+
691
+ return `
692
+ <!-- Back Button & Queue Header -->
693
+ <div class="queue-detail-header mb-4">
694
+ <button class="btn-secondary" onclick="queueData.expandedQueueId = null; renderIssueView();">
695
+ <i data-lucide="arrow-left" class="w-4 h-4"></i>
696
+ <span>${t('common.back') || 'Back'}</span>
697
+ </button>
698
+ <div class="queue-detail-title">
699
+ <h3 class="font-mono text-lg">${escapeHtml(queue.id || queueId)}</h3>
700
+ <div class="flex items-center gap-2">
701
+ ${isActive ? '<span class="queue-active-badge">Active</span>' : ''}
702
+ <span class="queue-status-badge ${escapeHtml(queue.status || '')}">${escapeHtml(queue.status || 'unknown')}</span>
703
+ </div>
704
+ </div>
705
+ <div class="queue-detail-actions">
706
+ ${!isActive && queue.status !== 'merged' ? `
707
+ <button class="btn-primary" onclick="activateQueue('${safeQueueId}')">
708
+ <i data-lucide="check-circle" class="w-4 h-4"></i>
709
+ <span>${t('issues.activate') || 'Activate'}</span>
710
+ </button>
711
+ ` : ''}
712
+ ${isActive ? `
713
+ <button class="btn-secondary btn-warning" onclick="deactivateQueue('${safeQueueId}')">
714
+ <i data-lucide="x-circle" class="w-4 h-4"></i>
715
+ <span>${t('issues.deactivate') || 'Deactivate'}</span>
716
+ </button>
717
+ ` : ''}
718
+ <button class="btn-secondary" onclick="refreshExpandedQueue('${safeQueueId}')">
719
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
720
+ </button>
721
+ </div>
722
+ </div>
723
+
724
+ <!-- Queue Stats -->
725
+ <div class="queue-stats-grid mb-4">
726
+ <div class="queue-stat-card">
727
+ <span class="queue-stat-value">${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}</span>
728
+ <span class="queue-stat-label">${isSolutionLevel ? 'Solutions' : 'Tasks'}</span>
729
+ </div>
730
+ <div class="queue-stat-card pending">
731
+ <span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
732
+ <span class="queue-stat-label">Pending</span>
733
+ </div>
734
+ <div class="queue-stat-card executing">
735
+ <span class="queue-stat-value">${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length}</span>
736
+ <span class="queue-stat-label">Executing</span>
737
+ </div>
738
+ <div class="queue-stat-card completed">
739
+ <span class="queue-stat-value">${isSolutionLevel ? (metadata.completed_solutions || 0) : (metadata.completed_tasks || queueItems.filter(i => i.status === 'completed').length)}</span>
740
+ <span class="queue-stat-label">Completed</span>
741
+ </div>
742
+ <div class="queue-stat-card failed">
743
+ <span class="queue-stat-value">${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length}</span>
744
+ <span class="queue-stat-label">Failed</span>
745
+ </div>
746
+ </div>
747
+
748
+ <div class="queue-info mb-4">
749
+ <p class="text-sm text-muted-foreground">
750
+ <i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
751
+ ${t('issues.reorderHint') || 'Drag items within a group to reorder. Click item to view details.'}
752
+ </p>
753
+ </div>
754
+
755
+ <div class="queue-timeline">
756
+ ${groups.map(group => renderQueueGroupWithDelete(group, groupedItems[group.id] || groupMap[group.id] || [], queueId)).join('')}
757
+ </div>
758
+
759
+ ${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
760
+ `;
761
+ }
762
+
763
+ // Async loader for expanded queue view - renders into DOM container
764
+ async function loadAndRenderExpandedQueue(queueId) {
765
+ const wrapper = document.getElementById('queueExpandedWrapper');
766
+ if (!wrapper) return;
767
+
768
+ try {
769
+ const html = await renderExpandedQueueView(queueId);
770
+ wrapper.innerHTML = html;
771
+ // Re-init icons and drag-drop after DOM update
772
+ if (window.lucide) {
773
+ window.lucide.createIcons();
774
+ }
775
+ // Initialize drag-drop for queue items
776
+ initQueueDragDrop();
777
+ } catch (err) {
778
+ console.error('Failed to load expanded queue:', err);
779
+ wrapper.innerHTML = `
780
+ <div class="text-center py-8 text-red-500">
781
+ <i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
782
+ <p>Failed to load queue: ${escapeHtml(err.message || 'Unknown error')}</p>
783
+ <button class="btn-secondary mt-4" onclick="queueData.expandedQueueId = null; renderIssueView();">
784
+ <i data-lucide="arrow-left" class="w-4 h-4"></i> Back
785
+ </button>
786
+ </div>
427
787
  `;
788
+ if (window.lucide) {
789
+ window.lucide.createIcons();
790
+ }
791
+ }
792
+ }
793
+
794
+ function renderQueueGroupWithDelete(group, items, queueId) {
795
+ const isParallel = group.type === 'parallel';
796
+ const itemCount = group.solution_count || group.task_count || items.length;
797
+ const itemLabel = group.solution_count ? 'solutions' : 'tasks';
798
+
799
+ return `
800
+ <div class="queue-group" data-group-id="${group.id}">
801
+ <div class="queue-group-header">
802
+ <div class="queue-group-type ${isParallel ? 'parallel' : 'sequential'}">
803
+ <i data-lucide="${isParallel ? 'git-merge' : 'arrow-right'}" class="w-4 h-4"></i>
804
+ ${group.id} (${isParallel ? 'Parallel' : 'Sequential'})
805
+ </div>
806
+ <span class="text-sm text-muted-foreground">${itemCount} ${itemLabel}</span>
807
+ </div>
808
+ <div class="queue-items ${isParallel ? 'parallel' : 'sequential'}">
809
+ ${items.map((item, idx) => renderQueueItemWithDelete(item, idx, items.length, queueId)).join('')}
810
+ </div>
811
+ </div>
812
+ `;
813
+ }
814
+
815
+ function renderQueueItemWithDelete(item, index, total, queueId) {
816
+ const statusColors = {
817
+ pending: '',
818
+ ready: 'ready',
819
+ executing: 'executing',
820
+ completed: 'completed',
821
+ failed: 'failed',
822
+ blocked: 'blocked'
823
+ };
824
+
825
+ const isSolutionItem = item.task_count !== undefined;
826
+ const safeItemId = escapeHtml(item.item_id || '');
827
+ const safeIssueId = escapeHtml(item.issue_id || '');
828
+ const safeQueueId = escapeHtml(queueId || '');
829
+ const safeSolutionId = escapeHtml(item.solution_id || '');
830
+ const safeTaskId = escapeHtml(item.task_id || '-');
831
+ const safeFilesTouched = item.files_touched ? escapeHtml(item.files_touched.join(', ')) : '';
832
+ const safeDependsOn = item.depends_on ? escapeHtml(item.depends_on.join(', ')) : '';
833
+
834
+ return `
835
+ <div class="queue-item ${statusColors[item.status] || ''}"
836
+ draggable="true"
837
+ data-item-id="${safeItemId}"
838
+ data-group-id="${escapeHtml(item.execution_group || '')}"
839
+ onclick="openQueueItemDetail('${safeItemId}')">
840
+ <span class="queue-item-id font-mono text-xs">${safeItemId}</span>
841
+ <span class="queue-item-issue text-xs text-muted-foreground">${safeIssueId}</span>
842
+ ${isSolutionItem ? `
843
+ <span class="queue-item-solution text-sm" title="${safeSolutionId}">
844
+ <i data-lucide="package" class="w-3 h-3 inline mr-1"></i>
845
+ ${item.task_count} tasks
846
+ </span>
847
+ ${item.files_touched && item.files_touched.length > 0 ? `
848
+ <span class="queue-item-files text-xs text-muted-foreground" title="${safeFilesTouched}">
849
+ <i data-lucide="file" class="w-3 h-3"></i>
850
+ ${item.files_touched.length}
851
+ </span>
852
+ ` : ''}
853
+ ` : `
854
+ <span class="queue-item-task text-sm">${safeTaskId}</span>
855
+ `}
856
+ <span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
857
+ <i data-lucide="arrow-up" class="w-3 h-3"></i>
858
+ </span>
859
+ ${item.depends_on && item.depends_on.length > 0 ? `
860
+ <span class="queue-item-deps text-xs text-muted-foreground" title="Depends on: ${safeDependsOn}">
861
+ <i data-lucide="link" class="w-3 h-3"></i>
862
+ </span>
863
+ ` : ''}
864
+ <button class="queue-item-delete btn-icon" onclick="event.stopPropagation(); deleteQueueItem('${safeQueueId}', '${safeItemId}')" title="Delete item">
865
+ <i data-lucide="trash-2" class="w-3 h-3"></i>
866
+ </button>
867
+ </div>
868
+ `;
869
+ }
870
+
871
+ async function deleteQueueItem(queueId, itemId) {
872
+ if (!confirm('Delete this item from queue?')) return;
873
+
874
+ try {
875
+ const response = await fetch('/api/queue/' + queueId + '/item/' + encodeURIComponent(itemId) + '?path=' + encodeURIComponent(projectPath), {
876
+ method: 'DELETE'
877
+ });
878
+ const result = await response.json();
879
+
880
+ if (result.success) {
881
+ showNotification('Item deleted from queue', 'success');
882
+ await Promise.all([loadQueueData(), loadAllQueues()]);
883
+ renderIssueView();
884
+ } else {
885
+ showNotification(result.error || 'Failed to delete item', 'error');
886
+ }
887
+ } catch (err) {
888
+ console.error('Failed to delete queue item:', err);
889
+ showNotification('Failed to delete item', 'error');
890
+ }
891
+ }
892
+
893
+ async function refreshExpandedQueue(queueId) {
894
+ await Promise.all([loadQueueData(), loadAllQueues()]);
895
+ renderIssueView();
896
+ }
897
+
898
+ // ========== Queue Merge Modal ==========
899
+ function showMergeQueueModal(sourceQueueId) {
900
+ let modal = document.getElementById('mergeQueueModal');
901
+ if (!modal) {
902
+ modal = document.createElement('div');
903
+ modal.id = 'mergeQueueModal';
904
+ modal.className = 'issue-modal';
905
+ document.body.appendChild(modal);
906
+ }
907
+
908
+ const otherQueues = queueData.queues.filter(q =>
909
+ q.id !== sourceQueueId && q.status !== 'merged'
910
+ );
911
+
912
+ const safeSourceId = escapeHtml(sourceQueueId || '');
913
+
914
+ modal.innerHTML = `
915
+ <div class="issue-modal-backdrop" onclick="hideMergeQueueModal()"></div>
916
+ <div class="issue-modal-content" style="max-width: 500px;">
917
+ <div class="issue-modal-header">
918
+ <h3><i data-lucide="git-merge" class="w-5 h-5 inline mr-2"></i>Merge Queue</h3>
919
+ <button class="btn-icon" onclick="hideMergeQueueModal()">
920
+ <i data-lucide="x" class="w-5 h-5"></i>
921
+ </button>
922
+ </div>
923
+ <div class="issue-modal-body">
924
+ <p class="mb-4">Merge <strong class="font-mono">${safeSourceId}</strong> into another queue:</p>
925
+ ${otherQueues.length === 0 ? `
926
+ <p class="text-muted-foreground text-center py-4">No other queues available for merging</p>
927
+ ` : `
928
+ <div class="form-group">
929
+ <label>Target Queue</label>
930
+ <select id="targetQueueSelect" class="w-full">
931
+ ${otherQueues.map(q => `
932
+ <option value="${escapeHtml(q.id)}">${escapeHtml(q.id)} (${q.total_solutions || q.total_tasks || 0} items)</option>
933
+ `).join('')}
934
+ </select>
935
+ </div>
936
+ <p class="text-sm text-muted-foreground mt-2">
937
+ <i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
938
+ Items from source queue will be appended to target queue. Source queue will be marked as "merged".
939
+ </p>
940
+ `}
941
+ </div>
942
+ <div class="issue-modal-footer">
943
+ <button class="btn-secondary" onclick="hideMergeQueueModal()">Cancel</button>
944
+ ${otherQueues.length > 0 ? `
945
+ <button class="btn-primary" onclick="executeQueueMerge('${safeSourceId}')">
946
+ <i data-lucide="git-merge" class="w-4 h-4"></i>
947
+ Merge
948
+ </button>
949
+ ` : ''}
950
+ </div>
951
+ </div>
952
+ `;
953
+
954
+ modal.classList.remove('hidden');
955
+ lucide.createIcons();
956
+ }
957
+
958
+ function hideMergeQueueModal() {
959
+ const modal = document.getElementById('mergeQueueModal');
960
+ if (modal) {
961
+ modal.classList.add('hidden');
962
+ }
963
+ }
964
+
965
+ async function executeQueueMerge(sourceQueueId) {
966
+ const targetQueueId = document.getElementById('targetQueueSelect')?.value;
967
+ if (!targetQueueId) return;
968
+
969
+ try {
970
+ const response = await fetch('/api/queue/merge?path=' + encodeURIComponent(projectPath), {
971
+ method: 'POST',
972
+ headers: { 'Content-Type': 'application/json' },
973
+ body: JSON.stringify({ sourceQueueId, targetQueueId })
974
+ });
975
+ const result = await response.json();
976
+
977
+ if (result.success) {
978
+ showNotification('Merged ' + result.mergedItemCount + ' items into ' + targetQueueId, 'success');
979
+ hideMergeQueueModal();
980
+ queueData.expandedQueueId = null;
981
+ await Promise.all([loadQueueData(), loadAllQueues()]);
982
+ renderIssueView();
983
+ } else {
984
+ showNotification(result.error || 'Failed to merge queues', 'error');
985
+ }
986
+ } catch (err) {
987
+ console.error('Failed to merge queues:', err);
988
+ showNotification('Failed to merge queues', 'error');
989
+ }
990
+ }
991
+
992
+ // ========== Legacy Queue Render (for backward compatibility) ==========
993
+ function renderLegacyQueueSection() {
994
+ const queue = issueData.queue;
995
+ const queueItems = queue.solutions || queue.tasks || [];
996
+ const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
997
+ const metadata = queue._metadata || {};
998
+
999
+ if (queueItems.length === 0) {
1000
+ return `<div class="queue-empty"><p>Queue is empty</p></div>`;
428
1001
  }
429
1002
 
430
- // Group items by execution_group or treat all as single group
431
1003
  const groups = queue.execution_groups || [];
432
1004
  let groupedItems = queue.grouped_items || {};
433
1005
 
434
- // If no execution_groups, create a default grouping from queue items
435
1006
  if (groups.length === 0 && queueItems.length > 0) {
436
1007
  const groupMap = {};
437
1008
  queueItems.forEach(item => {
@@ -442,7 +1013,6 @@ function renderQueueSection() {
442
1013
  groupMap[groupId].push(item);
443
1014
  });
444
1015
 
445
- // Create synthetic groups
446
1016
  const syntheticGroups = Object.keys(groupMap).map(groupId => ({
447
1017
  id: groupId,
448
1018
  type: 'sequential',
@@ -450,7 +1020,6 @@ function renderQueueSection() {
450
1020
  }));
451
1021
 
452
1022
  return `
453
- <!-- Queue Header -->
454
1023
  <div class="queue-toolbar mb-4">
455
1024
  <div class="queue-stats">
456
1025
  <div class="queue-info-card">
@@ -866,6 +1435,23 @@ function renderIssueDetailPanel(issue) {
866
1435
  `).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noTasks') || 'No tasks') + '</p>'}
867
1436
  </div>
868
1437
  </div>
1438
+
1439
+ <!-- Actions -->
1440
+ <div class="detail-section issue-detail-actions">
1441
+ <label class="detail-label">${t('issues.actions') || 'Actions'}</label>
1442
+ <div class="flex gap-2 flex-wrap">
1443
+ ${!issue._isArchived ? `
1444
+ <button class="btn-secondary btn-sm" onclick="confirmArchiveIssue('${issue.id}')">
1445
+ <i data-lucide="archive" class="w-4 h-4"></i>
1446
+ ${t('issues.archive') || 'Archive'}
1447
+ </button>
1448
+ ` : ''}
1449
+ <button class="btn-secondary btn-sm btn-danger" onclick="confirmDeleteIssue('${issue.id}', ${issue._isArchived || false})">
1450
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
1451
+ ${t('issues.delete') || 'Delete'}
1452
+ </button>
1453
+ </div>
1454
+ </div>
869
1455
  </div>
870
1456
  `;
871
1457
 
@@ -880,6 +1466,67 @@ function closeIssueDetail() {
880
1466
  issueData.selectedIssue = null;
881
1467
  }
882
1468
 
1469
+ // ========== Issue Delete & Archive ==========
1470
+ function confirmDeleteIssue(issueId, isArchived) {
1471
+ const msg = t('issues.confirmDeleteIssue') || 'Are you sure you want to delete this issue? This action cannot be undone.';
1472
+ if (confirm(msg)) {
1473
+ deleteIssue(issueId, isArchived);
1474
+ }
1475
+ }
1476
+
1477
+ async function deleteIssue(issueId, isArchived) {
1478
+ try {
1479
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
1480
+ method: 'DELETE'
1481
+ });
1482
+ const result = await response.json();
1483
+ if (result.success) {
1484
+ showNotification(t('issues.issueDeleted') || 'Issue deleted successfully', 'success');
1485
+ closeIssueDetail();
1486
+ if (isArchived) {
1487
+ issueData.historyIssues = issueData.historyIssues.filter(i => i.id !== issueId);
1488
+ } else {
1489
+ issueData.issues = issueData.issues.filter(i => i.id !== issueId);
1490
+ }
1491
+ renderIssueView();
1492
+ updateIssueBadge();
1493
+ } else {
1494
+ showNotification(result.error || 'Failed to delete issue', 'error');
1495
+ }
1496
+ } catch (err) {
1497
+ console.error('Failed to delete issue:', err);
1498
+ showNotification('Failed to delete issue', 'error');
1499
+ }
1500
+ }
1501
+
1502
+ function confirmArchiveIssue(issueId) {
1503
+ const msg = t('issues.confirmArchiveIssue') || 'Archive this issue? It will be moved to history.';
1504
+ if (confirm(msg)) {
1505
+ archiveIssue(issueId);
1506
+ }
1507
+ }
1508
+
1509
+ async function archiveIssue(issueId) {
1510
+ try {
1511
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/archive?path=' + encodeURIComponent(projectPath), {
1512
+ method: 'POST'
1513
+ });
1514
+ const result = await response.json();
1515
+ if (result.success) {
1516
+ showNotification(t('issues.issueArchived') || 'Issue archived successfully', 'success');
1517
+ closeIssueDetail();
1518
+ await loadIssueData();
1519
+ renderIssueView();
1520
+ updateIssueBadge();
1521
+ } else {
1522
+ showNotification(result.error || 'Failed to archive issue', 'error');
1523
+ }
1524
+ } catch (err) {
1525
+ console.error('Failed to archive issue:', err);
1526
+ showNotification('Failed to archive issue', 'error');
1527
+ }
1528
+ }
1529
+
883
1530
  function toggleSolutionExpand(solId) {
884
1531
  const el = document.getElementById('solution-' + solId);
885
1532
  if (el) {
@@ -1234,6 +1881,20 @@ function escapeHtml(text) {
1234
1881
  return div.innerHTML;
1235
1882
  }
1236
1883
 
1884
+ // Helper: escape regex special characters
1885
+ function escapeRegex(str) {
1886
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1887
+ }
1888
+
1889
+ // Helper: highlight matching text in search results
1890
+ function highlightMatch(text, query) {
1891
+ if (!text || !query) return escapeHtml(text || '');
1892
+ const escaped = escapeHtml(text);
1893
+ const escapedQuery = escapeRegex(escapeHtml(query));
1894
+ const regex = new RegExp(`(${escapedQuery})`, 'gi');
1895
+ return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
1896
+ }
1897
+
1237
1898
  function openQueueItemDetail(itemId) {
1238
1899
  // Support both solution-level and task-level queues
1239
1900
  const items = issueData.queue.solutions || issueData.queue.tasks || [];
@@ -1366,16 +2027,178 @@ async function updateTaskStatus(issueId, taskId, status) {
1366
2027
  }
1367
2028
 
1368
2029
  // ========== Search Functions ==========
2030
+ var searchDebounceTimer = null;
2031
+
1369
2032
  function handleIssueSearch(value) {
1370
2033
  issueData.searchQuery = value;
1371
- renderIssueView();
2034
+
2035
+ // Update suggestions immediately (no debounce for dropdown)
2036
+ updateSearchSuggestions(value);
2037
+ issueData.showSuggestions = value.length > 0;
2038
+ issueData.selectedSuggestion = -1;
2039
+ updateSuggestionsDropdown();
2040
+
2041
+ // Clear previous timer
2042
+ if (searchDebounceTimer) {
2043
+ clearTimeout(searchDebounceTimer);
2044
+ }
2045
+
2046
+ // 300ms debounce for full re-render to prevent freeze on rapid input
2047
+ searchDebounceTimer = setTimeout(() => {
2048
+ renderIssueView();
2049
+ // Restore input focus and cursor position
2050
+ const input = document.getElementById('issueSearchInput');
2051
+ if (input) {
2052
+ input.focus();
2053
+ input.setSelectionRange(value.length, value.length);
2054
+ }
2055
+ }, 300);
1372
2056
  }
1373
2057
 
1374
2058
  function clearIssueSearch() {
2059
+ if (searchDebounceTimer) {
2060
+ clearTimeout(searchDebounceTimer);
2061
+ }
1375
2062
  issueData.searchQuery = '';
2063
+ issueData.showSuggestions = false;
2064
+ issueData.searchSuggestions = [];
2065
+ issueData.selectedSuggestion = -1;
1376
2066
  renderIssueView();
1377
2067
  }
1378
2068
 
2069
+ // Update search suggestions based on query
2070
+ function updateSearchSuggestions(query) {
2071
+ if (!query || query.length < 1) {
2072
+ issueData.searchSuggestions = [];
2073
+ return;
2074
+ }
2075
+
2076
+ const q = query.toLowerCase();
2077
+ const allIssues = [...issueData.issues, ...issueData.historyIssues];
2078
+
2079
+ // Find matching issues (max 6)
2080
+ issueData.searchSuggestions = allIssues
2081
+ .filter(issue => {
2082
+ const idMatch = issue.id.toLowerCase().includes(q);
2083
+ const titleMatch = issue.title && issue.title.toLowerCase().includes(q);
2084
+ const contextMatch = issue.context && issue.context.toLowerCase().includes(q);
2085
+ const solutionMatch = issue.solutions && issue.solutions.some(sol =>
2086
+ (sol.description && sol.description.toLowerCase().includes(q)) ||
2087
+ (sol.approach && sol.approach.toLowerCase().includes(q))
2088
+ );
2089
+ return idMatch || titleMatch || contextMatch || solutionMatch;
2090
+ })
2091
+ .slice(0, 6);
2092
+ }
2093
+
2094
+ // Render search suggestions dropdown
2095
+ function renderSearchSuggestions() {
2096
+ if (!issueData.searchSuggestions || issueData.searchSuggestions.length === 0) {
2097
+ return '';
2098
+ }
2099
+
2100
+ return issueData.searchSuggestions.map((issue, index) => `
2101
+ <div class="search-suggestion-item ${index === issueData.selectedSuggestion ? 'selected' : ''}"
2102
+ onclick="selectSuggestion(${index})"
2103
+ onmouseenter="issueData.selectedSuggestion = ${index}">
2104
+ <div class="suggestion-id">${highlightMatch(issue.id, issueData.searchQuery)}</div>
2105
+ <div class="suggestion-title">${highlightMatch(issue.title || issue.id, issueData.searchQuery)}</div>
2106
+ </div>
2107
+ `).join('');
2108
+ }
2109
+
2110
+ // Show search suggestions
2111
+ function showSearchSuggestions() {
2112
+ if (issueData.searchQuery) {
2113
+ updateSearchSuggestions(issueData.searchQuery);
2114
+ issueData.showSuggestions = true;
2115
+ updateSuggestionsDropdown();
2116
+ }
2117
+ }
2118
+
2119
+ // Hide search suggestions
2120
+ function hideSearchSuggestions() {
2121
+ issueData.showSuggestions = false;
2122
+ issueData.selectedSuggestion = -1;
2123
+ const dropdown = document.getElementById('searchSuggestions');
2124
+ if (dropdown) {
2125
+ dropdown.classList.remove('show');
2126
+ }
2127
+ }
2128
+
2129
+ // Update suggestions dropdown without full re-render
2130
+ function updateSuggestionsDropdown() {
2131
+ const dropdown = document.getElementById('searchSuggestions');
2132
+ if (dropdown) {
2133
+ dropdown.innerHTML = renderSearchSuggestions();
2134
+ if (issueData.showSuggestions && issueData.searchSuggestions.length > 0) {
2135
+ dropdown.classList.add('show');
2136
+ } else {
2137
+ dropdown.classList.remove('show');
2138
+ }
2139
+ }
2140
+ }
2141
+
2142
+ // Select a suggestion
2143
+ function selectSuggestion(index) {
2144
+ const issue = issueData.searchSuggestions[index];
2145
+ if (issue) {
2146
+ hideSearchSuggestions();
2147
+ openIssueDetail(issue.id, issue._isArchived);
2148
+ }
2149
+ }
2150
+
2151
+ // Handle keyboard navigation in search
2152
+ function handleSearchKeydown(event) {
2153
+ const suggestions = issueData.searchSuggestions || [];
2154
+
2155
+ if (!issueData.showSuggestions || suggestions.length === 0) {
2156
+ // If Enter and no suggestions, just search
2157
+ if (event.key === 'Enter') {
2158
+ hideSearchSuggestions();
2159
+ }
2160
+ return;
2161
+ }
2162
+
2163
+ switch (event.key) {
2164
+ case 'ArrowDown':
2165
+ event.preventDefault();
2166
+ issueData.selectedSuggestion = Math.min(
2167
+ issueData.selectedSuggestion + 1,
2168
+ suggestions.length - 1
2169
+ );
2170
+ updateSuggestionsDropdown();
2171
+ break;
2172
+
2173
+ case 'ArrowUp':
2174
+ event.preventDefault();
2175
+ issueData.selectedSuggestion = Math.max(issueData.selectedSuggestion - 1, -1);
2176
+ updateSuggestionsDropdown();
2177
+ break;
2178
+
2179
+ case 'Enter':
2180
+ event.preventDefault();
2181
+ if (issueData.selectedSuggestion >= 0) {
2182
+ selectSuggestion(issueData.selectedSuggestion);
2183
+ } else {
2184
+ hideSearchSuggestions();
2185
+ }
2186
+ break;
2187
+
2188
+ case 'Escape':
2189
+ hideSearchSuggestions();
2190
+ break;
2191
+ }
2192
+ }
2193
+
2194
+ // Close suggestions when clicking outside
2195
+ document.addEventListener('click', function(event) {
2196
+ const searchContainer = document.querySelector('.issue-search');
2197
+ if (searchContainer && !searchContainer.contains(event.target)) {
2198
+ hideSearchSuggestions();
2199
+ }
2200
+ });
2201
+
1379
2202
  // ========== Create Issue Modal ==========
1380
2203
  function generateIssueId() {
1381
2204
  // Generate unique ID: ISSUE-YYYYMMDD-XXX format