bugsage 0.2.0

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +442 -0
  3. data/CHANGELOG.md +28 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +301 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +344 -0
  8. data/ROADMAP.md +217 -0
  9. data/SECURITY.md +83 -0
  10. data/docs/AI.md +167 -0
  11. data/docs/Configuration.md +168 -0
  12. data/docs/GettingStarted.md +146 -0
  13. data/docs/RELEASE_CHECKLIST.md +346 -0
  14. data/docs/Troubleshooting.md +181 -0
  15. data/docs/images/BadRequest.png +0 -0
  16. data/docs/images/BadRequest2.png +0 -0
  17. data/docs/images/BugSage_Logo.png +0 -0
  18. data/docs/images/BugSage_Social_Preview.png +0 -0
  19. data/docs/images/NoMethodError.png +0 -0
  20. data/docs/images/NoMethodError2.png +0 -0
  21. data/docs/images/README.md +38 -0
  22. data/docs/releases/README.md +21 -0
  23. data/docs/releases/v0.2.0.md +40 -0
  24. data/exe/bugsage +7 -0
  25. data/lib/bugsage/ai_analyzer.rb +145 -0
  26. data/lib/bugsage/ai_chat.rb +388 -0
  27. data/lib/bugsage/ai_context.rb +69 -0
  28. data/lib/bugsage/ai_panel.rb +708 -0
  29. data/lib/bugsage/ai_support.rb +36 -0
  30. data/lib/bugsage/auto_configurator.rb +97 -0
  31. data/lib/bugsage/cli.rb +59 -0
  32. data/lib/bugsage/code_context.rb +81 -0
  33. data/lib/bugsage/code_patch.rb +147 -0
  34. data/lib/bugsage/configuration.rb +149 -0
  35. data/lib/bugsage/console_context.rb +51 -0
  36. data/lib/bugsage/cursor_client.rb +165 -0
  37. data/lib/bugsage/dashboard.rb +627 -0
  38. data/lib/bugsage/editor_links.rb +34 -0
  39. data/lib/bugsage/error_page.rb +298 -0
  40. data/lib/bugsage/exception_handler.rb +66 -0
  41. data/lib/bugsage/exception_support.rb +95 -0
  42. data/lib/bugsage/exceptions_app.rb +31 -0
  43. data/lib/bugsage/fix_applicator.rb +107 -0
  44. data/lib/bugsage/formatter.rb +37 -0
  45. data/lib/bugsage/http_error_capture.rb +104 -0
  46. data/lib/bugsage/http_response_error.rb +14 -0
  47. data/lib/bugsage/inline_console.rb +226 -0
  48. data/lib/bugsage/installation.rb +94 -0
  49. data/lib/bugsage/installer.rb +66 -0
  50. data/lib/bugsage/json_endpoint.rb +34 -0
  51. data/lib/bugsage/locales/en.yml +439 -0
  52. data/lib/bugsage/middleware.rb +152 -0
  53. data/lib/bugsage/openai_client.rb +103 -0
  54. data/lib/bugsage/page_actions.rb +255 -0
  55. data/lib/bugsage/railtie.rb +49 -0
  56. data/lib/bugsage/request_context.rb +56 -0
  57. data/lib/bugsage/rule.rb +271 -0
  58. data/lib/bugsage/session_clear.rb +35 -0
  59. data/lib/bugsage/store.rb +60 -0
  60. data/lib/bugsage/suggestion.rb +58 -0
  61. data/lib/bugsage/trace_cleaner.rb +24 -0
  62. data/lib/bugsage/translations.rb +85 -0
  63. data/lib/bugsage/version.rb +5 -0
  64. data/lib/bugsage.rb +65 -0
  65. data/lib/generators/bugsage/install/install_generator.rb +21 -0
  66. data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
  67. data/scripts/publish-github-release.sh +38 -0
  68. data/sig/bugsage.rbs +4 -0
  69. metadata +157 -0
