claude-code-workflow 6.3.4 → 6.3.6

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 (111) hide show
  1. package/.claude/agents/issue-plan-agent.md +859 -0
  2. package/.claude/agents/issue-queue-agent.md +702 -0
  3. package/.claude/commands/issue/execute.md +453 -0
  4. package/.claude/commands/issue/manage.md +865 -0
  5. package/.claude/commands/issue/new.md +484 -0
  6. package/.claude/commands/issue/plan.md +421 -0
  7. package/.claude/commands/issue/queue.md +354 -0
  8. package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
  9. package/.claude/commands/workflow/docs/analyze.md +1467 -0
  10. package/.claude/commands/workflow/docs/copyright.md +1265 -0
  11. package/.claude/commands/workflow/execute.md +0 -1
  12. package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
  13. package/.claude/commands/workflow/tools/context-gather.md +0 -2
  14. package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
  15. package/.claude/commands/workflow/tools/task-generate-tdd.md +0 -9
  16. package/.claude/commands/workflow/tools/test-context-gather.md +2 -3
  17. package/.claude/commands/workflow/tools/test-task-generate.md +0 -2
  18. package/.claude/skills/_shared/mermaid-utils.md +584 -0
  19. package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +0 -2
  20. package/.claude/skills/command-guide/reference/commands/workflow/execute.md +1 -1
  21. package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +1 -2
  22. package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +1 -8
  23. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-context-gather.md +1 -4
  24. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +0 -2
  25. package/.claude/skills/copyright-docs/SKILL.md +132 -0
  26. package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
  27. package/.claude/skills/copyright-docs/phases/01.5-project-exploration.md +150 -0
  28. package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +664 -0
  29. package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
  30. package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
  31. package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
  32. package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
  33. package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
  34. package/.claude/skills/project-analyze/SKILL.md +162 -0
  35. package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
  36. package/.claude/skills/project-analyze/phases/02-project-exploration.md +176 -0
  37. package/.claude/skills/project-analyze/phases/03-deep-analysis.md +854 -0
  38. package/.claude/skills/project-analyze/phases/03.5-consolidation.md +233 -0
  39. package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
  40. package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
  41. package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
  42. package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
  43. package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
  44. package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +136 -0
  45. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +74 -0
  46. package/.claude/workflows/cli-templates/schemas/queue-schema.json +136 -0
  47. package/.claude/workflows/cli-templates/schemas/registry-schema.json +94 -0
  48. package/.claude/workflows/cli-templates/schemas/solution-schema.json +120 -0
  49. package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +125 -0
  50. package/.codex/prompts/issue-execute.md +266 -0
  51. package/README.md +11 -1
  52. package/ccw/dist/cli.d.ts.map +1 -1
  53. package/ccw/dist/cli.js +25 -0
  54. package/ccw/dist/cli.js.map +1 -1
  55. package/ccw/dist/commands/cli.d.ts.map +1 -1
  56. package/ccw/dist/commands/cli.js +46 -8
  57. package/ccw/dist/commands/cli.js.map +1 -1
  58. package/ccw/dist/commands/issue.d.ts +21 -0
  59. package/ccw/dist/commands/issue.d.ts.map +1 -0
  60. package/ccw/dist/commands/issue.js +895 -0
  61. package/ccw/dist/commands/issue.js.map +1 -0
  62. package/ccw/dist/core/dashboard-generator-patch.js +1 -0
  63. package/ccw/dist/core/dashboard-generator-patch.js.map +1 -1
  64. package/ccw/dist/core/routes/cli-routes.js +2 -2
  65. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  66. package/ccw/dist/core/routes/issue-routes.d.ts +34 -0
  67. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -0
  68. package/ccw/dist/core/routes/issue-routes.js +487 -0
  69. package/ccw/dist/core/routes/issue-routes.js.map +1 -0
  70. package/ccw/dist/core/server.d.ts.map +1 -1
  71. package/ccw/dist/core/server.js +17 -2
  72. package/ccw/dist/core/server.js.map +1 -1
  73. package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
  74. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  75. package/ccw/dist/tools/claude-cli-tools.js +31 -17
  76. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  77. package/ccw/dist/tools/smart-search.d.ts +25 -0
  78. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  79. package/ccw/dist/tools/smart-search.js +121 -17
  80. package/ccw/dist/tools/smart-search.js.map +1 -1
  81. package/ccw/src/cli.ts +26 -0
  82. package/ccw/src/commands/cli.ts +49 -7
  83. package/ccw/src/commands/issue.ts +1184 -0
  84. package/ccw/src/core/dashboard-generator-patch.ts +1 -0
  85. package/ccw/src/core/routes/cli-routes.ts +3 -3
  86. package/ccw/src/core/routes/issue-routes.ts +559 -0
  87. package/ccw/src/core/server.ts +17 -2
  88. package/ccw/src/templates/dashboard-css/32-issue-manager.css +2544 -0
  89. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +467 -0
  90. package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
  91. package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
  92. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +461 -0
  93. package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
  94. package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
  95. package/ccw/src/templates/dashboard-js/i18n.js +290 -2
  96. package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
  97. package/ccw/src/templates/dashboard-js/views/history.js +19 -4
  98. package/ccw/src/templates/dashboard-js/views/hook-manager.js +11 -5
  99. package/ccw/src/templates/dashboard-js/views/issue-manager.js +1546 -0
  100. package/ccw/src/templates/dashboard.html +55 -0
  101. package/ccw/src/tools/claude-cli-tools.ts +37 -20
  102. package/ccw/src/tools/smart-search.ts +157 -16
  103. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  104. package/codex-lens/src/codexlens/config.py +5 -0
  105. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  106. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  107. package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
  108. package/codex-lens/src/codexlens/search/ranking.py +267 -1
  109. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  110. package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
  111. package/package.json +2 -2
