claude-code-workflow 6.3.9 → 6.3.11

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 (91) hide show
  1. package/.claude/CLAUDE.md +1 -1
  2. package/.claude/agents/issue-plan-agent.md +21 -15
  3. package/.claude/agents/issue-queue-agent.md +114 -87
  4. package/.claude/commands/issue/discover.md +427 -0
  5. package/.claude/commands/issue/execute.md +195 -363
  6. package/.claude/commands/issue/new.md +13 -1
  7. package/.claude/commands/issue/plan.md +55 -32
  8. package/.claude/commands/issue/queue.md +145 -71
  9. package/.claude/commands/workflow/init.md +75 -29
  10. package/.claude/commands/workflow/lite-fix.md +8 -0
  11. package/.claude/commands/workflow/lite-plan.md +8 -0
  12. package/.claude/commands/workflow/review-module-cycle.md +4 -0
  13. package/.claude/commands/workflow/review-session-cycle.md +4 -0
  14. package/.claude/commands/workflow/review.md +4 -4
  15. package/.claude/commands/workflow/session/solidify.md +299 -0
  16. package/.claude/commands/workflow/session/start.md +10 -7
  17. package/.claude/commands/workflow/tools/context-gather.md +17 -10
  18. package/.claude/skills/software-manual/SKILL.md +184 -0
  19. package/.claude/skills/software-manual/phases/01-requirements-discovery.md +162 -0
  20. package/.claude/skills/software-manual/phases/02-project-exploration.md +101 -0
  21. package/.claude/skills/software-manual/phases/02.5-api-extraction.md +161 -0
  22. package/.claude/skills/software-manual/phases/03-parallel-analysis.md +183 -0
  23. package/.claude/skills/software-manual/phases/03.5-consolidation.md +82 -0
  24. package/.claude/skills/software-manual/phases/04-screenshot-capture.md +89 -0
  25. package/.claude/skills/software-manual/phases/05-html-assembly.md +132 -0
  26. package/.claude/skills/software-manual/phases/06-iterative-refinement.md +259 -0
  27. package/.claude/skills/software-manual/scripts/api-extractor.md +245 -0
  28. package/.claude/skills/software-manual/scripts/bundle-libraries.md +85 -0
  29. package/.claude/skills/software-manual/scripts/extract_apis.py +270 -0
  30. package/.claude/skills/software-manual/scripts/screenshot-helper.md +447 -0
  31. package/.claude/skills/software-manual/scripts/swagger-runner.md +419 -0
  32. package/.claude/skills/software-manual/scripts/typedoc-runner.md +357 -0
  33. package/.claude/skills/software-manual/specs/html-template.md +325 -0
  34. package/.claude/skills/software-manual/specs/quality-standards.md +253 -0
  35. package/.claude/skills/software-manual/specs/writing-style.md +298 -0
  36. package/.claude/skills/software-manual/templates/css/wiki-base.css +788 -0
  37. package/.claude/skills/software-manual/templates/css/wiki-dark.css +278 -0
  38. package/.claude/skills/software-manual/templates/tiddlywiki-shell.html +327 -0
  39. package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +219 -0
  40. package/.claude/workflows/cli-templates/schemas/discovery-state-schema.json +125 -0
  41. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +168 -74
  42. package/.claude/workflows/cli-templates/schemas/queue-schema.json +225 -108
  43. package/.claude/workflows/cli-templates/schemas/solution-schema.json +6 -28
  44. package/.claude/workflows/context-tools.md +17 -25
  45. package/.codex/AGENTS.md +10 -5
  46. package/.codex/prompts/issue-execute.md +174 -84
  47. package/.codex/prompts/issue-plan.md +106 -0
  48. package/.codex/prompts/issue-queue.md +225 -0
  49. package/ccw/dist/cli.d.ts.map +1 -1
  50. package/ccw/dist/cli.js +1 -0
  51. package/ccw/dist/cli.js.map +1 -1
  52. package/ccw/dist/commands/issue.d.ts.map +1 -1
  53. package/ccw/dist/commands/issue.js +443 -123
  54. package/ccw/dist/commands/issue.js.map +1 -1
  55. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  56. package/ccw/dist/core/dashboard-generator.js +4 -1
  57. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  58. package/ccw/dist/core/data-aggregator.d.ts +32 -0
  59. package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
  60. package/ccw/dist/core/data-aggregator.js +55 -11
  61. package/ccw/dist/core/data-aggregator.js.map +1 -1
  62. package/ccw/dist/core/routes/discovery-routes.d.ts +37 -0
  63. package/ccw/dist/core/routes/discovery-routes.d.ts.map +1 -0
  64. package/ccw/dist/core/routes/discovery-routes.js +514 -0
  65. package/ccw/dist/core/routes/discovery-routes.js.map +1 -0
  66. package/ccw/dist/core/server.d.ts.map +1 -1
  67. package/ccw/dist/core/server.js +9 -1
  68. package/ccw/dist/core/server.js.map +1 -1
  69. package/ccw/dist/tools/codex-lens.d.ts +12 -1
  70. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  71. package/ccw/dist/tools/codex-lens.js +56 -7
  72. package/ccw/dist/tools/codex-lens.js.map +1 -1
  73. package/ccw/src/cli.ts +1 -0
  74. package/ccw/src/commands/issue.ts +498 -158
  75. package/ccw/src/core/dashboard-generator.ts +4 -1
  76. package/ccw/src/core/data-aggregator.ts +94 -11
  77. package/ccw/src/core/routes/discovery-routes.ts +607 -0
  78. package/ccw/src/core/server.ts +9 -1
  79. package/ccw/src/templates/dashboard-css/34-discovery.css +783 -0
  80. package/ccw/src/templates/dashboard-js/components/cli-status.js +1 -78
  81. package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
  82. package/ccw/src/templates/dashboard-js/i18n.js +140 -4
  83. package/ccw/src/templates/dashboard-js/views/cli-manager.js +0 -18
  84. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +13 -3
  85. package/ccw/src/templates/dashboard-js/views/issue-discovery.js +730 -0
  86. package/ccw/src/templates/dashboard-js/views/issue-manager.js +57 -26
  87. package/ccw/src/templates/dashboard-js/views/project-overview.js +153 -0
  88. package/ccw/src/templates/dashboard.html +5 -0
  89. package/ccw/src/tools/codex-lens.ts +75 -9
  90. package/package.json +1 -1
  91. package/.claude/workflows/context-tools-ace.md +0 -105