@@ -0,0 +1,627 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bugsage
6
+ class Dashboard
7
+ def self.render(suggestions)
8
+ <<~HTML
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <title>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.title"))}</title>
14
+ <style>
15
+ #{shared_styles}
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="dashboard">
20
+ <aside class="sidebar">
21
+ <header class="sidebar-header">
22
+ <h1>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.brand"))}</h1>
23
+ <p class="sidebar-subtitle">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.session_errors"))}</p>
24
+ <div class="sidebar-stats">
25
+ <span class="stat-pill">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.caught_count", count: suggestions.size))}</span>
26
+ <span class="stat-pill">#{CodeContext.escape_html(suggestions.empty? ? Bugsage.t("ui.dashboard.avg_empty") : Bugsage.t("ui.dashboard.avg_confidence", confidence: average_confidence(suggestions)))}</span>
27
+ </div>
28
+ #{PageActions.render_clear_button unless suggestions.empty?}
29
+ </header>
30
+
31
+ <div class="bug-list">
32
+ #{suggestions.empty? ? render_empty_list : suggestions.map.with_index { |event, index| render_list_item(event, index) }.join}
33
+ </div>
34
+ </aside>
35
+
36
+ <main class="detail-panel">
37
+ #{suggestions.empty? ? render_empty_detail : suggestions.map.with_index { |event, index| render_detail(event, index) }.join}
38
+ #{render_detail_script unless suggestions.empty?}
39
+ #{InlineConsole.render_script if Bugsage.configuration.show_inline_console? && !suggestions.empty?}
40
+ #{AiPanel.render_script if Bugsage.configuration.ai_configured? && !suggestions.empty?}
41
+ #{PageActions.render_script unless suggestions.empty?}
42
+ </main>
43
+ </div>
44
+ </body>
45
+ </html>
46
+ HTML
47
+ end
48
+
49
+ def self.render_empty_list
50
+ <<~HTML
51
+ <div class="empty-list">
52
+ <p>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_list_title"))}</p>
53
+ <p class="empty-hint">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_list_hint"))}</p>
54
+ </div>
55
+ HTML
56
+ end
57
+
58
+ def self.render_empty_detail
59
+ <<~HTML
60
+ <div class="detail-placeholder">
61
+ <h2>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_detail_title"))}</h2>
62
+ <p>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_detail_body"))}</p>
63
+ </div>
64
+ HTML
65
+ end
66
+
67
+ def self.render_list_item(event, index)
68
+ active_class = index.zero? ? " active" : ""
69
+
70
+ <<~HTML
71
+ <button type="button" class="bug-item#{active_class}" data-bug-id="bug-#{index}" aria-controls="bug-#{index}">
72
+ <div class="bug-item-heading">
73
+ <span class="issue">#{CodeContext.escape_html(event[:issue])}</span>
74
+ <span class="confidence">#{event[:confidence]}%</span>
75
+ </div>
76
+ <div class="location">#{CodeContext.escape_html(short_location(event[:location]))}</div>
77
+ <p class="cause-preview">#{CodeContext.escape_html(truncate(event[:root_cause], 80))}</p>
78
+ <span class="meta">#{CodeContext.escape_html(event[:timestamp])}</span>
79
+ </button>
80
+ HTML
81
+ end
82
+
83
+ def self.render_detail(event, index)
84
+ file_path, line_number = CodeContext.extract_location(event[:location])
85
+ code_context = CodeContext.render_code_context(file_path, line_number)
86
+ context = event[:context] || {}
87
+ fixes = event[:fixes] || []
88
+ active_class = index.zero? ? " active" : ""
89
+ hidden_attr = index.zero? ? "" : " hidden"
90
+
91
+ <<~HTML
92
+ <section id="bug-#{index}" class="bug-detail#{active_class}"#{hidden_attr}>
93
+ <header class="detail-header">
94
+ <h2>#{CodeContext.escape_html(event[:issue])}</h2>
95
+ <span class="confidence-badge" id="bugsage-confidence-bug-#{index}">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.confidence_badge", confidence: event[:confidence]))}</span>
96
+ #{render_source_badge(event)}
97
+ </header>
98
+
99
+ <div class="detail-meta">
100
+ <span><strong>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.location_label"))}</strong> #{CodeContext.escape_html(event[:location])}</span>
101
+ <span><strong>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.time_label"))}</strong> #{CodeContext.escape_html(event[:timestamp])}</span>
102
+ </div>
103
+
104
+ #{code_context}
105
+
106
+ <div class="message-box">
107
+ <strong>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.error_message"))}</strong>
108
+ <p>#{CodeContext.escape_html(event[:root_cause])}</p>
109
+ </div>
110
+
111
+ #{InlineConsole.render_panel(bug_index: index, include_script: false)}
112
+
113
+ #{AiPanel.render_panel(bug_index: index, include_script: false, suggestion: suggestion_from_event(event))}
114
+
115
+ <div class="detail-grid">
116
+ <div class="detail-section">
117
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.suggested_fixes"))}</div>
118
+ <ul class="fixes" id="bugsage-fixes-bug-#{index}">
119
+ #{fixes.map.with_index { |fix, fix_index| "<li#{' class=\"selected\"' if fix_index.zero?}>#{CodeContext.escape_html(fix)}</li>" }.join}
120
+ </ul>
121
+ #{PageActions.render_fix_actions(location: event[:location], suffix: "-bug-#{index}", hidden: false)}
122
+ </div>
123
+
124
+ <div class="detail-section">
125
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.analysis"))}</div>
126
+ <p class="confidence-detail">#{analysis_summary(event)}</p>
127
+ </div>
128
+ </div>
129
+
130
+ #{render_request_context(context)}
131
+ </section>
132
+ HTML
133
+ end
134
+
135
+ def self.render_detail_script
136
+ <<~HTML
137
+ <script>
138
+ (function () {
139
+ var items = document.querySelectorAll(".bug-item");
140
+ var panels = document.querySelectorAll(".bug-detail");
141
+
142
+ function showBug(id) {
143
+ items.forEach(function (item) {
144
+ item.classList.toggle("active", item.dataset.bugId === id);
145
+ });
146
+
147
+ panels.forEach(function (panel) {
148
+ var isActive = panel.id === id;
149
+ panel.classList.toggle("active", isActive);
150
+ panel.hidden = !isActive;
151
+ });
152
+ }
153
+
154
+ items.forEach(function (item) {
155
+ item.addEventListener("click", function () {
156
+ showBug(item.dataset.bugId);
157
+ });
158
+ });
159
+ })();
160
+ </script>
161
+ HTML
162
+ end
163
+
164
+ def self.render_request_context(context)
165
+ return "" if context.nil? || context.empty?
166
+
167
+ rows = context.map do |label, value|
168
+ "<div class=\"context-row\">" \
169
+ "<div class=\"context-label\">#{CodeContext.escape_html(label)}</div>" \
170
+ "<div class=\"context-value\">#{CodeContext.escape_html(format_value(value))}</div>" \
171
+ "</div>"
172
+ end.join
173
+
174
+ <<~HTML
175
+ <div class="detail-section">
176
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.rails_request_context"))}</div>
177
+ <div class="context-panel">#{rows}</div>
178
+ </div>
179
+ HTML
180
+ end
181
+
182
+ def self.format_value(value)
183
+ case value
184
+ when Hash, Array
185
+ JSON.pretty_generate(value)
186
+ when NilClass
187
+ Bugsage.t("common.not_available")
188
+ else
189
+ value.to_s
190
+ end
191
+ end
192
+
193
+ def self.analysis_summary(event)
194
+ confidence = event[:confidence]
195
+ source = event[:source].to_s
196
+ ai_error = event[:ai_error].to_s.strip
197
+
198
+ return Bugsage.t("ui.dashboard.ai_analysis_failed", error: ai_error) unless ai_error.empty?
199
+
200
+ case source
201
+ when "hybrid", "ai"
202
+ Bugsage.t("ui.dashboard.analysis_hybrid", confidence: confidence)
203
+ else
204
+ Bugsage.t("ui.dashboard.analysis_rules", confidence: confidence)
205
+ end
206
+ end
207
+
208
+ def self.suggestion_from_event(event)
209
+ Suggestion.new(
210
+ issue: event[:issue],
211
+ location: event[:location],
212
+ root_cause: event[:root_cause],
213
+ fixes: event[:fixes] || [],
214
+ confidence: event[:confidence],
215
+ source: event[:source] || :rules,
216
+ ai_notes: event[:ai_notes],
217
+ code_patch: event[:code_patch]
218
+ )
219
+ end
220
+
221
+ def self.render_source_badge(event)
222
+ return "" unless %w[hybrid ai].include?(event[:source].to_s)
223
+
224
+ "<span class=\"source-badge\">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.ai_enhanced"))}</span>"
225
+ end
226
+
227
+ def self.render_ai_notes(event)
228
+ notes = event[:ai_notes].to_s.strip
229
+ return "" if notes.empty?
230
+
231
+ <<~HTML
232
+ <div class="detail-section">
233
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.ai_notes"))}</div>
234
+ <div class="ai-notes">#{CodeContext.escape_html(notes)}</div>
235
+ </div>
236
+ HTML
237
+ end
238
+
239
+ def self.render_ai_error(event)
240
+ error = event[:ai_error].to_s.strip
241
+ return "" if error.empty?
242
+
243
+ <<~HTML
244
+ <div class="detail-section">
245
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.ai_status"))}</div>
246
+ <div class="ai-error">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.ai_error", error: error))}</div>
247
+ </div>
248
+ HTML
249
+ end
250
+
251
+ def self.short_location(location)
252
+ return Bugsage.t("common.unknown") unless location
253
+
254
+ file_path, line_number = CodeContext.extract_location(location)
255
+ return location unless file_path
256
+
257
+ file_name = File.basename(file_path)
258
+ line_number ? "#{file_name}:#{line_number}" : file_name
259
+ end
260
+
261
+ def self.truncate(text, length)
262
+ value = text.to_s
263
+ return value if value.length <= length
264
+
265
+ "#{value[0, length - 3]}..."
266
+ end
267
+
268
+ def self.average_confidence(suggestions)
269
+ return 0 if suggestions.empty?
270
+
271
+ total = suggestions.sum { |event| event[:confidence].to_i }
272
+ (total.to_f / suggestions.size).round
273
+ end
274
+
275
+ def self.shared_styles
276
+ <<~CSS
277
+ * { box-sizing: border-box; }
278
+ body {
279
+ background: #1e1e2e;
280
+ color: #cdd6f4;
281
+ font-family: 'SF Mono', Consolas, 'Courier New', monospace;
282
+ margin: 0;
283
+ min-height: 100vh;
284
+ }
285
+ .dashboard {
286
+ display: grid;
287
+ grid-template-columns: 340px 1fr;
288
+ min-height: 100vh;
289
+ }
290
+ .sidebar {
291
+ background: #181825;
292
+ border-right: 1px solid #45475a;
293
+ display: flex;
294
+ flex-direction: column;
295
+ min-height: 100vh;
296
+ }
297
+ .sidebar-header {
298
+ padding: 24px 20px;
299
+ border-bottom: 1px solid #45475a;
300
+ }
301
+ .sidebar-header h1 {
302
+ color: #f38ba8;
303
+ font-size: 22px;
304
+ margin: 0 0 4px 0;
305
+ }
306
+ .sidebar-subtitle {
307
+ color: #a6adc8;
308
+ margin: 0 0 16px 0;
309
+ font-size: 13px;
310
+ }
311
+ .sidebar-stats {
312
+ display: flex;
313
+ flex-wrap: wrap;
314
+ gap: 8px;
315
+ }
316
+ .stat-pill {
317
+ background: rgba(137, 180, 250, 0.15);
318
+ color: #89b4fa;
319
+ padding: 4px 10px;
320
+ border-radius: 999px;
321
+ font-size: 11px;
322
+ font-weight: bold;
323
+ }
324
+ .bug-list {
325
+ flex: 1;
326
+ overflow-y: auto;
327
+ padding: 12px;
328
+ }
329
+ .bug-item {
330
+ width: 100%;
331
+ text-align: left;
332
+ background: #313244;
333
+ border: 1px solid #45475a;
334
+ border-radius: 8px;
335
+ padding: 14px 16px;
336
+ margin-bottom: 10px;
337
+ cursor: pointer;
338
+ color: inherit;
339
+ font: inherit;
340
+ transition: border-color 0.15s ease, background 0.15s ease;
341
+ }
342
+ .bug-item:hover {
343
+ border-color: #89b4fa;
344
+ background: #3b3d52;
345
+ }
346
+ .bug-item.active {
347
+ border-color: #f38ba8;
348
+ background: rgba(243, 139, 168, 0.12);
349
+ box-shadow: inset 3px 0 0 #f38ba8;
350
+ }
351
+ .bug-item-heading {
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: space-between;
355
+ gap: 8px;
356
+ margin-bottom: 6px;
357
+ }
358
+ .issue {
359
+ color: #f38ba8;
360
+ font-size: 14px;
361
+ font-weight: bold;
362
+ }
363
+ .confidence {
364
+ background: #a6e3a1;
365
+ color: #1e1e2e;
366
+ padding: 2px 8px;
367
+ border-radius: 999px;
368
+ font-size: 11px;
369
+ font-weight: bold;
370
+ white-space: nowrap;
371
+ }
372
+ .location {
373
+ color: #89b4fa;
374
+ font-size: 12px;
375
+ word-break: break-all;
376
+ margin-bottom: 6px;
377
+ }
378
+ .cause-preview {
379
+ margin: 0 0 8px 0;
380
+ color: #cdd6f4;
381
+ font-size: 12px;
382
+ line-height: 1.4;
383
+ }
384
+ .meta {
385
+ color: #6c7086;
386
+ font-size: 11px;
387
+ }
388
+ .empty-list {
389
+ padding: 20px 12px;
390
+ color: #a6adc8;
391
+ text-align: center;
392
+ }
393
+ .empty-hint {
394
+ margin-top: 8px;
395
+ color: #6c7086;
396
+ font-size: 12px;
397
+ }
398
+ .detail-panel {
399
+ background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
400
+ overflow-y: auto;
401
+ padding: 32px;
402
+ min-height: 100vh;
403
+ }
404
+ .detail-placeholder {
405
+ max-width: 560px;
406
+ margin: 80px auto;
407
+ text-align: center;
408
+ color: #a6adc8;
409
+ }
410
+ .detail-placeholder h2 {
411
+ color: #f38ba8;
412
+ margin-bottom: 12px;
413
+ }
414
+ .bug-detail[hidden] { display: none; }
415
+ .sidebar-header .bugsage-clear-logs {
416
+ margin-top: 12px;
417
+ width: 100%;
418
+ }
419
+ .detail-header {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ gap: 16px;
424
+ margin-bottom: 16px;
425
+ }
426
+ .detail-header h2 {
427
+ color: #f38ba8;
428
+ font-size: 28px;
429
+ margin: 0;
430
+ }
431
+ .confidence-badge {
432
+ background: #a6e3a1;
433
+ color: #1e1e2e;
434
+ padding: 6px 14px;
435
+ border-radius: 999px;
436
+ font-size: 12px;
437
+ font-weight: bold;
438
+ white-space: nowrap;
439
+ }
440
+ .source-badge {
441
+ background: rgba(137, 180, 250, 0.2);
442
+ color: #89b4fa;
443
+ padding: 6px 14px;
444
+ border-radius: 999px;
445
+ font-size: 12px;
446
+ font-weight: bold;
447
+ white-space: nowrap;
448
+ }
449
+ .ai-notes {
450
+ background: rgba(137, 180, 250, 0.1);
451
+ border-left: 4px solid #89b4fa;
452
+ padding: 16px;
453
+ border-radius: 4px;
454
+ margin-bottom: 20px;
455
+ white-space: pre-wrap;
456
+ }
457
+ .ai-error {
458
+ background: rgba(250, 179, 135, 0.1);
459
+ border-left: 4px solid #fab387;
460
+ padding: 16px;
461
+ border-radius: 4px;
462
+ margin-bottom: 20px;
463
+ color: #f9e2af;
464
+ white-space: pre-wrap;
465
+ }
466
+ .detail-meta {
467
+ display: flex;
468
+ flex-wrap: wrap;
469
+ gap: 16px 24px;
470
+ color: #a6adc8;
471
+ font-size: 13px;
472
+ margin-bottom: 20px;
473
+ }
474
+ .message-box {
475
+ background: rgba(243, 139, 168, 0.1);
476
+ border-left: 4px solid #f38ba8;
477
+ padding: 16px;
478
+ border-radius: 4px;
479
+ margin-bottom: 20px;
480
+ color: #f5c2e7;
481
+ }
482
+ .message-box strong {
483
+ display: block;
484
+ margin-bottom: 8px;
485
+ }
486
+ .message-box p { margin: 0; }
487
+ .inline-console .label {
488
+ color: #89b4fa;
489
+ text-transform: uppercase;
490
+ font-size: 11px;
491
+ letter-spacing: 2px;
492
+ margin-bottom: 12px;
493
+ font-weight: bold;
494
+ }
495
+ .code-section {
496
+ background: #1e1e2e;
497
+ border-radius: 8px;
498
+ margin: 20px 0;
499
+ overflow: hidden;
500
+ border: 1px solid #45475a;
501
+ }
502
+ .code-header {
503
+ background: #45475a;
504
+ padding: 12px 16px;
505
+ border-bottom: 1px solid #585869;
506
+ color: #a6e3a1;
507
+ font-size: 12px;
508
+ font-weight: bold;
509
+ }
510
+ .code-block {
511
+ max-height: 420px;
512
+ overflow-y: auto;
513
+ }
514
+ .code-line {
515
+ display: flex;
516
+ border-bottom: 1px solid #45475a;
517
+ }
518
+ .code-line:last-child { border-bottom: none; }
519
+ .code-line-number {
520
+ background: #313244;
521
+ color: #6e7086;
522
+ padding: 8px 16px;
523
+ text-align: right;
524
+ min-width: 60px;
525
+ border-right: 1px solid #45475a;
526
+ user-select: none;
527
+ font-size: 12px;
528
+ }
529
+ .code-line-content {
530
+ flex: 1;
531
+ padding: 8px 16px;
532
+ white-space: pre;
533
+ overflow-x: auto;
534
+ font-size: 13px;
535
+ }
536
+ .code-line.error {
537
+ background: rgba(243, 139, 168, 0.1);
538
+ border-left: 3px solid #f38ba8;
539
+ }
540
+ .code-line.error .code-line-number {
541
+ background: rgba(243, 139, 168, 0.2);
542
+ color: #f38ba8;
543
+ font-weight: bold;
544
+ }
545
+ .detail-grid {
546
+ display: grid;
547
+ grid-template-columns: 2fr 1fr;
548
+ gap: 20px;
549
+ margin: 20px 0;
550
+ }
551
+ .detail-section { margin-top: 4px; }
552
+ .label {
553
+ color: #89b4fa;
554
+ text-transform: uppercase;
555
+ font-size: 11px;
556
+ letter-spacing: 2px;
557
+ margin-bottom: 12px;
558
+ font-weight: bold;
559
+ }
560
+ .fixes {
561
+ list-style: none;
562
+ padding: 0;
563
+ margin: 0;
564
+ }
565
+ .fixes li {
566
+ background: rgba(166, 227, 161, 0.1);
567
+ padding: 12px 16px;
568
+ border-radius: 6px;
569
+ margin-bottom: 8px;
570
+ border-left: 3px solid #a6e3a1;
571
+ }
572
+ .fixes li::before {
573
+ content: "✓ ";
574
+ color: #a6e3a1;
575
+ font-weight: bold;
576
+ }
577
+ .confidence-detail {
578
+ margin: 0;
579
+ color: #a6adc8;
580
+ line-height: 1.6;
581
+ }
582
+ .context-panel {
583
+ background: #1e1e2e;
584
+ border: 1px solid #45475a;
585
+ border-radius: 6px;
586
+ overflow: hidden;
587
+ }
588
+ .context-row {
589
+ display: grid;
590
+ grid-template-columns: 180px 1fr;
591
+ border-bottom: 1px solid #45475a;
592
+ }
593
+ .context-row:last-child { border-bottom: none; }
594
+ .context-label {
595
+ padding: 12px 16px;
596
+ background: #313244;
597
+ color: #89b4fa;
598
+ font-size: 12px;
599
+ font-weight: bold;
600
+ }
601
+ .context-value {
602
+ padding: 12px 16px;
603
+ white-space: pre-wrap;
604
+ word-break: break-word;
605
+ font-size: 13px;
606
+ }
607
+ #{InlineConsole.styles}
608
+ #{AiPanel.styles}
609
+ #{PageActions.styles}
610
+ .fixes li {
611
+ cursor: pointer;
612
+ }
613
+ .fixes li.selected {
614
+ border-left-color: #89b4fa;
615
+ background: rgba(137, 180, 250, 0.12);
616
+ }
617
+ @media (max-width: 900px) {
618
+ .dashboard { grid-template-columns: 1fr; }
619
+ .sidebar { min-height: auto; border-right: none; border-bottom: 1px solid #45475a; }
620
+ .bug-list { max-height: 280px; }
621
+ .detail-panel { min-height: auto; }
622
+ .detail-grid, .context-row { grid-template-columns: 1fr; }
623
+ }
624
+ CSS
625
+ end
626
+ end
627
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ module EditorLinks
5
+ module_function
6
+
7
+ def for_location(location)
8
+ file_path, line_number = CodeContext.extract_location(location)
9
+ return {} unless file_path
10
+
11
+ absolute = File.expand_path(file_path)
12
+ line = line_number || 1
13
+
14
+ {
15
+ file_path: absolute,
16
+ line_number: line,
17
+ cursor: cursor_url(absolute, line),
18
+ vscode: vscode_url(absolute, line)
19
+ }
20
+ end
21
+
22
+ def cursor_url(file_path, line = 1, column = 1)
23
+ "cursor://file/#{escape_path(file_path)}:#{line}:#{column}"
24
+ end
25
+
26
+ def vscode_url(file_path, line = 1, column = 1)
27
+ "vscode://file/#{escape_path(file_path)}:#{line}:#{column}"
28
+ end
29
+
30
+ def escape_path(file_path)
31
+ file_path.to_s.gsub(" ", "%20")
32
+ end
33
+ end
34
+ end