@@ -0,0 +1,1546 @@
1
+ // ==========================================
2
+ // ISSUE MANAGER VIEW
3
+ // Manages issues, solutions, and execution queue
4
+ // ==========================================
5
+
6
+ // ========== Issue State ==========
7
+ var issueData = {
8
+ issues: [],
9
+ queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
10
+ selectedIssue: null,
11
+ selectedSolution: null,
12
+ selectedSolutionIssueId: null,
13
+ statusFilter: 'all',
14
+ searchQuery: '',
15
+ viewMode: 'issues' // 'issues' | 'queue'
16
+ };
17
+ var issueLoading = false;
18
+ var issueDragState = {
19
+ dragging: null,
20
+ groupId: null
21
+ };
22
+
23
+ // ========== Main Render Function ==========
24
+ async function renderIssueManager() {
25
+ const container = document.getElementById('mainContent');
26
+ if (!container) return;
27
+
28
+ // Hide stats grid and search
29
+ hideStatsAndCarousel();
30
+
31
+ // Show loading state
32
+ container.innerHTML = '<div class="issue-manager loading">' +
33
+ '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
34
+ '<p>' + t('common.loading') + '</p>' +
35
+ '</div>';
36
+
37
+ // Load data
38
+ await Promise.all([loadIssueData(), loadQueueData()]);
39
+
40
+ // Render the main view
41
+ renderIssueView();
42
+ }
43
+
44
+ // ========== Data Loading ==========
45
+ async function loadIssueData() {
46
+ issueLoading = true;
47
+ try {
48
+ const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath));
49
+ if (!response.ok) throw new Error('Failed to load issues');
50
+ const data = await response.json();
51
+ issueData.issues = data.issues || [];
52
+ updateIssueBadge();
53
+ } catch (err) {
54
+ console.error('Failed to load issues:', err);
55
+ issueData.issues = [];
56
+ } finally {
57
+ issueLoading = false;
58
+ }
59
+ }
60
+
61
+ async function loadQueueData() {
62
+ try {
63
+ const response = await fetch('/api/queue?path=' + encodeURIComponent(projectPath));
64
+ if (!response.ok) throw new Error('Failed to load queue');
65
+ issueData.queue = await response.json();
66
+ } catch (err) {
67
+ console.error('Failed to load queue:', err);
68
+ issueData.queue = { queue: [], conflicts: [], execution_groups: [], grouped_items: {} };
69
+ }
70
+ }
71
+
72
+ async function loadIssueDetail(issueId) {
73
+ try {
74
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath));
75
+ if (!response.ok) throw new Error('Failed to load issue detail');
76
+ return await response.json();
77
+ } catch (err) {
78
+ console.error('Failed to load issue detail:', err);
79
+ return null;
80
+ }
81
+ }
82
+
83
+ function updateIssueBadge() {
84
+ const badge = document.getElementById('badgeIssues');
85
+ if (badge) {
86
+ badge.textContent = issueData.issues.length;
87
+ }
88
+ }
89
+
90
+ // ========== Main View Render ==========
91
+ function renderIssueView() {
92
+ const container = document.getElementById('mainContent');
93
+ if (!container) return;
94
+
95
+ const issues = issueData.issues || [];
96
+ // Apply both status and search filters
97
+ let filteredIssues = issueData.statusFilter === 'all'
98
+ ? issues
99
+ : issues.filter(i => i.status === issueData.statusFilter);
100
+
101
+ if (issueData.searchQuery) {
102
+ const query = issueData.searchQuery.toLowerCase();
103
+ filteredIssues = filteredIssues.filter(i =>
104
+ i.id.toLowerCase().includes(query) ||
105
+ (i.title && i.title.toLowerCase().includes(query)) ||
106
+ (i.context && i.context.toLowerCase().includes(query))
107
+ );
108
+ }
109
+
110
+ container.innerHTML = `
111
+ <div class="issue-manager">
112
+ <!-- Header -->
113
+ <div class="issue-header mb-6">
114
+ <div class="flex items-center justify-between flex-wrap gap-4">
115
+ <div class="flex items-center gap-3">
116
+ <div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
117
+ <i data-lucide="clipboard-list" class="w-5 h-5 text-primary"></i>
118
+ </div>
119
+ <div>
120
+ <h2 class="text-lg font-semibold text-foreground">${t('issues.title') || 'Issue Manager'}</h2>
121
+ <p class="text-sm text-muted-foreground">${t('issues.description') || 'Manage issues, solutions, and execution queue'}</p>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="flex items-center gap-3">
126
+ <!-- Create Button -->
127
+ <button class="issue-create-btn" onclick="showCreateIssueModal()">
128
+ <i data-lucide="plus" class="w-4 h-4"></i>
129
+ <span>${t('issues.create') || 'Create'}</span>
130
+ </button>
131
+
132
+ <!-- View Toggle -->
133
+ <div class="issue-view-toggle">
134
+ <button class="${issueData.viewMode === 'issues' ? 'active' : ''}" onclick="switchIssueView('issues')">
135
+ <i data-lucide="list" class="w-4 h-4 mr-1"></i>
136
+ ${t('issues.viewIssues') || 'Issues'}
137
+ </button>
138
+ <button class="${issueData.viewMode === 'queue' ? 'active' : ''}" onclick="switchIssueView('queue')">
139
+ <i data-lucide="git-branch" class="w-4 h-4 mr-1"></i>
140
+ ${t('issues.viewQueue') || 'Queue'}
141
+ </button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ ${issueData.viewMode === 'issues' ? renderIssueListSection(filteredIssues) : renderQueueSection()}
148
+
149
+ <!-- Detail Panel -->
150
+ <div id="issueDetailPanel" class="issue-detail-panel hidden"></div>
151
+
152
+ <!-- Solution Detail Modal -->
153
+ <div id="solutionDetailModal" class="solution-modal hidden">
154
+ <div class="solution-modal-backdrop" onclick="closeSolutionDetail()"></div>
155
+ <div class="solution-modal-content">
156
+ <div class="solution-modal-header">
157
+ <div class="solution-modal-title">
158
+ <span id="solutionDetailId" class="font-mono text-sm text-muted-foreground"></span>
159
+ <h3 id="solutionDetailTitle">${t('issues.solutionDetail') || 'Solution Details'}</h3>
160
+ </div>
161
+ <div class="solution-modal-actions">
162
+ <button id="solutionBindBtn" class="btn-secondary" onclick="toggleSolutionBind()">
163
+ <i data-lucide="link" class="w-4 h-4"></i>
164
+ <span>${t('issues.bind') || 'Bind'}</span>
165
+ </button>
166
+ <button class="btn-icon" onclick="closeSolutionDetail()">
167
+ <i data-lucide="x" class="w-5 h-5"></i>
168
+ </button>
169
+ </div>
170
+ </div>
171
+ <div class="solution-modal-body" id="solutionDetailBody">
172
+ <!-- Content will be rendered dynamically -->
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- Create Issue Modal -->
178
+ <div id="createIssueModal" class="issue-modal hidden">
179
+ <div class="issue-modal-backdrop" onclick="hideCreateIssueModal()"></div>
180
+ <div class="issue-modal-content">
181
+ <div class="issue-modal-header">
182
+ <h3>${t('issues.createTitle') || 'Create New Issue'}</h3>
183
+ <button class="btn-icon" onclick="hideCreateIssueModal()">
184
+ <i data-lucide="x" class="w-5 h-5"></i>
185
+ </button>
186
+ </div>
187
+ <div class="issue-modal-body">
188
+ <div class="form-group">
189
+ <label>${t('issues.issueId') || 'Issue ID'}</label>
190
+ <div class="input-with-action">
191
+ <input type="text" id="newIssueId" placeholder="${t('issues.idAutoGenerated') || 'Auto-generated'}" />
192
+ <button type="button" class="btn-icon" onclick="regenerateIssueId()" title="${t('issues.regenerateId') || 'Regenerate ID'}">
193
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
194
+ </button>
195
+ </div>
196
+ </div>
197
+ <div class="form-group">
198
+ <label>${t('issues.issueTitle') || 'Title'}</label>
199
+ <input type="text" id="newIssueTitle" placeholder="${t('issues.titlePlaceholder') || 'Brief description of the issue'}" />
200
+ </div>
201
+ <div class="form-group">
202
+ <label>${t('issues.issueContext') || 'Context'} (${t('common.optional') || 'optional'})</label>
203
+ <textarea id="newIssueContext" rows="4" placeholder="${t('issues.contextPlaceholder') || 'Detailed description, requirements, etc.'}"></textarea>
204
+ </div>
205
+ <div class="form-group">
206
+ <label>${t('issues.issuePriority') || 'Priority'}</label>
207
+ <select id="newIssuePriority">
208
+ <option value="1">1 - ${t('issues.priorityLowest') || 'Lowest'}</option>
209
+ <option value="2">2 - ${t('issues.priorityLow') || 'Low'}</option>
210
+ <option value="3" selected>3 - ${t('issues.priorityMedium') || 'Medium'}</option>
211
+ <option value="4">4 - ${t('issues.priorityHigh') || 'High'}</option>
212
+ <option value="5">5 - ${t('issues.priorityCritical') || 'Critical'}</option>
213
+ </select>
214
+ </div>
215
+ </div>
216
+ <div class="issue-modal-footer">
217
+ <button class="btn-secondary" onclick="hideCreateIssueModal()">${t('common.cancel') || 'Cancel'}</button>
218
+ <button class="btn-primary" onclick="createIssue()">${t('issues.create') || 'Create'}</button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ `;
224
+
225
+ lucide.createIcons();
226
+
227
+ // Initialize drag-drop if in queue view
228
+ if (issueData.viewMode === 'queue') {
229
+ initQueueDragDrop();
230
+ }
231
+ }
232
+
233
+ function switchIssueView(mode) {
234
+ issueData.viewMode = mode;
235
+ renderIssueView();
236
+ }
237
+
238
+ // ========== Issue List Section ==========
239
+ function renderIssueListSection(issues) {
240
+ const statuses = ['all', 'registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed'];
241
+ const totalIssues = issueData.issues?.length || 0;
242
+
243
+ return `
244
+ <!-- Toolbar: Search + Filters -->
245
+ <div class="issue-toolbar mb-4">
246
+ <div class="issue-search">
247
+ <i data-lucide="search" class="w-4 h-4"></i>
248
+ <input type="text"
249
+ id="issueSearchInput"
250
+ placeholder="${t('issues.searchPlaceholder') || 'Search issues...'}"
251
+ value="${issueData.searchQuery}"
252
+ oninput="handleIssueSearch(this.value)" />
253
+ ${issueData.searchQuery ? `
254
+ <button class="issue-search-clear" onclick="clearIssueSearch()">
255
+ <i data-lucide="x" class="w-3 h-3"></i>
256
+ </button>
257
+ ` : ''}
258
+ </div>
259
+
260
+ <div class="issue-filters">
261
+ <span class="text-sm text-muted-foreground">${t('issues.filterStatus') || 'Status'}:</span>
262
+ ${statuses.map(status => `
263
+ <button class="issue-filter-btn ${issueData.statusFilter === status ? 'active' : ''}"
264
+ onclick="filterIssuesByStatus('${status}')">
265
+ ${status === 'all' ? (t('issues.filterAll') || 'All') : status}
266
+ </button>
267
+ `).join('')}
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Issues Stats -->
272
+ <div class="issue-stats mb-4">
273
+ <span class="text-sm text-muted-foreground">
274
+ ${t('issues.showing') || 'Showing'} <strong>${issues.length}</strong> ${t('issues.of') || 'of'} <strong>${totalIssues}</strong> ${t('issues.issues') || 'issues'}
275
+ </span>
276
+ </div>
277
+
278
+ <!-- Issues Grid -->
279
+ <div class="issues-grid">
280
+ ${issues.length === 0 ? `
281
+ <div class="issue-empty-container">
282
+ <div class="issue-empty">
283
+ <i data-lucide="inbox" class="w-16 h-16"></i>
284
+ <p class="issue-empty-title">${t('issues.noIssues') || 'No issues found'}</p>
285
+ <p class="issue-empty-hint">${issueData.searchQuery || issueData.statusFilter !== 'all'
286
+ ? (t('issues.tryDifferentFilter') || 'Try adjusting your search or filters')
287
+ : (t('issues.createHint') || 'Click "Create" to add your first issue')}</p>
288
+ ${!issueData.searchQuery && issueData.statusFilter === 'all' ? `
289
+ <button class="issue-empty-btn" onclick="showCreateIssueModal()">
290
+ <i data-lucide="plus" class="w-4 h-4"></i>
291
+ ${t('issues.createFirst') || 'Create First Issue'}
292
+ </button>
293
+ ` : ''}
294
+ </div>
295
+ </div>
296
+ ` : issues.map(issue => renderIssueCard(issue)).join('')}
297
+ </div>
298
+ `;
299
+ }
300
+
301
+ function renderIssueCard(issue) {
302
+ const statusColors = {
303
+ registered: 'registered',
304
+ planning: 'planning',
305
+ planned: 'planned',
306
+ queued: 'queued',
307
+ executing: 'executing',
308
+ completed: 'completed',
309
+ failed: 'failed'
310
+ };
311
+
312
+ return `
313
+ <div class="issue-card" onclick="openIssueDetail('${issue.id}')">
314
+ <div class="flex items-start justify-between mb-3">
315
+ <div class="flex items-center gap-2">
316
+ <span class="issue-id font-mono text-sm">${issue.id}</span>
317
+ <span class="issue-status ${statusColors[issue.status] || ''}">${issue.status || 'unknown'}</span>
318
+ </div>
319
+ <span class="issue-priority" title="${t('issues.priority') || 'Priority'}: ${issue.priority || 3}">
320
+ ${renderPriorityStars(issue.priority || 3)}
321
+ </span>
322
+ </div>
323
+
324
+ <h3 class="issue-title text-foreground font-medium mb-2">${issue.title || issue.id}</h3>
325
+
326
+ <div class="issue-meta flex items-center gap-4 text-sm text-muted-foreground">
327
+ <span class="flex items-center gap-1">
328
+ <i data-lucide="file-text" class="w-3.5 h-3.5"></i>
329
+ ${issue.task_count || 0} ${t('issues.tasks') || 'tasks'}
330
+ </span>
331
+ <span class="flex items-center gap-1">
332
+ <i data-lucide="lightbulb" class="w-3.5 h-3.5"></i>
333
+ ${issue.solution_count || 0} ${t('issues.solutions') || 'solutions'}
334
+ </span>
335
+ ${issue.bound_solution_id ? `
336
+ <span class="flex items-center gap-1 text-primary">
337
+ <i data-lucide="link" class="w-3.5 h-3.5"></i>
338
+ ${t('issues.boundSolution') || 'Bound'}
339
+ </span>
340
+ ` : ''}
341
+ </div>
342
+ </div>
343
+ `;
344
+ }
345
+
346
+ function renderPriorityStars(priority) {
347
+ const maxStars = 5;
348
+ let stars = '';
349
+ for (let i = 1; i <= maxStars; i++) {
350
+ stars += `<i data-lucide="star" class="w-3 h-3 ${i <= priority ? 'text-warning fill-warning' : 'text-muted'}"></i>`;
351
+ }
352
+ return stars;
353
+ }
354
+
355
+ function filterIssuesByStatus(status) {
356
+ issueData.statusFilter = status;
357
+ renderIssueView();
358
+ }
359
+
360
+ // ========== Queue Section ==========
361
+ function renderQueueSection() {
362
+ const queue = issueData.queue;
363
+ const queueItems = queue.queue || [];
364
+ const metadata = queue._metadata || {};
365
+
366
+ // Check if queue is empty
367
+ if (queueItems.length === 0) {
368
+ return `
369
+ <div class="queue-empty-container">
370
+ <div class="queue-empty">
371
+ <i data-lucide="git-branch" class="w-16 h-16"></i>
372
+ <p class="queue-empty-title">${t('issues.queueEmpty') || 'Queue is empty'}</p>
373
+ <p class="queue-empty-hint">${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}</p>
374
+ <button class="queue-create-btn" onclick="createExecutionQueue()">
375
+ <i data-lucide="play" class="w-4 h-4"></i>
376
+ <span>${t('issues.createQueue') || 'Create Queue'}</span>
377
+ </button>
378
+ </div>
379
+ </div>
380
+ `;
381
+ }
382
+
383
+ // Group items by execution_group or treat all as single group
384
+ const groups = queue.execution_groups || [];
385
+ let groupedItems = queue.grouped_items || {};
386
+
387
+ // If no execution_groups, create a default grouping from queue items
388
+ if (groups.length === 0 && queueItems.length > 0) {
389
+ const groupMap = {};
390
+ queueItems.forEach(item => {
391
+ const groupId = item.execution_group || 'default';
392
+ if (!groupMap[groupId]) {
393
+ groupMap[groupId] = [];
394
+ }
395
+ groupMap[groupId].push(item);
396
+ });
397
+
398
+ // Create synthetic groups
399
+ const syntheticGroups = Object.keys(groupMap).map(groupId => ({
400
+ id: groupId,
401
+ type: 'sequential',
402
+ task_count: groupMap[groupId].length
403
+ }));
404
+
405
+ return `
406
+ <!-- Queue Header -->
407
+ <div class="queue-toolbar mb-4">
408
+ <div class="queue-stats">
409
+ <div class="queue-info-card">
410
+ <span class="queue-info-label">${t('issues.queueId') || 'Queue ID'}</span>
411
+ <span class="queue-info-value font-mono text-sm">${queue.id || 'N/A'}</span>
412
+ </div>
413
+ <div class="queue-info-card">
414
+ <span class="queue-info-label">${t('issues.status') || 'Status'}</span>
415
+ <span class="queue-status-badge ${queue.status || ''}">${queue.status || 'unknown'}</span>
416
+ </div>
417
+ <div class="queue-info-card">
418
+ <span class="queue-info-label">${t('issues.issues') || 'Issues'}</span>
419
+ <span class="queue-info-value">${(queue.issue_ids || []).join(', ') || 'N/A'}</span>
420
+ </div>
421
+ </div>
422
+ <div class="queue-actions">
423
+ <button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
424
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
425
+ </button>
426
+ <button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
427
+ <i data-lucide="rotate-cw" class="w-4 h-4"></i>
428
+ <span>${t('issues.regenerate') || 'Regenerate'}</span>
429
+ </button>
430
+ </div>
431
+ </div>
432
+
433
+ <!-- Queue Stats -->
434
+ <div class="queue-stats-grid mb-4">
435
+ <div class="queue-stat-card">
436
+ <span class="queue-stat-value">${metadata.total_tasks || queueItems.length}</span>
437
+ <span class="queue-stat-label">${t('issues.totalTasks') || 'Total'}</span>
438
+ </div>
439
+ <div class="queue-stat-card pending">
440
+ <span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
441
+ <span class="queue-stat-label">${t('issues.pending') || 'Pending'}</span>
442
+ </div>
443
+ <div class="queue-stat-card executing">
444
+ <span class="queue-stat-value">${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length}</span>
445
+ <span class="queue-stat-label">${t('issues.executing') || 'Executing'}</span>
446
+ </div>
447
+ <div class="queue-stat-card completed">
448
+ <span class="queue-stat-value">${metadata.completed_count || queueItems.filter(i => i.status === 'completed').length}</span>
449
+ <span class="queue-stat-label">${t('issues.completed') || 'Completed'}</span>
450
+ </div>
451
+ <div class="queue-stat-card failed">
452
+ <span class="queue-stat-value">${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length}</span>
453
+ <span class="queue-stat-label">${t('issues.failed') || 'Failed'}</span>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- Queue Items -->
458
+ <div class="queue-timeline">
459
+ ${syntheticGroups.map(group => renderQueueGroup(group, groupMap[group.id] || [])).join('')}
460
+ </div>
461
+
462
+ ${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
463
+ `;
464
+ }
465
+
466
+ return `
467
+ <!-- Queue Toolbar -->
468
+ <div class="queue-toolbar mb-4">
469
+ <div class="queue-stats">
470
+ <span class="text-sm text-muted-foreground">
471
+ ${groups.length} ${t('issues.executionGroups') || 'groups'} ·
472
+ ${queueItems.length} ${t('issues.totalItems') || 'items'}
473
+ </span>
474
+ </div>
475
+ <div class="queue-actions">
476
+ <button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
477
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
478
+ </button>
479
+ <button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
480
+ <i data-lucide="rotate-cw" class="w-4 h-4"></i>
481
+ <span>${t('issues.regenerate') || 'Regenerate'}</span>
482
+ </button>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="queue-info mb-4">
487
+ <p class="text-sm text-muted-foreground">
488
+ <i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
489
+ ${t('issues.reorderHint') || 'Drag items within a group to reorder'}
490
+ </p>
491
+ </div>
492
+
493
+ <div class="queue-timeline">
494
+ ${groups.map(group => renderQueueGroup(group, groupedItems[group.id] || [])).join('')}
495
+ </div>
496
+
497
+ ${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
498
+ `;
499
+ }
500
+
501
+ function renderQueueGroup(group, items) {
502
+ const isParallel = group.type === 'parallel';
503
+
504
+ return `
505
+ <div class="queue-group" data-group-id="${group.id}">
506
+ <div class="queue-group-header">
507
+ <div class="queue-group-type ${isParallel ? 'parallel' : 'sequential'}">
508
+ <i data-lucide="${isParallel ? 'git-merge' : 'arrow-right'}" class="w-4 h-4"></i>
509
+ ${group.id} (${isParallel ? t('issues.parallelGroup') || 'Parallel' : t('issues.sequentialGroup') || 'Sequential'})
510
+ </div>
511
+ <span class="text-sm text-muted-foreground">${group.task_count} tasks</span>
512
+ </div>
513
+ <div class="queue-items ${isParallel ? 'parallel' : 'sequential'}">
514
+ ${items.map((item, idx) => renderQueueItem(item, idx, items.length)).join('')}
515
+ </div>
516
+ </div>
517
+ `;
518
+ }
519
+
520
+ function renderQueueItem(item, index, total) {
521
+ const statusColors = {
522
+ pending: '',
523
+ ready: 'ready',
524
+ executing: 'executing',
525
+ completed: 'completed',
526
+ failed: 'failed',
527
+ blocked: 'blocked'
528
+ };
529
+
530
+ return `
531
+ <div class="queue-item ${statusColors[item.status] || ''}"
532
+ draggable="true"
533
+ data-queue-id="${item.queue_id}"
534
+ data-group-id="${item.execution_group}"
535
+ onclick="openQueueItemDetail('${item.queue_id}')">
536
+ <span class="queue-item-id font-mono text-xs">${item.queue_id}</span>
537
+ <span class="queue-item-issue text-xs text-muted-foreground">${item.issue_id}</span>
538
+ <span class="queue-item-task text-sm">${item.task_id}</span>
539
+ <span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
540
+ <i data-lucide="arrow-up" class="w-3 h-3"></i>
541
+ </span>
542
+ ${item.depends_on && item.depends_on.length > 0 ? `
543
+ <span class="queue-item-deps text-xs text-muted-foreground" title="${t('issues.dependsOn') || 'Depends on'}: ${item.depends_on.join(', ')}">
544
+ <i data-lucide="link" class="w-3 h-3"></i>
545
+ </span>
546
+ ` : ''}
547
+ </div>
548
+ `;
549
+ }
550
+
551
+ function renderConflictsSection(conflicts) {
552
+ return `
553
+ <div class="conflicts-section mt-6">
554
+ <h3 class="text-sm font-semibold text-foreground mb-3">
555
+ <i data-lucide="alert-triangle" class="w-4 h-4 inline text-warning mr-1"></i>
556
+ Conflicts (${conflicts.length})
557
+ </h3>
558
+ <div class="conflicts-list">
559
+ ${conflicts.map(c => `
560
+ <div class="conflict-item">
561
+ <span class="conflict-file font-mono text-xs">${c.file}</span>
562
+ <span class="conflict-tasks text-xs text-muted-foreground">${c.tasks.join(' → ')}</span>
563
+ <span class="conflict-status ${c.resolved ? 'resolved' : 'pending'}">
564
+ ${c.resolved ? 'Resolved' : 'Pending'}
565
+ </span>
566
+ </div>
567
+ `).join('')}
568
+ </div>
569
+ </div>
570
+ `;
571
+ }
572
+
573
+ // ========== Drag-Drop for Queue ==========
574
+ function initQueueDragDrop() {
575
+ const items = document.querySelectorAll('.queue-item[draggable="true"]');
576
+
577
+ items.forEach(item => {
578
+ item.addEventListener('dragstart', handleIssueDragStart);
579
+ item.addEventListener('dragend', handleIssueDragEnd);
580
+ item.addEventListener('dragover', handleIssueDragOver);
581
+ item.addEventListener('drop', handleIssueDrop);
582
+ });
583
+ }
584
+
585
+ function handleIssueDragStart(e) {
586
+ const item = e.target.closest('.queue-item');
587
+ if (!item) return;
588
+
589
+ issueDragState.dragging = item.dataset.queueId;
590
+ issueDragState.groupId = item.dataset.groupId;
591
+
592
+ item.classList.add('dragging');
593
+ e.dataTransfer.effectAllowed = 'move';
594
+ e.dataTransfer.setData('text/plain', item.dataset.queueId);
595
+ }
596
+
597
+ function handleIssueDragEnd(e) {
598
+ const item = e.target.closest('.queue-item');
599
+ if (item) {
600
+ item.classList.remove('dragging');
601
+ }
602
+ issueDragState.dragging = null;
603
+ issueDragState.groupId = null;
604
+
605
+ // Remove all placeholders
606
+ document.querySelectorAll('.queue-drop-placeholder').forEach(p => p.remove());
607
+ }
608
+
609
+ function handleIssueDragOver(e) {
610
+ e.preventDefault();
611
+
612
+ const target = e.target.closest('.queue-item');
613
+ if (!target || target.dataset.queueId === issueDragState.dragging) return;
614
+
615
+ // Only allow drag within same group
616
+ if (target.dataset.groupId !== issueDragState.groupId) {
617
+ e.dataTransfer.dropEffect = 'none';
618
+ return;
619
+ }
620
+
621
+ e.dataTransfer.dropEffect = 'move';
622
+ }
623
+
624
+ function handleIssueDrop(e) {
625
+ e.preventDefault();
626
+
627
+ const target = e.target.closest('.queue-item');
628
+ if (!target || !issueDragState.dragging) return;
629
+
630
+ // Only allow drop within same group
631
+ if (target.dataset.groupId !== issueDragState.groupId) return;
632
+
633
+ const container = target.closest('.queue-items');
634
+ if (!container) return;
635
+
636
+ // Get new order
637
+ const items = Array.from(container.querySelectorAll('.queue-item'));
638
+ const draggedItem = items.find(i => i.dataset.queueId === issueDragState.dragging);
639
+ const targetIndex = items.indexOf(target);
640
+ const draggedIndex = items.indexOf(draggedItem);
641
+
642
+ if (draggedIndex === targetIndex) return;
643
+
644
+ // Reorder in DOM
645
+ if (draggedIndex < targetIndex) {
646
+ target.after(draggedItem);
647
+ } else {
648
+ target.before(draggedItem);
649
+ }
650
+
651
+ // Get new order and save
652
+ const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.queueId);
653
+ saveQueueOrder(issueDragState.groupId, newOrder);
654
+ }
655
+
656
+ async function saveQueueOrder(groupId, newOrder) {
657
+ try {
658
+ const response = await fetch('/api/queue/reorder?path=' + encodeURIComponent(projectPath), {
659
+ method: 'POST',
660
+ headers: { 'Content-Type': 'application/json' },
661
+ body: JSON.stringify({ groupId, newOrder })
662
+ });
663
+
664
+ if (!response.ok) {
665
+ throw new Error('Failed to save queue order');
666
+ }
667
+
668
+ const result = await response.json();
669
+ if (result.error) {
670
+ showNotification(result.error, 'error');
671
+ } else {
672
+ showNotification('Queue reordered', 'success');
673
+ // Reload queue data
674
+ await loadQueueData();
675
+ }
676
+ } catch (err) {
677
+ console.error('Failed to save queue order:', err);
678
+ showNotification('Failed to save queue order', 'error');
679
+ // Reload to restore original order
680
+ await loadQueueData();
681
+ renderIssueView();
682
+ }
683
+ }
684
+
685
+ // ========== Detail Panel ==========
686
+ async function openIssueDetail(issueId) {
687
+ const panel = document.getElementById('issueDetailPanel');
688
+ if (!panel) return;
689
+
690
+ panel.innerHTML = '<div class="p-8 text-center"><i data-lucide="loader-2" class="w-8 h-8 animate-spin mx-auto"></i></div>';
691
+ panel.classList.remove('hidden');
692
+ lucide.createIcons();
693
+
694
+ const detail = await loadIssueDetail(issueId);
695
+ if (!detail) {
696
+ panel.innerHTML = '<div class="p-8 text-center text-destructive">Failed to load issue</div>';
697
+ return;
698
+ }
699
+
700
+ issueData.selectedIssue = detail;
701
+ renderIssueDetailPanel(detail);
702
+ }
703
+
704
+ function renderIssueDetailPanel(issue) {
705
+ const panel = document.getElementById('issueDetailPanel');
706
+ if (!panel) return;
707
+
708
+ const boundSolution = issue.solutions?.find(s => s.is_bound);
709
+
710
+ panel.innerHTML = `
711
+ <div class="issue-detail-header">
712
+ <div class="flex items-center justify-between">
713
+ <h3 class="text-lg font-semibold">${issue.id}</h3>
714
+ <button class="btn-icon" onclick="closeIssueDetail()">
715
+ <i data-lucide="x" class="w-5 h-5"></i>
716
+ </button>
717
+ </div>
718
+ <span class="issue-status ${issue.status || ''}">${issue.status || 'unknown'}</span>
719
+ </div>
720
+
721
+ <div class="issue-detail-content">
722
+ <!-- Title (editable) -->
723
+ <div class="detail-section">
724
+ <label class="detail-label">Title</label>
725
+ <div class="detail-editable" id="issueTitle">
726
+ <span class="detail-value">${issue.title || issue.id}</span>
727
+ <button class="btn-edit" onclick="startEditField('${issue.id}', 'title', '${(issue.title || issue.id).replace(/'/g, "\\'")}')">
728
+ <i data-lucide="pencil" class="w-3.5 h-3.5"></i>
729
+ </button>
730
+ </div>
731
+ </div>
732
+
733
+ <!-- Context (editable) -->
734
+ <div class="detail-section">
735
+ <label class="detail-label">Context</label>
736
+ <div class="detail-context" id="issueContext">
737
+ <pre class="detail-pre">${issue.context || 'No context'}</pre>
738
+ <button class="btn-edit" onclick="startEditContext('${issue.id}')">
739
+ <i data-lucide="pencil" class="w-3.5 h-3.5"></i>
740
+ </button>
741
+ </div>
742
+ </div>
743
+
744
+ <!-- Solutions -->
745
+ <div class="detail-section">
746
+ <label class="detail-label">${t('issues.solutions') || 'Solutions'} (${issue.solutions?.length || 0})</label>
747
+ <div class="solutions-list">
748
+ ${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
749
+ <div class="solution-item ${sol.is_bound ? 'bound' : ''}" onclick="openSolutionDetail('${issue.id}', '${sol.id}')">
750
+ <div class="solution-header">
751
+ <span class="solution-id font-mono text-xs">${sol.id}</span>
752
+ ${sol.is_bound ? '<span class="solution-bound-badge">' + (t('issues.bound') || 'Bound') + '</span>' : ''}
753
+ <span class="solution-tasks text-xs">${sol.tasks?.length || 0} ${t('issues.tasks') || 'tasks'}</span>
754
+ <i data-lucide="chevron-right" class="w-4 h-4 ml-auto text-muted-foreground"></i>
755
+ </div>
756
+ </div>
757
+ `).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noSolutions') || 'No solutions') + '</p>'}
758
+ </div>
759
+ </div>
760
+
761
+ <!-- Tasks (from tasks.jsonl) -->
762
+ <div class="detail-section">
763
+ <label class="detail-label">${t('issues.tasks') || 'Tasks'} (${issue.tasks?.length || 0})</label>
764
+ <div class="tasks-list">
765
+ ${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
766
+ <div class="task-item-detail">
767
+ <div class="flex items-center justify-between">
768
+ <span class="font-mono text-sm">${task.id}</span>
769
+ <select class="task-status-select" onchange="updateTaskStatus('${issue.id}', '${task.id}', this.value)">
770
+ ${['pending', 'ready', 'in_progress', 'completed', 'failed', 'paused', 'skipped'].map(s =>
771
+ `<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>`
772
+ ).join('')}
773
+ </select>
774
+ </div>
775
+ <p class="task-title-detail">${task.title || task.description || ''}</p>
776
+ </div>
777
+ `).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noTasks') || 'No tasks') + '</p>'}
778
+ </div>
779
+ </div>
780
+ </div>
781
+ `;
782
+
783
+ lucide.createIcons();
784
+ }
785
+
786
+ function closeIssueDetail() {
787
+ const panel = document.getElementById('issueDetailPanel');
788
+ if (panel) {
789
+ panel.classList.add('hidden');
790
+ }
791
+ issueData.selectedIssue = null;
792
+ }
793
+
794
+ function toggleSolutionExpand(solId) {
795
+ const el = document.getElementById('solution-' + solId);
796
+ if (el) {
797
+ el.classList.toggle('hidden');
798
+ }
799
+ }
800
+
801
+ // ========== Solution Detail Modal ==========
802
+ function openSolutionDetail(issueId, solutionId) {
803
+ const issue = issueData.selectedIssue || issueData.issues.find(i => i.id === issueId);
804
+ if (!issue) return;
805
+
806
+ const solution = issue.solutions?.find(s => s.id === solutionId);
807
+ if (!solution) return;
808
+
809
+ issueData.selectedSolution = solution;
810
+ issueData.selectedSolutionIssueId = issueId;
811
+
812
+ const modal = document.getElementById('solutionDetailModal');
813
+ if (modal) {
814
+ modal.classList.remove('hidden');
815
+ renderSolutionDetail(solution);
816
+ lucide.createIcons();
817
+ }
818
+ }
819
+
820
+ function closeSolutionDetail() {
821
+ const modal = document.getElementById('solutionDetailModal');
822
+ if (modal) {
823
+ modal.classList.add('hidden');
824
+ }
825
+ issueData.selectedSolution = null;
826
+ issueData.selectedSolutionIssueId = null;
827
+ }
828
+
829
+ function renderSolutionDetail(solution) {
830
+ const idEl = document.getElementById('solutionDetailId');
831
+ const bodyEl = document.getElementById('solutionDetailBody');
832
+ const bindBtn = document.getElementById('solutionBindBtn');
833
+
834
+ if (idEl) {
835
+ idEl.textContent = solution.id;
836
+ }
837
+
838
+ // Update bind button state
839
+ if (bindBtn) {
840
+ if (solution.is_bound) {
841
+ bindBtn.innerHTML = `<i data-lucide="unlink" class="w-4 h-4"></i><span>${t('issues.unbind') || 'Unbind'}</span>`;
842
+ bindBtn.classList.remove('btn-secondary');
843
+ bindBtn.classList.add('btn-primary');
844
+ } else {
845
+ bindBtn.innerHTML = `<i data-lucide="link" class="w-4 h-4"></i><span>${t('issues.bind') || 'Bind'}</span>`;
846
+ bindBtn.classList.remove('btn-primary');
847
+ bindBtn.classList.add('btn-secondary');
848
+ }
849
+ }
850
+
851
+ if (!bodyEl) return;
852
+
853
+ const tasks = solution.tasks || [];
854
+
855
+ bodyEl.innerHTML = `
856
+ <!-- Solution Overview -->
857
+ <div class="solution-detail-section">
858
+ <div class="solution-overview">
859
+ <div class="solution-stat">
860
+ <span class="solution-stat-value">${tasks.length}</span>
861
+ <span class="solution-stat-label">${t('issues.totalTasks') || 'Total Tasks'}</span>
862
+ </div>
863
+ <div class="solution-stat">
864
+ <span class="solution-stat-value">${solution.is_bound ? '✓' : '—'}</span>
865
+ <span class="solution-stat-label">${t('issues.bindStatus') || 'Bind Status'}</span>
866
+ </div>
867
+ <div class="solution-stat">
868
+ <span class="solution-stat-value">${solution.created_at ? new Date(solution.created_at).toLocaleDateString() : '—'}</span>
869
+ <span class="solution-stat-label">${t('issues.createdAt') || 'Created'}</span>
870
+ </div>
871
+ </div>
872
+ </div>
873
+
874
+ <!-- Tasks List -->
875
+ <div class="solution-detail-section">
876
+ <h4 class="solution-detail-section-title">
877
+ <i data-lucide="list-checks" class="w-4 h-4"></i>
878
+ ${t('issues.taskList') || 'Task List'}
879
+ </h4>
880
+ <div class="solution-tasks-detail">
881
+ ${tasks.length === 0 ? `
882
+ <p class="text-sm text-muted-foreground text-center py-4">${t('issues.noTasks') || 'No tasks in this solution'}</p>
883
+ ` : tasks.map((task, index) => renderSolutionTask(task, index)).join('')}
884
+ </div>
885
+ </div>
886
+
887
+ <!-- Raw JSON (collapsible) -->
888
+ <div class="solution-detail-section">
889
+ <button class="solution-json-toggle" onclick="toggleSolutionJson()">
890
+ <i data-lucide="code" class="w-4 h-4"></i>
891
+ <span>${t('issues.viewJson') || 'View Raw JSON'}</span>
892
+ <i data-lucide="chevron-down" class="w-4 h-4 ml-auto"></i>
893
+ </button>
894
+ <div id="solutionJsonContent" class="solution-json-content hidden">
895
+ <pre class="solution-json-pre">${escapeHtml(JSON.stringify(solution, null, 2))}</pre>
896
+ </div>
897
+ </div>
898
+ `;
899
+
900
+ lucide.createIcons();
901
+ }
902
+
903
+ function renderSolutionTask(task, index) {
904
+ const actionClass = (task.action || 'unknown').toLowerCase();
905
+ const modPoints = task.modification_points || [];
906
+ // Support both old and new field names
907
+ const implSteps = task.implementation || task.implementation_steps || [];
908
+ const acceptance = task.acceptance || task.acceptance_criteria || [];
909
+ const testInfo = task.test || {};
910
+ const regression = task.regression || [];
911
+ const commitInfo = task.commit || {};
912
+ const dependsOn = task.depends_on || task.dependencies || [];
913
+
914
+ // Handle acceptance as object or array
915
+ const acceptanceCriteria = Array.isArray(acceptance) ? acceptance : (acceptance.criteria || []);
916
+ const acceptanceVerification = acceptance.verification || [];
917
+
918
+ return `
919
+ <div class="solution-task-card">
920
+ <div class="solution-task-header" onclick="toggleTaskExpand(${index})">
921
+ <div class="solution-task-info">
922
+ <span class="solution-task-index">#${index + 1}</span>
923
+ <span class="solution-task-id font-mono">${task.id || ''}</span>
924
+ <span class="task-action-badge ${actionClass}">${task.action || 'Unknown'}</span>
925
+ </div>
926
+ <i data-lucide="chevron-down" class="w-4 h-4 task-expand-icon" id="taskExpandIcon${index}"></i>
927
+ </div>
928
+ <div class="solution-task-title">${task.title || task.description || 'No title'}</div>
929
+
930
+ <div class="solution-task-details hidden" id="taskDetails${index}">
931
+ ${task.scope ? `
932
+ <div class="solution-task-scope">
933
+ <span class="solution-task-scope-label">${t('issues.scope') || 'Scope'}:</span>
934
+ <span class="font-mono text-sm">${task.scope}</span>
935
+ </div>
936
+ ` : ''}
937
+
938
+ <!-- Phase 1: Implementation -->
939
+ ${implSteps.length > 0 ? `
940
+ <div class="solution-task-section">
941
+ <h5 class="solution-task-subtitle">
942
+ <i data-lucide="code" class="w-3.5 h-3.5"></i>
943
+ <span class="phase-badge phase-1">1</span>
944
+ ${t('issues.implementation') || 'Implementation'}
945
+ </h5>
946
+ <ol class="solution-impl-list">
947
+ ${implSteps.map(step => `<li>${typeof step === 'string' ? step : step.description || JSON.stringify(step)}</li>`).join('')}
948
+ </ol>
949
+ </div>
950
+ ` : ''}
951
+
952
+ ${modPoints.length > 0 ? `
953
+ <div class="solution-task-section">
954
+ <h5 class="solution-task-subtitle">
955
+ <i data-lucide="file-edit" class="w-3.5 h-3.5"></i>
956
+ ${t('issues.modificationPoints') || 'Modification Points'}
957
+ </h5>
958
+ <ul class="solution-task-list">
959
+ ${modPoints.map(mp => `
960
+ <li class="solution-mod-point">
961
+ <span class="mod-point-file font-mono">${mp.file || mp}</span>
962
+ ${mp.target ? `<span class="mod-point-target">→ ${mp.target}</span>` : ''}
963
+ ${mp.change ? `<span class="mod-point-change">${mp.change}</span>` : ''}
964
+ </li>
965
+ `).join('')}
966
+ </ul>
967
+ </div>
968
+ ` : ''}
969
+
970
+ <!-- Phase 2: Test -->
971
+ ${(testInfo.unit?.length > 0 || testInfo.commands?.length > 0) ? `
972
+ <div class="solution-task-section">
973
+ <h5 class="solution-task-subtitle">
974
+ <i data-lucide="flask-conical" class="w-3.5 h-3.5"></i>
975
+ <span class="phase-badge phase-2">2</span>
976
+ ${t('issues.test') || 'Test'}
977
+ ${testInfo.coverage_target ? `<span class="coverage-target">(${testInfo.coverage_target}% coverage)</span>` : ''}
978
+ </h5>
979
+ ${testInfo.unit?.length > 0 ? `
980
+ <div class="test-subsection">
981
+ <span class="test-label">${t('issues.unitTests') || 'Unit Tests'}:</span>
982
+ <ul class="test-list">
983
+ ${testInfo.unit.map(t => `<li>${t}</li>`).join('')}
984
+ </ul>
985
+ </div>
986
+ ` : ''}
987
+ ${testInfo.integration?.length > 0 ? `
988
+ <div class="test-subsection">
989
+ <span class="test-label">${t('issues.integrationTests') || 'Integration'}:</span>
990
+ <ul class="test-list">
991
+ ${testInfo.integration.map(t => `<li>${t}</li>`).join('')}
992
+ </ul>
993
+ </div>
994
+ ` : ''}
995
+ ${testInfo.commands?.length > 0 ? `
996
+ <div class="test-subsection">
997
+ <span class="test-label">${t('issues.commands') || 'Commands'}:</span>
998
+ <div class="test-commands">
999
+ ${testInfo.commands.map(cmd => `<code class="test-command">${cmd}</code>`).join('')}
1000
+ </div>
1001
+ </div>
1002
+ ` : ''}
1003
+ </div>
1004
+ ` : ''}
1005
+
1006
+ <!-- Phase 3: Regression -->
1007
+ ${regression.length > 0 ? `
1008
+ <div class="solution-task-section">
1009
+ <h5 class="solution-task-subtitle">
1010
+ <i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
1011
+ <span class="phase-badge phase-3">3</span>
1012
+ ${t('issues.regression') || 'Regression'}
1013
+ </h5>
1014
+ <div class="test-commands">
1015
+ ${regression.map(cmd => `<code class="test-command">${cmd}</code>`).join('')}
1016
+ </div>
1017
+ </div>
1018
+ ` : ''}
1019
+
1020
+ <!-- Phase 4: Acceptance -->
1021
+ ${acceptanceCriteria.length > 0 ? `
1022
+ <div class="solution-task-section">
1023
+ <h5 class="solution-task-subtitle">
1024
+ <i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
1025
+ <span class="phase-badge phase-4">4</span>
1026
+ ${t('issues.acceptance') || 'Acceptance'}
1027
+ </h5>
1028
+ <div class="acceptance-subsection">
1029
+ <span class="acceptance-label">${t('issues.criteria') || 'Criteria'}:</span>
1030
+ <ul class="solution-acceptance-list">
1031
+ ${acceptanceCriteria.map(ac => `<li>${typeof ac === 'string' ? ac : ac.description || JSON.stringify(ac)}</li>`).join('')}
1032
+ </ul>
1033
+ </div>
1034
+ ${acceptanceVerification.length > 0 ? `
1035
+ <div class="acceptance-subsection">
1036
+ <span class="acceptance-label">${t('issues.verification') || 'Verification'}:</span>
1037
+ <div class="verification-commands">
1038
+ ${acceptanceVerification.map(v => `<code class="verification-command">${v}</code>`).join('')}
1039
+ </div>
1040
+ </div>
1041
+ ` : ''}
1042
+ </div>
1043
+ ` : ''}
1044
+
1045
+ <!-- Phase 5: Commit -->
1046
+ ${commitInfo.type ? `
1047
+ <div class="solution-task-section">
1048
+ <h5 class="solution-task-subtitle">
1049
+ <i data-lucide="git-commit" class="w-3.5 h-3.5"></i>
1050
+ <span class="phase-badge phase-5">5</span>
1051
+ ${t('issues.commit') || 'Commit'}
1052
+ </h5>
1053
+ <div class="commit-info">
1054
+ <div class="commit-type">
1055
+ <span class="commit-type-badge ${commitInfo.type}">${commitInfo.type}</span>
1056
+ <span class="commit-scope">(${commitInfo.scope || 'core'})</span>
1057
+ ${commitInfo.breaking ? '<span class="commit-breaking">BREAKING</span>' : ''}
1058
+ </div>
1059
+ ${commitInfo.message_template ? `
1060
+ <pre class="commit-message">${commitInfo.message_template}</pre>
1061
+ ` : ''}
1062
+ </div>
1063
+ </div>
1064
+ ` : ''}
1065
+
1066
+ <!-- Dependencies -->
1067
+ ${dependsOn.length > 0 ? `
1068
+ <div class="solution-task-section">
1069
+ <h5 class="solution-task-subtitle">
1070
+ <i data-lucide="git-branch" class="w-3.5 h-3.5"></i>
1071
+ ${t('issues.dependencies') || 'Dependencies'}
1072
+ </h5>
1073
+ <div class="solution-deps-list">
1074
+ ${dependsOn.map(dep => `<span class="solution-dep-tag font-mono">${dep}</span>`).join('')}
1075
+ </div>
1076
+ </div>
1077
+ ` : ''}
1078
+ </div>
1079
+ </div>
1080
+ `;
1081
+ }
1082
+
1083
+ function toggleTaskExpand(index) {
1084
+ const details = document.getElementById('taskDetails' + index);
1085
+ const icon = document.getElementById('taskExpandIcon' + index);
1086
+ if (details) {
1087
+ details.classList.toggle('hidden');
1088
+ }
1089
+ if (icon) {
1090
+ icon.style.transform = details?.classList.contains('hidden') ? '' : 'rotate(180deg)';
1091
+ }
1092
+ }
1093
+
1094
+ function toggleSolutionJson() {
1095
+ const content = document.getElementById('solutionJsonContent');
1096
+ if (content) {
1097
+ content.classList.toggle('hidden');
1098
+ }
1099
+ }
1100
+
1101
+ async function toggleSolutionBind() {
1102
+ const solution = issueData.selectedSolution;
1103
+ const issueId = issueData.selectedSolutionIssueId;
1104
+ if (!solution || !issueId) return;
1105
+
1106
+ const action = solution.is_bound ? 'unbind' : 'bind';
1107
+
1108
+ try {
1109
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
1110
+ method: 'PATCH',
1111
+ headers: { 'Content-Type': 'application/json' },
1112
+ body: JSON.stringify({
1113
+ bound_solution_id: action === 'bind' ? solution.id : null
1114
+ })
1115
+ });
1116
+
1117
+ if (!response.ok) throw new Error('Failed to ' + action);
1118
+
1119
+ showNotification(action === 'bind' ? (t('issues.solutionBound') || 'Solution bound') : (t('issues.solutionUnbound') || 'Solution unbound'), 'success');
1120
+
1121
+ // Refresh data
1122
+ await loadIssueData();
1123
+ const detail = await loadIssueDetail(issueId);
1124
+ if (detail) {
1125
+ issueData.selectedIssue = detail;
1126
+ // Update solution reference
1127
+ const updatedSolution = detail.solutions?.find(s => s.id === solution.id);
1128
+ if (updatedSolution) {
1129
+ issueData.selectedSolution = updatedSolution;
1130
+ renderSolutionDetail(updatedSolution);
1131
+ }
1132
+ renderIssueDetailPanel(detail);
1133
+ }
1134
+ } catch (err) {
1135
+ console.error('Failed to ' + action + ' solution:', err);
1136
+ showNotification('Failed to ' + action + ' solution', 'error');
1137
+ }
1138
+ }
1139
+
1140
+ // Helper: escape HTML
1141
+ function escapeHtml(text) {
1142
+ if (!text) return '';
1143
+ const div = document.createElement('div');
1144
+ div.textContent = text;
1145
+ return div.innerHTML;
1146
+ }
1147
+
1148
+ function openQueueItemDetail(queueId) {
1149
+ const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
1150
+ if (item) {
1151
+ openIssueDetail(item.issue_id);
1152
+ }
1153
+ }
1154
+
1155
+ // ========== Edit Functions ==========
1156
+ function startEditField(issueId, field, currentValue) {
1157
+ const container = document.getElementById('issueTitle');
1158
+ if (!container) return;
1159
+
1160
+ container.innerHTML = `
1161
+ <input type="text" class="edit-input" id="editField" value="${currentValue}" />
1162
+ <div class="edit-actions">
1163
+ <button class="btn-save" onclick="saveFieldEdit('${issueId}', '${field}')">
1164
+ <i data-lucide="check" class="w-4 h-4"></i>
1165
+ </button>
1166
+ <button class="btn-cancel" onclick="cancelEdit()">
1167
+ <i data-lucide="x" class="w-4 h-4"></i>
1168
+ </button>
1169
+ </div>
1170
+ `;
1171
+ lucide.createIcons();
1172
+ document.getElementById('editField')?.focus();
1173
+ }
1174
+
1175
+ function startEditContext(issueId) {
1176
+ const container = document.getElementById('issueContext');
1177
+ const currentValue = issueData.selectedIssue?.context || '';
1178
+ if (!container) return;
1179
+
1180
+ container.innerHTML = `
1181
+ <textarea class="edit-textarea" id="editContext" rows="8">${currentValue}</textarea>
1182
+ <div class="edit-actions">
1183
+ <button class="btn-save" onclick="saveContextEdit('${issueId}')">
1184
+ <i data-lucide="check" class="w-4 h-4"></i>
1185
+ </button>
1186
+ <button class="btn-cancel" onclick="cancelEdit()">
1187
+ <i data-lucide="x" class="w-4 h-4"></i>
1188
+ </button>
1189
+ </div>
1190
+ `;
1191
+ lucide.createIcons();
1192
+ document.getElementById('editContext')?.focus();
1193
+ }
1194
+
1195
+ async function saveFieldEdit(issueId, field) {
1196
+ const input = document.getElementById('editField');
1197
+ if (!input) return;
1198
+
1199
+ const value = input.value.trim();
1200
+ if (!value) return;
1201
+
1202
+ try {
1203
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
1204
+ method: 'PATCH',
1205
+ headers: { 'Content-Type': 'application/json' },
1206
+ body: JSON.stringify({ [field]: value })
1207
+ });
1208
+
1209
+ if (!response.ok) throw new Error('Failed to update');
1210
+
1211
+ showNotification('Updated ' + field, 'success');
1212
+
1213
+ // Refresh data
1214
+ await loadIssueData();
1215
+ const detail = await loadIssueDetail(issueId);
1216
+ if (detail) {
1217
+ issueData.selectedIssue = detail;
1218
+ renderIssueDetailPanel(detail);
1219
+ }
1220
+ } catch (err) {
1221
+ showNotification('Failed to update', 'error');
1222
+ cancelEdit();
1223
+ }
1224
+ }
1225
+
1226
+ async function saveContextEdit(issueId) {
1227
+ const textarea = document.getElementById('editContext');
1228
+ if (!textarea) return;
1229
+
1230
+ const value = textarea.value;
1231
+
1232
+ try {
1233
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
1234
+ method: 'PATCH',
1235
+ headers: { 'Content-Type': 'application/json' },
1236
+ body: JSON.stringify({ context: value })
1237
+ });
1238
+
1239
+ if (!response.ok) throw new Error('Failed to update');
1240
+
1241
+ showNotification('Context updated', 'success');
1242
+
1243
+ // Refresh detail
1244
+ const detail = await loadIssueDetail(issueId);
1245
+ if (detail) {
1246
+ issueData.selectedIssue = detail;
1247
+ renderIssueDetailPanel(detail);
1248
+ }
1249
+ } catch (err) {
1250
+ showNotification('Failed to update context', 'error');
1251
+ cancelEdit();
1252
+ }
1253
+ }
1254
+
1255
+ function cancelEdit() {
1256
+ if (issueData.selectedIssue) {
1257
+ renderIssueDetailPanel(issueData.selectedIssue);
1258
+ }
1259
+ }
1260
+
1261
+ async function updateTaskStatus(issueId, taskId, status) {
1262
+ try {
1263
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/tasks/' + encodeURIComponent(taskId) + '?path=' + encodeURIComponent(projectPath), {
1264
+ method: 'PATCH',
1265
+ headers: { 'Content-Type': 'application/json' },
1266
+ body: JSON.stringify({ status })
1267
+ });
1268
+
1269
+ if (!response.ok) throw new Error('Failed to update task');
1270
+
1271
+ showNotification('Task status updated', 'success');
1272
+ } catch (err) {
1273
+ showNotification('Failed to update task status', 'error');
1274
+ }
1275
+ }
1276
+
1277
+ // ========== Search Functions ==========
1278
+ function handleIssueSearch(value) {
1279
+ issueData.searchQuery = value;
1280
+ renderIssueView();
1281
+ }
1282
+
1283
+ function clearIssueSearch() {
1284
+ issueData.searchQuery = '';
1285
+ renderIssueView();
1286
+ }
1287
+
1288
+ // ========== Create Issue Modal ==========
1289
+ function generateIssueId() {
1290
+ // Generate unique ID: ISSUE-YYYYMMDD-XXX format
1291
+ const now = new Date();
1292
+ const dateStr = now.getFullYear().toString() +
1293
+ String(now.getMonth() + 1).padStart(2, '0') +
1294
+ String(now.getDate()).padStart(2, '0');
1295
+
1296
+ // Find existing IDs with same date prefix
1297
+ const prefix = 'ISSUE-' + dateStr + '-';
1298
+ const existingIds = (issueData.issues || [])
1299
+ .map(i => i.id)
1300
+ .filter(id => id.startsWith(prefix));
1301
+
1302
+ // Get next sequence number
1303
+ let maxSeq = 0;
1304
+ existingIds.forEach(id => {
1305
+ const seqStr = id.replace(prefix, '');
1306
+ const seq = parseInt(seqStr, 10);
1307
+ if (!isNaN(seq) && seq > maxSeq) {
1308
+ maxSeq = seq;
1309
+ }
1310
+ });
1311
+
1312
+ return prefix + String(maxSeq + 1).padStart(3, '0');
1313
+ }
1314
+
1315
+ function showCreateIssueModal() {
1316
+ const modal = document.getElementById('createIssueModal');
1317
+ if (modal) {
1318
+ modal.classList.remove('hidden');
1319
+
1320
+ // Auto-generate issue ID
1321
+ const idInput = document.getElementById('newIssueId');
1322
+ if (idInput) {
1323
+ idInput.value = generateIssueId();
1324
+ }
1325
+
1326
+ lucide.createIcons();
1327
+ // Focus on title input instead of ID
1328
+ setTimeout(() => {
1329
+ document.getElementById('newIssueTitle')?.focus();
1330
+ }, 100);
1331
+ }
1332
+ }
1333
+
1334
+ function regenerateIssueId() {
1335
+ const idInput = document.getElementById('newIssueId');
1336
+ if (idInput) {
1337
+ idInput.value = generateIssueId();
1338
+ }
1339
+ }
1340
+
1341
+ function hideCreateIssueModal() {
1342
+ const modal = document.getElementById('createIssueModal');
1343
+ if (modal) {
1344
+ modal.classList.add('hidden');
1345
+ // Clear form
1346
+ const idInput = document.getElementById('newIssueId');
1347
+ const titleInput = document.getElementById('newIssueTitle');
1348
+ const contextInput = document.getElementById('newIssueContext');
1349
+ const prioritySelect = document.getElementById('newIssuePriority');
1350
+ if (idInput) idInput.value = '';
1351
+ if (titleInput) titleInput.value = '';
1352
+ if (contextInput) contextInput.value = '';
1353
+ if (prioritySelect) prioritySelect.value = '3';
1354
+ }
1355
+ }
1356
+
1357
+ async function createIssue() {
1358
+ const idInput = document.getElementById('newIssueId');
1359
+ const titleInput = document.getElementById('newIssueTitle');
1360
+ const contextInput = document.getElementById('newIssueContext');
1361
+ const prioritySelect = document.getElementById('newIssuePriority');
1362
+
1363
+ const issueId = idInput?.value?.trim();
1364
+ const title = titleInput?.value?.trim();
1365
+ const context = contextInput?.value?.trim();
1366
+ const priority = parseInt(prioritySelect?.value || '3');
1367
+
1368
+ if (!issueId) {
1369
+ showNotification(t('issues.idRequired') || 'Issue ID is required', 'error');
1370
+ idInput?.focus();
1371
+ return;
1372
+ }
1373
+
1374
+ if (!title) {
1375
+ showNotification(t('issues.titleRequired') || 'Title is required', 'error');
1376
+ titleInput?.focus();
1377
+ return;
1378
+ }
1379
+
1380
+ try {
1381
+ const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath), {
1382
+ method: 'POST',
1383
+ headers: { 'Content-Type': 'application/json' },
1384
+ body: JSON.stringify({
1385
+ id: issueId,
1386
+ title: title,
1387
+ context: context,
1388
+ priority: priority,
1389
+ source: 'dashboard'
1390
+ })
1391
+ });
1392
+
1393
+ const result = await response.json();
1394
+
1395
+ if (!response.ok || result.error) {
1396
+ showNotification(result.error || 'Failed to create issue', 'error');
1397
+ return;
1398
+ }
1399
+
1400
+ showNotification(t('issues.created') || 'Issue created successfully', 'success');
1401
+ hideCreateIssueModal();
1402
+
1403
+ // Reload data and refresh view
1404
+ await loadIssueData();
1405
+ renderIssueView();
1406
+ } catch (err) {
1407
+ console.error('Failed to create issue:', err);
1408
+ showNotification('Failed to create issue', 'error');
1409
+ }
1410
+ }
1411
+
1412
+ // ========== Delete Issue ==========
1413
+ async function deleteIssue(issueId) {
1414
+ if (!confirm(t('issues.confirmDelete') || 'Are you sure you want to delete this issue?')) {
1415
+ return;
1416
+ }
1417
+
1418
+ try {
1419
+ const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
1420
+ method: 'DELETE'
1421
+ });
1422
+
1423
+ if (!response.ok) throw new Error('Failed to delete');
1424
+
1425
+ showNotification(t('issues.deleted') || 'Issue deleted', 'success');
1426
+ closeIssueDetail();
1427
+
1428
+ // Reload data and refresh view
1429
+ await loadIssueData();
1430
+ renderIssueView();
1431
+ } catch (err) {
1432
+ showNotification('Failed to delete issue', 'error');
1433
+ }
1434
+ }
1435
+
1436
+ // ========== Queue Operations ==========
1437
+ async function refreshQueue() {
1438
+ try {
1439
+ await loadQueueData();
1440
+ renderIssueView();
1441
+ showNotification(t('issues.queueRefreshed') || 'Queue refreshed', 'success');
1442
+ } catch (err) {
1443
+ showNotification('Failed to refresh queue', 'error');
1444
+ }
1445
+ }
1446
+
1447
+ function createExecutionQueue() {
1448
+ showQueueCommandModal();
1449
+ }
1450
+
1451
+ function showQueueCommandModal() {
1452
+ // Create modal if not exists
1453
+ let modal = document.getElementById('queueCommandModal');
1454
+ if (!modal) {
1455
+ modal = document.createElement('div');
1456
+ modal.id = 'queueCommandModal';
1457
+ modal.className = 'issue-modal';
1458
+ document.body.appendChild(modal);
1459
+ }
1460
+
1461
+ const command = 'claude /issue:queue';
1462
+ const altCommand = 'ccw issue queue';
1463
+
1464
+ modal.innerHTML = `
1465
+ <div class="issue-modal-backdrop" onclick="hideQueueCommandModal()"></div>
1466
+ <div class="issue-modal-content" style="max-width: 560px;">
1467
+ <div class="issue-modal-header">
1468
+ <h3>${t('issues.createQueue') || 'Create Execution Queue'}</h3>
1469
+ <button class="btn-icon" onclick="hideQueueCommandModal()">
1470
+ <i data-lucide="x" class="w-5 h-5"></i>
1471
+ </button>
1472
+ </div>
1473
+ <div class="issue-modal-body">
1474
+ <p class="text-sm text-muted-foreground mb-4">
1475
+ ${t('issues.queueCommandHint') || 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:'}
1476
+ </p>
1477
+
1478
+ <div class="command-option mb-3">
1479
+ <label class="text-xs font-medium text-muted-foreground mb-1 block">
1480
+ <i data-lucide="terminal" class="w-3 h-3 inline mr-1"></i>
1481
+ Claude Code CLI
1482
+ </label>
1483
+ <div class="command-box">
1484
+ <code class="command-text">${command}</code>
1485
+ <button class="btn-icon" onclick="copyCommand('${command}')" title="${t('common.copy') || 'Copy'}">
1486
+ <i data-lucide="copy" class="w-4 h-4"></i>
1487
+ </button>
1488
+ </div>
1489
+ </div>
1490
+
1491
+ <div class="command-option">
1492
+ <label class="text-xs font-medium text-muted-foreground mb-1 block">
1493
+ <i data-lucide="terminal" class="w-3 h-3 inline mr-1"></i>
1494
+ CCW CLI (${t('issues.alternative') || 'Alternative'})
1495
+ </label>
1496
+ <div class="command-box">
1497
+ <code class="command-text">${altCommand}</code>
1498
+ <button class="btn-icon" onclick="copyCommand('${altCommand}')" title="${t('common.copy') || 'Copy'}">
1499
+ <i data-lucide="copy" class="w-4 h-4"></i>
1500
+ </button>
1501
+ </div>
1502
+ </div>
1503
+
1504
+ <div class="command-info mt-4">
1505
+ <p class="text-xs text-muted-foreground">
1506
+ <i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
1507
+ ${t('issues.queueCommandInfo') || 'After running the command, click "Refresh" to see the updated queue.'}
1508
+ </p>
1509
+ </div>
1510
+ </div>
1511
+ <div class="issue-modal-footer">
1512
+ <button class="btn-secondary" onclick="hideQueueCommandModal()">${t('common.close') || 'Close'}</button>
1513
+ <button class="btn-primary" onclick="hideQueueCommandModal(); refreshQueue();">
1514
+ <i data-lucide="refresh-cw" class="w-4 h-4"></i>
1515
+ ${t('issues.refreshAfter') || 'Refresh Queue'}
1516
+ </button>
1517
+ </div>
1518
+ </div>
1519
+ `;
1520
+
1521
+ modal.classList.remove('hidden');
1522
+ lucide.createIcons();
1523
+ }
1524
+
1525
+ function hideQueueCommandModal() {
1526
+ const modal = document.getElementById('queueCommandModal');
1527
+ if (modal) {
1528
+ modal.classList.add('hidden');
1529
+ }
1530
+ }
1531
+
1532
+ function copyCommand(command) {
1533
+ navigator.clipboard.writeText(command).then(() => {
1534
+ showNotification(t('common.copied') || 'Copied to clipboard', 'success');
1535
+ }).catch(err => {
1536
+ console.error('Failed to copy:', err);
1537
+ // Fallback: select text
1538
+ const textArea = document.createElement('textarea');
1539
+ textArea.value = command;
1540
+ document.body.appendChild(textArea);
1541
+ textArea.select();
1542
+ document.execCommand('copy');
1543
+ document.body.removeChild(textArea);
1544
+ showNotification(t('common.copied') || 'Copied to clipboard', 'success');
1545
+ });
1546
+ }