@@ -0,0 +1,730 @@
1
+ // ==========================================
2
+ // ISSUE DISCOVERY VIEW
3
+ // Manages discovery sessions and findings
4
+ // ==========================================
5
+
6
+ // ========== Discovery State ==========
7
+ var discoveryData = {
8
+ discoveries: [],
9
+ selectedDiscovery: null,
10
+ selectedFinding: null,
11
+ findings: [],
12
+ perspectiveFilter: 'all',
13
+ priorityFilter: 'all',
14
+ searchQuery: '',
15
+ selectedFindings: new Set(),
16
+ viewMode: 'list' // 'list' | 'detail'
17
+ };
18
+ var discoveryLoading = false;
19
+ var discoveryPollingInterval = null;
20
+
21
+ // ========== Helper Functions ==========
22
+ function getFilteredFindings() {
23
+ const findings = discoveryData.findings || [];
24
+ let filtered = findings;
25
+
26
+ if (discoveryData.perspectiveFilter !== 'all') {
27
+ filtered = filtered.filter(f => f.perspective === discoveryData.perspectiveFilter);
28
+ }
29
+ if (discoveryData.priorityFilter !== 'all') {
30
+ filtered = filtered.filter(f => f.priority === discoveryData.priorityFilter);
31
+ }
32
+ if (discoveryData.searchQuery) {
33
+ const q = discoveryData.searchQuery.toLowerCase();
34
+ filtered = filtered.filter(f =>
35
+ (f.title && f.title.toLowerCase().includes(q)) ||
36
+ (f.file && f.file.toLowerCase().includes(q)) ||
37
+ (f.description && f.description.toLowerCase().includes(q))
38
+ );
39
+ }
40
+ return filtered;
41
+ }
42
+
43
+ // ========== Main Render Function ==========
44
+ async function renderIssueDiscovery() {
45
+ const container = document.getElementById('mainContent');
46
+ if (!container) return;
47
+
48
+ // Hide stats grid and carousel
49
+ hideStatsAndCarousel();
50
+
51
+ // Show loading state
52
+ container.innerHTML = '<div class="discovery-manager loading">' +
53
+ '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
54
+ '<p>' + t('common.loading') + '</p>' +
55
+ '</div>';
56
+ lucide.createIcons();
57
+
58
+ // Load data
59
+ await loadDiscoveryData();
60
+
61
+ // Render the main view
62
+ renderDiscoveryView();
63
+ }
64
+
65
+ // ========== Data Loading ==========
66
+ async function loadDiscoveryData() {
67
+ discoveryLoading = true;
68
+ try {
69
+ const response = await fetch('/api/discoveries?path=' + encodeURIComponent(projectPath));
70
+ if (!response.ok) throw new Error('Failed to load discoveries');
71
+ const data = await response.json();
72
+ discoveryData.discoveries = data.discoveries || [];
73
+ updateDiscoveryBadge();
74
+ } catch (err) {
75
+ console.error('Failed to load discoveries:', err);
76
+ discoveryData.discoveries = [];
77
+ } finally {
78
+ discoveryLoading = false;
79
+ }
80
+ }
81
+
82
+ async function loadDiscoveryDetail(discoveryId) {
83
+ try {
84
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath));
85
+ if (!response.ok) throw new Error('Failed to load discovery detail');
86
+ return await response.json();
87
+ } catch (err) {
88
+ console.error('Failed to load discovery detail:', err);
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async function loadDiscoveryFindings(discoveryId) {
94
+ try {
95
+ let url = '/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings?path=' + encodeURIComponent(projectPath);
96
+ if (discoveryData.perspectiveFilter !== 'all') {
97
+ url += '&perspective=' + encodeURIComponent(discoveryData.perspectiveFilter);
98
+ }
99
+ if (discoveryData.priorityFilter !== 'all') {
100
+ url += '&priority=' + encodeURIComponent(discoveryData.priorityFilter);
101
+ }
102
+ const response = await fetch(url);
103
+ if (!response.ok) throw new Error('Failed to load findings');
104
+ const data = await response.json();
105
+ return data.findings || [];
106
+ } catch (err) {
107
+ console.error('Failed to load findings:', err);
108
+ return [];
109
+ }
110
+ }
111
+
112
+ async function loadDiscoveryProgress(discoveryId) {
113
+ try {
114
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/progress?path=' + encodeURIComponent(projectPath));
115
+ if (!response.ok) return null;
116
+ return await response.json();
117
+ } catch (err) {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ function updateDiscoveryBadge() {
123
+ const badge = document.getElementById('badgeDiscovery');
124
+ if (badge) {
125
+ badge.textContent = discoveryData.discoveries.length;
126
+ }
127
+ }
128
+
129
+ // ========== Main View Render ==========
130
+ function renderDiscoveryView() {
131
+ const container = document.getElementById('mainContent');
132
+ if (!container) return;
133
+
134
+ container.innerHTML = `
135
+ <div class="discovery-manager">
136
+ <!-- Header -->
137
+ <div class="discovery-header mb-6">
138
+ <div class="flex items-center justify-between flex-wrap gap-4">
139
+ <div class="flex items-center gap-3">
140
+ <div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
141
+ <i data-lucide="search-code" class="w-5 h-5 text-primary"></i>
142
+ </div>
143
+ <div>
144
+ <h2 class="text-lg font-semibold text-foreground">${t('discovery.title') || 'Issue Discovery'}</h2>
145
+ <p class="text-sm text-muted-foreground">${t('discovery.description') || 'Discover potential issues from multiple perspectives'}</p>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="flex items-center gap-3">
150
+ ${discoveryData.viewMode === 'detail' ? `
151
+ <button class="discovery-back-btn" onclick="backToDiscoveryList()">
152
+ <i data-lucide="arrow-left" class="w-4 h-4"></i>
153
+ <span>${t('common.back') || 'Back'}</span>
154
+ </button>
155
+ ` : ''}
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ ${discoveryData.viewMode === 'list' ? renderDiscoveryListSection() : renderDiscoveryDetailSection()}
161
+ </div>
162
+ `;
163
+
164
+ lucide.createIcons();
165
+ }
166
+
167
+ // ========== Discovery List Section ==========
168
+ function renderDiscoveryListSection() {
169
+ const discoveries = discoveryData.discoveries || [];
170
+
171
+ if (discoveries.length === 0) {
172
+ return `
173
+ <div class="discovery-empty">
174
+ <div class="empty-icon">
175
+ <i data-lucide="search-x" class="w-12 h-12 text-muted-foreground"></i>
176
+ </div>
177
+ <h3 class="text-lg font-medium text-foreground mt-4">${t('discovery.noDiscoveries') || 'No discoveries yet'}</h3>
178
+ <p class="text-sm text-muted-foreground mt-2">${t('discovery.runCommand') || 'Run /issue:discover to start discovering issues'}</p>
179
+ <div class="mt-4 p-3 bg-muted/50 rounded-lg">
180
+ <code class="text-sm text-primary">/issue:discover src/auth/**</code>
181
+ </div>
182
+ </div>
183
+ `;
184
+ }
185
+
186
+ return `
187
+ <div class="discovery-list-container">
188
+ ${discoveries.map(d => renderDiscoveryCard(d)).join('')}
189
+ </div>
190
+ `;
191
+ }
192
+
193
+ function renderDiscoveryCard(discovery) {
194
+ const { discovery_id, target_pattern, perspectives, phase, total_findings, issues_generated, priority_distribution, progress } = discovery;
195
+
196
+ const isComplete = phase === 'complete';
197
+ const isRunning = phase && phase !== 'complete' && phase !== 'failed';
198
+
199
+ // Calculate progress percentage
200
+ let progressPercent = 0;
201
+ if (progress && progress.perspective_analysis) {
202
+ progressPercent = progress.perspective_analysis.percent_complete || 0;
203
+ } else if (isComplete) {
204
+ progressPercent = 100;
205
+ }
206
+
207
+ // Priority distribution bar
208
+ const critical = priority_distribution?.critical || 0;
209
+ const high = priority_distribution?.high || 0;
210
+ const medium = priority_distribution?.medium || 0;
211
+ const low = priority_distribution?.low || 0;
212
+ const total = critical + high + medium + low || 1;
213
+
214
+ return `
215
+ <div class="discovery-card ${isComplete ? 'complete' : ''} ${isRunning ? 'running' : ''}" onclick="viewDiscoveryDetail('${discovery_id}')">
216
+ <div class="discovery-card-header">
217
+ <div class="discovery-id">
218
+ <i data-lucide="search" class="w-4 h-4"></i>
219
+ <span>${discovery_id}</span>
220
+ </div>
221
+ <span class="discovery-phase ${phase}">${phase || 'unknown'}</span>
222
+ </div>
223
+
224
+ <div class="discovery-card-body">
225
+ <div class="discovery-target">
226
+ <i data-lucide="folder" class="w-4 h-4 text-muted-foreground"></i>
227
+ <span class="text-sm text-foreground">${target_pattern || 'N/A'}</span>
228
+ </div>
229
+
230
+ ${perspectives && perspectives.length > 0 ? `
231
+ <div class="discovery-perspectives">
232
+ ${perspectives.slice(0, 5).map(p => `<span class="perspective-badge ${p}">${p}</span>`).join('')}
233
+ ${perspectives.length > 5 ? `<span class="perspective-badge more">+${perspectives.length - 5}</span>` : ''}
234
+ </div>
235
+ ` : ''}
236
+
237
+ ${isRunning ? `
238
+ <div class="discovery-progress-bar">
239
+ <div class="progress-fill" style="width: ${progressPercent}%"></div>
240
+ </div>
241
+ <div class="text-xs text-muted-foreground mt-1">${progressPercent}% complete</div>
242
+ ` : ''}
243
+
244
+ <div class="discovery-stats">
245
+ <div class="stat">
246
+ <span class="stat-value">${total_findings || 0}</span>
247
+ <span class="stat-label">${t('discovery.findings') || 'Findings'}</span>
248
+ </div>
249
+ <div class="stat">
250
+ <span class="stat-value">${issues_generated || 0}</span>
251
+ <span class="stat-label">${t('discovery.exported') || 'Exported'}</span>
252
+ </div>
253
+ </div>
254
+
255
+ ${total_findings > 0 ? `
256
+ <div class="discovery-priority-bar">
257
+ <div class="priority-segment critical" style="width: ${(critical / total) * 100}%" title="Critical: ${critical}"></div>
258
+ <div class="priority-segment high" style="width: ${(high / total) * 100}%" title="High: ${high}"></div>
259
+ <div class="priority-segment medium" style="width: ${(medium / total) * 100}%" title="Medium: ${medium}"></div>
260
+ <div class="priority-segment low" style="width: ${(low / total) * 100}%" title="Low: ${low}"></div>
261
+ </div>
262
+ ` : ''}
263
+ </div>
264
+
265
+ <div class="discovery-card-footer">
266
+ <button class="discovery-action-btn" onclick="event.stopPropagation(); deleteDiscovery('${discovery_id}')">
267
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
268
+ </button>
269
+ </div>
270
+ </div>
271
+ `;
272
+ }
273
+
274
+ // ========== Discovery Detail Section ==========
275
+ function renderDiscoveryDetailSection() {
276
+ const discovery = discoveryData.selectedDiscovery;
277
+ if (!discovery) {
278
+ return '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>';
279
+ }
280
+
281
+ const findings = discoveryData.findings || [];
282
+ const perspectives = [...new Set(findings.map(f => f.perspective))];
283
+ const filteredFindings = getFilteredFindings();
284
+
285
+ return `
286
+ <div class="discovery-detail-container">
287
+ <!-- Left Panel: Findings List -->
288
+ <div class="discovery-findings-panel">
289
+ <!-- Toolbar -->
290
+ <div class="discovery-toolbar">
291
+ <div class="toolbar-filters">
292
+ <select class="filter-select" onchange="filterDiscoveryByPerspective(this.value)">
293
+ <option value="all" ${discoveryData.perspectiveFilter === 'all' ? 'selected' : ''}>${t('discovery.allPerspectives') || 'All Perspectives'}</option>
294
+ ${perspectives.map(p => `<option value="${p}" ${discoveryData.perspectiveFilter === p ? 'selected' : ''}>${p}</option>`).join('')}
295
+ </select>
296
+ <select class="filter-select" onchange="filterDiscoveryByPriority(this.value)">
297
+ <option value="all" ${discoveryData.priorityFilter === 'all' ? 'selected' : ''}>${t('discovery.allPriorities') || 'All Priorities'}</option>
298
+ <option value="critical" ${discoveryData.priorityFilter === 'critical' ? 'selected' : ''}>Critical</option>
299
+ <option value="high" ${discoveryData.priorityFilter === 'high' ? 'selected' : ''}>High</option>
300
+ <option value="medium" ${discoveryData.priorityFilter === 'medium' ? 'selected' : ''}>Medium</option>
301
+ <option value="low" ${discoveryData.priorityFilter === 'low' ? 'selected' : ''}>Low</option>
302
+ </select>
303
+ </div>
304
+ <div class="toolbar-search">
305
+ <i data-lucide="search" class="w-4 h-4"></i>
306
+ <input type="text" placeholder="${t('common.search') || 'Search...'}"
307
+ value="${discoveryData.searchQuery}"
308
+ oninput="searchDiscoveryFindings(this.value)">
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Findings Count -->
313
+ <div class="findings-count">
314
+ <div class="findings-count-left">
315
+ <span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
316
+ ${discoveryData.selectedFindings.size > 0 ? `
317
+ <span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
318
+ ` : ''}
319
+ </div>
320
+ <div class="findings-count-actions">
321
+ <button class="select-action-btn" onclick="selectAllFindings()">
322
+ <i data-lucide="check-square" class="w-3 h-3"></i>
323
+ <span>${t('discovery.selectAll') || 'Select All'}</span>
324
+ </button>
325
+ <button class="select-action-btn" onclick="deselectAllFindings()">
326
+ <i data-lucide="square" class="w-3 h-3"></i>
327
+ <span>${t('discovery.deselectAll') || 'Deselect All'}</span>
328
+ </button>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- Findings List -->
333
+ <div class="findings-list">
334
+ ${filteredFindings.length === 0 ? `
335
+ <div class="findings-empty">
336
+ <i data-lucide="inbox" class="w-8 h-8 text-muted-foreground"></i>
337
+ <p>${t('discovery.noFindings') || 'No findings match your filters'}</p>
338
+ </div>
339
+ ` : filteredFindings.map(f => renderFindingItem(f)).join('')}
340
+ </div>
341
+
342
+ <!-- Bulk Actions -->
343
+ ${discoveryData.selectedFindings.size > 0 ? `
344
+ <div class="bulk-actions">
345
+ <span class="bulk-count">${discoveryData.selectedFindings.size} selected</span>
346
+ <button class="bulk-action-btn export" onclick="exportSelectedFindings()">
347
+ <i data-lucide="upload" class="w-4 h-4"></i>
348
+ <span>${t('discovery.exportAsIssues') || 'Export as Issues'}</span>
349
+ </button>
350
+ <button class="bulk-action-btn dismiss" onclick="dismissSelectedFindings()">
351
+ <i data-lucide="x" class="w-4 h-4"></i>
352
+ <span>${t('discovery.dismiss') || 'Dismiss'}</span>
353
+ </button>
354
+ </div>
355
+ ` : ''}
356
+ </div>
357
+
358
+ <!-- Right Panel: Finding Preview -->
359
+ <div class="discovery-preview-panel">
360
+ ${discoveryData.selectedFinding ? renderFindingPreview(discoveryData.selectedFinding) : `
361
+ <div class="preview-empty">
362
+ <i data-lucide="mouse-pointer-click" class="w-12 h-12 text-muted-foreground"></i>
363
+ <p>${t('discovery.selectFinding') || 'Select a finding to preview'}</p>
364
+ </div>
365
+ `}
366
+ </div>
367
+ </div>
368
+ `;
369
+ }
370
+
371
+ function renderFindingItem(finding) {
372
+ const isSelected = discoveryData.selectedFindings.has(finding.id);
373
+ const isActive = discoveryData.selectedFinding?.id === finding.id;
374
+ const isExported = finding.exported === true;
375
+
376
+ return `
377
+ <div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''} ${isExported ? 'exported' : ''}"
378
+ onclick="selectFinding('${finding.id}')">
379
+ <div class="finding-checkbox" onclick="event.stopPropagation(); toggleFindingSelection('${finding.id}')">
380
+ <input type="checkbox" ${isSelected ? 'checked' : ''} ${isExported ? 'disabled' : ''}>
381
+ </div>
382
+ <div class="finding-content">
383
+ <div class="finding-header">
384
+ <span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
385
+ <span class="priority-badge ${finding.priority}">${finding.priority}</span>
386
+ ${isExported ? '<span class="exported-badge">' + (t('discovery.exported') || 'Exported') + '</span>' : ''}
387
+ </div>
388
+ <div class="finding-title">${finding.title || 'Untitled'}</div>
389
+ <div class="finding-location">
390
+ <i data-lucide="file" class="w-3 h-3"></i>
391
+ <span>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ `;
396
+ }
397
+
398
+ function renderFindingPreview(finding) {
399
+ return `
400
+ <div class="finding-preview">
401
+ <div class="preview-header">
402
+ <div class="preview-badges">
403
+ <span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
404
+ <span class="priority-badge ${finding.priority}">${finding.priority}</span>
405
+ ${finding.confidence ? `<span class="confidence-badge">${Math.round(finding.confidence * 100)}% confidence</span>` : ''}
406
+ </div>
407
+ <h3 class="preview-title">${finding.title || 'Untitled'}</h3>
408
+ </div>
409
+
410
+ <div class="preview-section">
411
+ <h4><i data-lucide="file-code" class="w-4 h-4"></i> ${t('discovery.location') || 'Location'}</h4>
412
+ <div class="preview-location">
413
+ <code>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</code>
414
+ </div>
415
+ </div>
416
+
417
+ ${finding.snippet ? `
418
+ <div class="preview-section">
419
+ <h4><i data-lucide="code" class="w-4 h-4"></i> ${t('discovery.code') || 'Code'}</h4>
420
+ <pre class="preview-snippet"><code>${escapeHtml(finding.snippet)}</code></pre>
421
+ </div>
422
+ ` : ''}
423
+
424
+ <div class="preview-section">
425
+ <h4><i data-lucide="info" class="w-4 h-4"></i> ${t('discovery.description') || 'Description'}</h4>
426
+ <p class="preview-description">${finding.description || 'No description'}</p>
427
+ </div>
428
+
429
+ ${finding.impact ? `
430
+ <div class="preview-section">
431
+ <h4><i data-lucide="alert-triangle" class="w-4 h-4"></i> ${t('discovery.impact') || 'Impact'}</h4>
432
+ <p class="preview-impact">${finding.impact}</p>
433
+ </div>
434
+ ` : ''}
435
+
436
+ ${finding.recommendation ? `
437
+ <div class="preview-section">
438
+ <h4><i data-lucide="lightbulb" class="w-4 h-4"></i> ${t('discovery.recommendation') || 'Recommendation'}</h4>
439
+ <p class="preview-recommendation">${finding.recommendation}</p>
440
+ </div>
441
+ ` : ''}
442
+
443
+ ${finding.suggested_issue ? `
444
+ <div class="preview-section suggested-issue">
445
+ <h4><i data-lucide="clipboard-list" class="w-4 h-4"></i> ${t('discovery.suggestedIssue') || 'Suggested Issue'}</h4>
446
+ <div class="suggested-issue-content">
447
+ <div class="issue-title">${finding.suggested_issue.title || finding.title}</div>
448
+ <div class="issue-meta">
449
+ <span class="issue-type">${finding.suggested_issue.type || 'bug'}</span>
450
+ <span class="issue-priority">P${finding.suggested_issue.priority || 3}</span>
451
+ ${finding.suggested_issue.labels ? finding.suggested_issue.labels.map(l => `<span class="issue-label">${l}</span>`).join('') : ''}
452
+ </div>
453
+ </div>
454
+ </div>
455
+ ` : ''}
456
+
457
+ <div class="preview-actions">
458
+ <button class="preview-action-btn primary" onclick="exportSingleFinding('${finding.id}')">
459
+ <i data-lucide="upload" class="w-4 h-4"></i>
460
+ <span>${t('discovery.exportAsIssue') || 'Export as Issue'}</span>
461
+ </button>
462
+ <button class="preview-action-btn secondary" onclick="dismissFinding('${finding.id}')">
463
+ <i data-lucide="x" class="w-4 h-4"></i>
464
+ <span>${t('discovery.dismiss') || 'Dismiss'}</span>
465
+ </button>
466
+ </div>
467
+ </div>
468
+ `;
469
+ }
470
+
471
+ // ========== Actions ==========
472
+ async function viewDiscoveryDetail(discoveryId) {
473
+ discoveryData.viewMode = 'detail';
474
+ discoveryData.selectedFinding = null;
475
+ discoveryData.selectedFindings.clear();
476
+ discoveryData.perspectiveFilter = 'all';
477
+ discoveryData.priorityFilter = 'all';
478
+ discoveryData.searchQuery = '';
479
+
480
+ // Show loading
481
+ renderDiscoveryView();
482
+
483
+ // Load detail
484
+ const detail = await loadDiscoveryDetail(discoveryId);
485
+ if (detail) {
486
+ discoveryData.selectedDiscovery = detail;
487
+ // Flatten findings from perspectives
488
+ const allFindings = [];
489
+ if (detail.perspectives) {
490
+ for (const p of detail.perspectives) {
491
+ if (p.findings) {
492
+ allFindings.push(...p.findings);
493
+ }
494
+ }
495
+ }
496
+ discoveryData.findings = allFindings;
497
+ }
498
+
499
+ // Start polling if running
500
+ if (detail && detail.phase && detail.phase !== 'complete' && detail.phase !== 'failed') {
501
+ startDiscoveryPolling(discoveryId);
502
+ }
503
+
504
+ renderDiscoveryView();
505
+ }
506
+
507
+ function backToDiscoveryList() {
508
+ stopDiscoveryPolling();
509
+ discoveryData.viewMode = 'list';
510
+ discoveryData.selectedDiscovery = null;
511
+ discoveryData.selectedFinding = null;
512
+ discoveryData.findings = [];
513
+ discoveryData.selectedFindings.clear();
514
+ renderDiscoveryView();
515
+ }
516
+
517
+ function selectFinding(findingId) {
518
+ const finding = discoveryData.findings.find(f => f.id === findingId);
519
+ discoveryData.selectedFinding = finding || null;
520
+ renderDiscoveryView();
521
+ }
522
+
523
+ function toggleFindingSelection(findingId) {
524
+ if (discoveryData.selectedFindings.has(findingId)) {
525
+ discoveryData.selectedFindings.delete(findingId);
526
+ } else {
527
+ discoveryData.selectedFindings.add(findingId);
528
+ }
529
+ renderDiscoveryView();
530
+ }
531
+
532
+ function selectAllFindings() {
533
+ // Get filtered findings (respecting current filters)
534
+ const filteredFindings = getFilteredFindings();
535
+ // Select only non-exported findings
536
+ for (const finding of filteredFindings) {
537
+ if (!finding.exported) {
538
+ discoveryData.selectedFindings.add(finding.id);
539
+ }
540
+ }
541
+ renderDiscoveryView();
542
+ }
543
+
544
+ function deselectAllFindings() {
545
+ discoveryData.selectedFindings.clear();
546
+ renderDiscoveryView();
547
+ }
548
+
549
+ function filterDiscoveryByPerspective(perspective) {
550
+ discoveryData.perspectiveFilter = perspective;
551
+ renderDiscoveryView();
552
+ }
553
+
554
+ function filterDiscoveryByPriority(priority) {
555
+ discoveryData.priorityFilter = priority;
556
+ renderDiscoveryView();
557
+ }
558
+
559
+ function searchDiscoveryFindings(query) {
560
+ discoveryData.searchQuery = query;
561
+ renderDiscoveryView();
562
+ }
563
+
564
+ async function exportSelectedFindings() {
565
+ if (discoveryData.selectedFindings.size === 0) return;
566
+
567
+ const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
568
+ if (!discoveryId) return;
569
+
570
+ try {
571
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
572
+ method: 'POST',
573
+ headers: { 'Content-Type': 'application/json' },
574
+ body: JSON.stringify({ finding_ids: Array.from(discoveryData.selectedFindings) })
575
+ });
576
+
577
+ const result = await response.json();
578
+ if (result.success) {
579
+ // Show detailed message if duplicates were skipped
580
+ const msg = result.skipped_count > 0
581
+ ? `Exported ${result.exported_count} issues, skipped ${result.skipped_count} duplicates`
582
+ : `Exported ${result.exported_count} issues`;
583
+ showNotification('success', msg);
584
+ discoveryData.selectedFindings.clear();
585
+ // Reload discovery data to reflect exported status
586
+ await loadDiscoveryData();
587
+ if (discoveryData.selectedDiscovery) {
588
+ await viewDiscoveryDetail(discoveryData.selectedDiscovery.discovery_id);
589
+ } else {
590
+ renderDiscoveryView();
591
+ }
592
+ } else {
593
+ showNotification('error', result.error || 'Export failed');
594
+ }
595
+ } catch (err) {
596
+ console.error('Export failed:', err);
597
+ showNotification('error', 'Export failed');
598
+ }
599
+ }
600
+
601
+ async function exportSingleFinding(findingId) {
602
+ const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
603
+ if (!discoveryId) return;
604
+
605
+ try {
606
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
607
+ method: 'POST',
608
+ headers: { 'Content-Type': 'application/json' },
609
+ body: JSON.stringify({ finding_ids: [findingId] })
610
+ });
611
+
612
+ const result = await response.json();
613
+ if (result.success) {
614
+ showNotification('success', 'Exported 1 issue');
615
+ // Reload discovery data
616
+ await loadDiscoveryData();
617
+ renderDiscoveryView();
618
+ } else {
619
+ showNotification('error', result.error || 'Export failed');
620
+ }
621
+ } catch (err) {
622
+ console.error('Export failed:', err);
623
+ showNotification('error', 'Export failed');
624
+ }
625
+ }
626
+
627
+ async function dismissFinding(findingId) {
628
+ const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
629
+ if (!discoveryId) return;
630
+
631
+ try {
632
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings/' + encodeURIComponent(findingId) + '?path=' + encodeURIComponent(projectPath), {
633
+ method: 'PATCH',
634
+ headers: { 'Content-Type': 'application/json' },
635
+ body: JSON.stringify({ dismissed: true })
636
+ });
637
+
638
+ const result = await response.json();
639
+ if (result.success) {
640
+ // Update local state
641
+ const finding = discoveryData.findings.find(f => f.id === findingId);
642
+ if (finding) {
643
+ finding.dismissed = true;
644
+ }
645
+ if (discoveryData.selectedFinding?.id === findingId) {
646
+ discoveryData.selectedFinding = null;
647
+ }
648
+ renderDiscoveryView();
649
+ }
650
+ } catch (err) {
651
+ console.error('Dismiss failed:', err);
652
+ }
653
+ }
654
+
655
+ async function dismissSelectedFindings() {
656
+ for (const findingId of discoveryData.selectedFindings) {
657
+ await dismissFinding(findingId);
658
+ }
659
+ discoveryData.selectedFindings.clear();
660
+ renderDiscoveryView();
661
+ }
662
+
663
+ async function deleteDiscovery(discoveryId) {
664
+ if (!confirm(`Delete discovery ${discoveryId}? This cannot be undone.`)) return;
665
+
666
+ try {
667
+ const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath), {
668
+ method: 'DELETE'
669
+ });
670
+
671
+ const result = await response.json();
672
+ if (result.success) {
673
+ showNotification('success', 'Discovery deleted');
674
+ await loadDiscoveryData();
675
+ renderDiscoveryView();
676
+ } else {
677
+ showNotification('error', result.error || 'Delete failed');
678
+ }
679
+ } catch (err) {
680
+ console.error('Delete failed:', err);
681
+ showNotification('error', 'Delete failed');
682
+ }
683
+ }
684
+
685
+ // ========== Progress Polling ==========
686
+ function startDiscoveryPolling(discoveryId) {
687
+ stopDiscoveryPolling();
688
+
689
+ discoveryPollingInterval = setInterval(async () => {
690
+ const progress = await loadDiscoveryProgress(discoveryId);
691
+ if (progress) {
692
+ // Update progress in UI
693
+ if (discoveryData.selectedDiscovery) {
694
+ discoveryData.selectedDiscovery.progress = progress.progress;
695
+ discoveryData.selectedDiscovery.phase = progress.phase;
696
+ }
697
+
698
+ // Stop polling if complete
699
+ if (progress.phase === 'complete' || progress.phase === 'failed') {
700
+ stopDiscoveryPolling();
701
+ // Reload full detail
702
+ viewDiscoveryDetail(discoveryId);
703
+ }
704
+ }
705
+ }, 3000); // Poll every 3 seconds
706
+ }
707
+
708
+ function stopDiscoveryPolling() {
709
+ if (discoveryPollingInterval) {
710
+ clearInterval(discoveryPollingInterval);
711
+ discoveryPollingInterval = null;
712
+ }
713
+ }
714
+
715
+ // ========== Utilities ==========
716
+ function escapeHtml(text) {
717
+ const div = document.createElement('div');
718
+ div.textContent = text;
719
+ return div.innerHTML;
720
+ }
721
+
722
+ // ========== Cleanup ==========
723
+ function cleanupDiscoveryView() {
724
+ stopDiscoveryPolling();
725
+ discoveryData.selectedDiscovery = null;
726
+ discoveryData.selectedFinding = null;
727
+ discoveryData.findings = [];
728
+ discoveryData.selectedFindings.clear();
729
+ discoveryData.viewMode = 'list';
730
+ }