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,708 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Bugsage
6
+ class AiPanel
7
+ extend JsonEndpoint
8
+
9
+ ENDPOINT = "/bugsage/ai-suggest"
10
+
11
+ def self.handle_request(env)
12
+ return not_found unless Bugsage.configuration.ai_configured?
13
+
14
+ payload = parse_request_body(env)
15
+ context = load_context(payload)
16
+ return json_response(error_response(Bugsage.t("errors.ai_context_not_available"))) unless context
17
+
18
+ suggestion, ai_error = AiAnalyzer.enhance(
19
+ context[:suggestion],
20
+ context[:exception],
21
+ context[:context]
22
+ )
23
+
24
+ persist_enhancement!(suggestion, ai_error, payload["index"])
25
+ json_response(serialize(suggestion, ai_error))
26
+ end
27
+
28
+ def self.load_context(payload)
29
+ if payload.key?("index")
30
+ event = Store.all[payload["index"].to_i]
31
+ return nil unless AiContext.load_from_event(event)
32
+
33
+ end
34
+ AiContext.current
35
+ end
36
+
37
+ def self.persist_enhancement!(suggestion, ai_error, index)
38
+ if index
39
+ Store.update_at(index.to_i, suggestion, ai_error: ai_error)
40
+ elsif Store.all.any?
41
+ Store.update_at(0, suggestion, ai_error: ai_error)
42
+ end
43
+ end
44
+
45
+ def self.serialize(suggestion, ai_error)
46
+ return error_response(ai_error) if ai_error
47
+
48
+ {
49
+ ok: true,
50
+ root_cause: suggestion.root_cause,
51
+ fixes: suggestion.fixes,
52
+ confidence: suggestion.confidence,
53
+ ai_notes: suggestion.ai_notes,
54
+ source: suggestion.source.to_s,
55
+ ai_enhanced: suggestion.ai_enhanced?,
56
+ location: suggestion.location,
57
+ editor_links: EditorLinks.for_location(suggestion.location),
58
+ code_patch: suggestion.code_patch,
59
+ code_fix: suggestion.code_fix
60
+ }
61
+ end
62
+
63
+ def self.patch_action(code_patch)
64
+ return nil unless code_patch.is_a?(Hash)
65
+
66
+ code_patch[:action] || code_patch["action"]
67
+ end
68
+
69
+ def self.render_panel(bug_index: nil, include_script: true, suggestion: nil)
70
+ return "" unless Bugsage.configuration.ai_configured?
71
+
72
+ suffix = bug_index.nil? ? "" : "-bug-#{bug_index}"
73
+ panel_id = "bugsage-ai-panel#{suffix}"
74
+ fixes_id = "bugsage-fixes#{suffix}"
75
+ notes_id = "bugsage-ai-notes#{suffix}"
76
+ confidence_id = "bugsage-confidence#{suffix}"
77
+ status_id = "bugsage-ai-status#{suffix}"
78
+ index_attr = bug_index.nil? ? "" : %( data-bug-index="#{bug_index}")
79
+ location_data = suggestion&.location ? %( data-location="#{CodeContext.escape_html(suggestion.location)}") : ""
80
+ patch_available = patch_action(suggestion&.code_patch) && patch_action(suggestion&.code_patch) != "no_change"
81
+ code_patch_data = if patch_available
82
+ %( data-code-patch="#{CodeContext.escape_html(JSON.generate(suggestion.code_patch))}")
83
+ else
84
+ ""
85
+ end
86
+ enhanced = suggestion&.ai_enhanced?
87
+
88
+ <<~HTML
89
+ <div id="#{panel_id}" class="section ai-panel"#{index_attr}#{location_data}#{code_patch_data}>
90
+ <div class="ai-panel-header">
91
+ <div class="label">#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.ai_suggestions"))}</div>
92
+ <div class="ai-panel-header-actions">
93
+ <button type="button" class="ai-chat-toggle bugsage-ai-chat-toggle" title="#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.chat_about_error_title"))}" aria-label="#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.chat_about_error_aria"))}">💬</button>
94
+ <label class="ai-toggle">
95
+ <input type="checkbox" class="bugsage-ai-toggle" checked>
96
+ <span>#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.enable_ai"))}</span>
97
+ </label>
98
+ </div>
99
+ </div>
100
+ #{AiChat.render_widget(suffix: suffix, bug_index: bug_index)}
101
+ <p class="ai-panel-help">
102
+ #{CodeContext.escape_html(Bugsage.t("ui.ai_panel.help"))}
103
+ </p>
104
+ <button type="button" class="quick-fix-button bugsage-quick-fix"#{" hidden" if enhanced}>
105
+ #{CodeContext.escape_html(Bugsage.t("ui.ai_panel.quick_fix_suggestion"))}
106
+ </button>
107
+ <div class="ai-apply-row#{" hidden" unless patch_available}">
108
+ <button type="button" class="apply-ai-button bugsage-apply-ai-codebase">
109
+ #{CodeContext.escape_html(Bugsage.t("ui.ai_panel.apply_ai_to_codebase"))}
110
+ </button>
111
+ <label class="editor-preference">
112
+ <span>#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.open_in"))}</span>
113
+ <select class="bugsage-editor-preference">
114
+ <option value="cursor">#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.cursor"))}</option>
115
+ <option value="vscode">#{CodeContext.escape_html(Bugsage.t("ui.ai_panel.vscode"))}</option>
116
+ </select>
117
+ </label>
118
+ </div>
119
+ <pre class="ai-code-preview#{" hidden" unless patch_available}" id="bugsage-code-preview#{suffix}">#{CodeContext.escape_html(suggestion&.code_fix.to_s)}</pre>
120
+ <div id="#{status_id}" class="ai-status" aria-live="polite"></div>
121
+ <div id="#{notes_id}" class="ai-notes#{" hidden" if suggestion&.ai_notes.to_s.strip.empty?}">
122
+ #{CodeContext.escape_html(suggestion&.ai_notes.to_s)}
123
+ </div>
124
+ </div>
125
+ <template id="bugsage-ai-targets#{suffix}" data-fixes-target="##{fixes_id}" data-confidence-target="##{confidence_id}" data-notes-target="##{notes_id}" data-status-target="##{status_id}"></template>
126
+ #{render_script if include_script}
127
+ HTML
128
+ end
129
+
130
+ def self.styles
131
+ <<~CSS
132
+ .ai-panel {
133
+ background: #313244;
134
+ border: 1px solid #45475a;
135
+ border-radius: 8px;
136
+ padding: 16px;
137
+ margin-bottom: 20px;
138
+ position: relative;
139
+ }
140
+ .ai-panel-header {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ gap: 12px;
145
+ margin-bottom: 8px;
146
+ }
147
+ .ai-panel-help {
148
+ color: #a6adc8;
149
+ font-size: 13px;
150
+ margin: 0 0 12px 0;
151
+ }
152
+ .ai-toggle {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ color: #cdd6f4;
157
+ font-size: 13px;
158
+ cursor: pointer;
159
+ user-select: none;
160
+ }
161
+ .ai-toggle input {
162
+ accent-color: #89b4fa;
163
+ }
164
+ .quick-fix-button {
165
+ background: #89b4fa;
166
+ color: #1e1e2e;
167
+ border: 0;
168
+ border-radius: 6px;
169
+ padding: 10px 14px;
170
+ font-family: inherit;
171
+ font-size: 13px;
172
+ font-weight: bold;
173
+ cursor: pointer;
174
+ }
175
+ .quick-fix-button:disabled {
176
+ opacity: 0.5;
177
+ cursor: not-allowed;
178
+ }
179
+ .quick-fix-button.hidden {
180
+ display: none;
181
+ }
182
+ .apply-ai-button {
183
+ background: #a6e3a1;
184
+ color: #1e1e2e;
185
+ border: 0;
186
+ border-radius: 6px;
187
+ padding: 10px 14px;
188
+ font-family: inherit;
189
+ font-size: 13px;
190
+ font-weight: bold;
191
+ cursor: pointer;
192
+ }
193
+ .apply-ai-button:disabled {
194
+ opacity: 0.5;
195
+ cursor: not-allowed;
196
+ }
197
+ .ai-apply-row {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ align-items: center;
201
+ gap: 12px;
202
+ margin-top: 12px;
203
+ }
204
+ .ai-apply-row.hidden {
205
+ display: none;
206
+ }
207
+ .editor-preference {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: 8px;
211
+ color: #a6adc8;
212
+ font-size: 12px;
213
+ }
214
+ .bugsage-editor-preference {
215
+ background: #1e1e2e;
216
+ color: #cdd6f4;
217
+ border: 1px solid #45475a;
218
+ border-radius: 6px;
219
+ padding: 8px 10px;
220
+ font-family: inherit;
221
+ font-size: 12px;
222
+ }
223
+ .ai-code-preview {
224
+ background: #1e1e2e;
225
+ border: 1px solid #45475a;
226
+ border-radius: 6px;
227
+ padding: 12px;
228
+ margin-top: 12px;
229
+ color: #a6e3a1;
230
+ font-size: 12px;
231
+ white-space: pre-wrap;
232
+ overflow-x: auto;
233
+ }
234
+ .ai-code-preview.hidden {
235
+ display: none;
236
+ }
237
+ .ai-status {
238
+ margin-top: 12px;
239
+ color: #a6adc8;
240
+ font-size: 13px;
241
+ min-height: 0;
242
+ }
243
+ .ai-status.loading {
244
+ color: #f9e2af;
245
+ }
246
+ .ai-status.error {
247
+ color: #f38ba8;
248
+ }
249
+ .ai-notes.hidden {
250
+ display: none;
251
+ }
252
+ #{AiChat.styles}
253
+ CSS
254
+ end
255
+
256
+ def self.render_script
257
+ loading_steps = Bugsage.t("ui.ai_panel.loading_steps")
258
+
259
+ <<~HTML
260
+ <script>
261
+ (function () {
262
+ if (window.__bugsageAiInitialized) return;
263
+ window.__bugsageAiInitialized = true;
264
+
265
+ var STORAGE_KEY = "bugsage-ai-enabled";
266
+ var EDITOR_KEY = "bugsage-preferred-editor";
267
+ var LOADING_STEPS = #{JSON.generate(loading_steps)};
268
+ var THINKING = #{JSON.generate(Bugsage.t("ui.ai_panel.thinking"))};
269
+ var CHAT_REQUEST_FAILED = #{JSON.generate(Bugsage.t("ui.ai_panel.chat_request_failed"))};
270
+ var CODE_PATCH_UPDATED = #{JSON.generate(Bugsage.t("ui.ai_panel.code_patch_updated"))};
271
+ var CONFIRM_APPLY_AI = #{JSON.generate(Bugsage.t("ui.ai_panel.confirm_apply_ai"))};
272
+ var APPLYING = #{JSON.generate(Bugsage.t("ui.ai_panel.applying"))};
273
+ var COULD_NOT_APPLY_AI = #{JSON.generate(Bugsage.t("ui.ai_panel.could_not_apply_ai"))};
274
+ var APPLIED = #{JSON.generate(Bugsage.t("ui.ai_panel.applied"))};
275
+ var NO_CODE_CHANGE = #{JSON.generate(Bugsage.t("code_patch.no_change"))};
276
+ var REQUESTING_AI = #{JSON.generate(Bugsage.t("ui.ai_panel.requesting_ai"))};
277
+ var AI_ENHANCED_APPLIED = #{JSON.generate(Bugsage.t("ui.ai_panel.ai_enhanced_applied"))};
278
+ var SUGGESTION_UPDATED = #{JSON.generate(Bugsage.t("ui.ai_panel.suggestion_updated"))};
279
+ var CONFIDENCE_SUFFIX = #{JSON.generate(Bugsage.t("ui.ai_panel.confidence_suffix"))};
280
+ var ASSISTANT_WELCOME = #{JSON.generate(Bugsage.t("ui.ai_panel.assistant_welcome"))};
281
+ var chatHistories = {};
282
+
283
+ function aiEnabled() {
284
+ return localStorage.getItem(STORAGE_KEY) !== "false";
285
+ }
286
+
287
+ function setAiEnabled(enabled) {
288
+ localStorage.setItem(STORAGE_KEY, enabled ? "true" : "false");
289
+ }
290
+
291
+ function preferredEditor() {
292
+ return localStorage.getItem(EDITOR_KEY) || "cursor";
293
+ }
294
+
295
+ function setPreferredEditor(value) {
296
+ localStorage.setItem(EDITOR_KEY, value);
297
+ }
298
+
299
+ function panelSuffix(panel) {
300
+ return panel.id.replace("bugsage-ai-panel", "");
301
+ }
302
+
303
+ function loadingOverlay(panel) {
304
+ return document.getElementById("bugsage-ai-loading" + panelSuffix(panel));
305
+ }
306
+
307
+ function loadingStep(panel) {
308
+ return document.getElementById("bugsage-ai-loading-step" + panelSuffix(panel));
309
+ }
310
+
311
+ var loadingTimers = {};
312
+
313
+ function showLoading(panel) {
314
+ var overlay = loadingOverlay(panel);
315
+ var stepEl = loadingStep(panel);
316
+ if (!overlay) return;
317
+
318
+ panel.classList.add("is-loading");
319
+ overlay.classList.remove("hidden");
320
+ overlay.setAttribute("aria-hidden", "false");
321
+
322
+ var stepIndex = 0;
323
+ if (stepEl) stepEl.textContent = LOADING_STEPS[0];
324
+
325
+ clearInterval(loadingTimers[panel.id]);
326
+ loadingTimers[panel.id] = setInterval(function () {
327
+ stepIndex = (stepIndex + 1) % LOADING_STEPS.length;
328
+ if (stepEl) stepEl.textContent = LOADING_STEPS[stepIndex];
329
+ }, 2200);
330
+ }
331
+
332
+ function hideLoading(panel) {
333
+ var overlay = loadingOverlay(panel);
334
+ if (!overlay) return;
335
+
336
+ panel.classList.remove("is-loading");
337
+ overlay.classList.add("hidden");
338
+ overlay.setAttribute("aria-hidden", "true");
339
+ clearInterval(loadingTimers[panel.id]);
340
+ }
341
+
342
+ function chatPanel(panel) {
343
+ return document.getElementById("bugsage-ai-chat" + panelSuffix(panel));
344
+ }
345
+
346
+ function chatMessages(panel) {
347
+ return document.getElementById("bugsage-ai-chat-messages" + panelSuffix(panel));
348
+ }
349
+
350
+ function chatHistoryKey(panel) {
351
+ return panel.id + (panel.dataset.bugIndex !== undefined ? "-" + panel.dataset.bugIndex : "");
352
+ }
353
+
354
+ function appendChatBubble(container, role, text) {
355
+ var bubble = document.createElement("div");
356
+ bubble.className = "ai-chat-bubble " + role;
357
+ bubble.textContent = text;
358
+ container.appendChild(bubble);
359
+ container.scrollTop = container.scrollHeight;
360
+ return bubble;
361
+ }
362
+
363
+ function toggleChat(panel, forceOpen) {
364
+ var chat = chatPanel(panel);
365
+ if (!chat) return;
366
+
367
+ var open = forceOpen === true || (forceOpen !== false && chat.classList.contains("hidden"));
368
+ chat.classList.toggle("hidden", !open);
369
+ }
370
+
371
+ function sendChatMessage(panel, form) {
372
+ var input = form.querySelector(".ai-chat-input");
373
+ var sendButton = form.querySelector(".ai-chat-send");
374
+ var messages = chatMessages(panel);
375
+ if (!input || !messages) return;
376
+
377
+ var message = input.value.trim();
378
+ if (!message) return;
379
+
380
+ var historyKey = chatHistoryKey(panel);
381
+ var history = chatHistories[historyKey] || [];
382
+
383
+ appendChatBubble(messages, "user", message);
384
+ input.value = "";
385
+ input.disabled = true;
386
+ if (sendButton) sendButton.disabled = true;
387
+
388
+ var loadingBubble = appendChatBubble(messages, "loading", THINKING);
389
+
390
+ var payload = { message: message, history: history };
391
+ if (panel.dataset.bugIndex !== undefined) {
392
+ payload.index = parseInt(panel.dataset.bugIndex, 10);
393
+ }
394
+
395
+ fetch("#{AiChat::ENDPOINT}", {
396
+ method: "POST",
397
+ headers: { "Content-Type": "application/json" },
398
+ body: JSON.stringify(payload)
399
+ })
400
+ .then(function (response) { return response.json(); })
401
+ .then(function (result) {
402
+ loadingBubble.remove();
403
+ input.disabled = false;
404
+ if (sendButton) sendButton.disabled = false;
405
+ input.focus();
406
+
407
+ if (!result.ok) {
408
+ appendChatBubble(messages, "assistant", result.error || CHAT_REQUEST_FAILED);
409
+ return;
410
+ }
411
+
412
+ chatHistories[historyKey] = result.history || history;
413
+ appendChatBubble(messages, "assistant", result.reply || "");
414
+
415
+ if (result.code_patch) {
416
+ revealAiApply(panel, result);
417
+ var targets = panelTargets(panel);
418
+ if (targets && targets.status) {
419
+ targets.status.textContent = CODE_PATCH_UPDATED;
420
+ targets.status.className = "ai-status";
421
+ }
422
+ }
423
+ })
424
+ .catch(function (error) {
425
+ loadingBubble.remove();
426
+ input.disabled = false;
427
+ if (sendButton) sendButton.disabled = false;
428
+ appendChatBubble(messages, "assistant", error.message);
429
+ });
430
+ }
431
+
432
+ function openEditorUrl(url) {
433
+ var link = document.createElement("a");
434
+ link.href = url;
435
+ link.rel = "noopener";
436
+ link.style.display = "none";
437
+ document.body.appendChild(link);
438
+ link.click();
439
+ document.body.removeChild(link);
440
+ }
441
+
442
+ function revealAiApply(panel, result) {
443
+ if (!result.code_patch && !result.code_fix) return;
444
+
445
+ panel.dataset.codePatch = JSON.stringify(result.code_patch || {});
446
+ panel.dataset.location = result.location || panel.dataset.location || "";
447
+
448
+ var applyRow = panel.querySelector(".ai-apply-row");
449
+ var preview = panel.querySelector(".ai-code-preview");
450
+ if (result.code_patch && result.code_patch.action === "no_change") {
451
+ if (applyRow) applyRow.classList.add("hidden");
452
+ if (preview) {
453
+ preview.textContent = result.code_fix || NO_CODE_CHANGE;
454
+ preview.classList.remove("hidden");
455
+ }
456
+ return;
457
+ }
458
+
459
+ if (applyRow) applyRow.classList.remove("hidden");
460
+ if (preview) {
461
+ preview.textContent = result.code_fix || "";
462
+ preview.classList.remove("hidden");
463
+ }
464
+ }
465
+
466
+ function applyAiToCodebase(panel, button) {
467
+ var location = panel.dataset.location;
468
+ var codePatch = null;
469
+ try {
470
+ codePatch = JSON.parse(panel.dataset.codePatch || "null");
471
+ } catch (error) {
472
+ codePatch = null;
473
+ }
474
+ if (!location || !codePatch) return;
475
+
476
+ if (!window.confirm(CONFIRM_APPLY_AI)) return;
477
+
478
+ var originalText = button.textContent;
479
+ button.disabled = true;
480
+ button.textContent = APPLYING;
481
+
482
+ fetch("#{FixApplicator::ENDPOINT}", {
483
+ method: "POST",
484
+ headers: { "Content-Type": "application/json" },
485
+ body: JSON.stringify({
486
+ location: location,
487
+ code_patch: codePatch
488
+ })
489
+ })
490
+ .then(function (response) { return response.json(); })
491
+ .then(function (result) {
492
+ button.disabled = false;
493
+ button.textContent = originalText;
494
+ if (!result.ok) {
495
+ window.alert(result.error || COULD_NOT_APPLY_AI);
496
+ return;
497
+ }
498
+
499
+ var editor = panel.querySelector(".bugsage-editor-preference");
500
+ var choice = editor ? editor.value : preferredEditor();
501
+ setPreferredEditor(choice);
502
+
503
+ var editorUrl = result.editor_links && result.editor_links[choice];
504
+ if (editorUrl) openEditorUrl(editorUrl);
505
+
506
+ button.textContent = APPLIED;
507
+ setTimeout(function () { button.textContent = originalText; }, 2000);
508
+ })
509
+ .catch(function (error) {
510
+ button.disabled = false;
511
+ button.textContent = originalText;
512
+ window.alert(error.message);
513
+ });
514
+ }
515
+
516
+ function panelTargets(panel) {
517
+ var suffix = panel.id.replace("bugsage-ai-panel", "");
518
+ var template = document.getElementById("bugsage-ai-targets" + suffix);
519
+ if (!template) return null;
520
+
521
+ return {
522
+ fixes: document.querySelector(template.dataset.fixesTarget),
523
+ confidence: document.querySelector(template.dataset.confidenceTarget),
524
+ notes: document.querySelector(template.dataset.notesTarget),
525
+ status: document.querySelector(template.dataset.statusTarget)
526
+ };
527
+ }
528
+
529
+ function updateFixes(target, fixes) {
530
+ if (!target) return;
531
+ target.innerHTML = fixes.map(function (fix, index) {
532
+ var selected = index === 0 ? " selected" : "";
533
+ return "<li class=\\"" + selected.trim() + "\\">" + fix.replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</li>";
534
+ }).join("");
535
+ }
536
+
537
+ function syncToggle(panel) {
538
+ var toggle = panel.querySelector(".bugsage-ai-toggle");
539
+ var button = panel.querySelector(".bugsage-quick-fix");
540
+ if (!toggle || !button) return;
541
+
542
+ toggle.checked = aiEnabled();
543
+ button.disabled = !toggle.checked;
544
+ }
545
+
546
+ document.querySelectorAll(".ai-panel").forEach(function (panel) {
547
+ syncToggle(panel);
548
+ var editorPick = panel.querySelector(".bugsage-editor-preference");
549
+ if (editorPick) editorPick.value = preferredEditor();
550
+ });
551
+
552
+ document.addEventListener("change", function (event) {
553
+ if (event.target.classList.contains("bugsage-editor-preference")) {
554
+ setPreferredEditor(event.target.value);
555
+ }
556
+ });
557
+
558
+ document.addEventListener("change", function (event) {
559
+ if (!event.target.classList.contains("bugsage-ai-toggle")) return;
560
+
561
+ var panel = event.target.closest(".ai-panel");
562
+ setAiEnabled(event.target.checked);
563
+ syncToggle(panel);
564
+ });
565
+
566
+ document.addEventListener("submit", function (event) {
567
+ var form = event.target.closest(".bugsage-ai-chat-form");
568
+ if (!form) return;
569
+
570
+ event.preventDefault();
571
+ var panel = form.closest(".ai-panel");
572
+ if (!panel || !aiEnabled()) return;
573
+
574
+ sendChatMessage(panel, form);
575
+ });
576
+
577
+ document.addEventListener("click", function (event) {
578
+ var chatToggle = event.target.closest(".bugsage-ai-chat-toggle");
579
+ if (chatToggle) {
580
+ event.preventDefault();
581
+ var chatPanelEl = chatToggle.closest(".ai-panel");
582
+ if (chatPanelEl) toggleChat(chatPanelEl);
583
+ return;
584
+ }
585
+
586
+ var chatClose = event.target.closest(".ai-chat-close");
587
+ if (chatClose) {
588
+ event.preventDefault();
589
+ var closePanel = chatClose.closest(".ai-panel");
590
+ if (closePanel) toggleChat(closePanel, false);
591
+ return;
592
+ }
593
+
594
+ var applyAiButton = event.target.closest(".bugsage-apply-ai-codebase");
595
+ if (applyAiButton) {
596
+ event.preventDefault();
597
+ var panel = applyAiButton.closest(".ai-panel");
598
+ if (panel) applyAiToCodebase(panel, applyAiButton);
599
+ return;
600
+ }
601
+
602
+ var button = event.target.closest(".bugsage-quick-fix");
603
+ if (!button || button.disabled) return;
604
+
605
+ var panel = button.closest(".ai-panel");
606
+ var targets = panelTargets(panel);
607
+ if (!targets || !targets.status) return;
608
+
609
+ button.disabled = true;
610
+ targets.status.textContent = REQUESTING_AI;
611
+ targets.status.className = "ai-status loading";
612
+ showLoading(panel);
613
+
614
+ var payload = {};
615
+ if (panel.dataset.bugIndex !== undefined) {
616
+ payload.index = parseInt(panel.dataset.bugIndex, 10);
617
+ }
618
+
619
+ fetch("#{ENDPOINT}", {
620
+ method: "POST",
621
+ headers: { "Content-Type": "application/json" },
622
+ body: JSON.stringify(payload)
623
+ })
624
+ .then(function (response) { return response.json(); })
625
+ .then(function (result) {
626
+ hideLoading(panel);
627
+
628
+ if (!result.ok) {
629
+ targets.status.textContent = result.error;
630
+ targets.status.className = "ai-status error";
631
+ button.disabled = !aiEnabled();
632
+ return;
633
+ }
634
+
635
+ if (targets.fixes) {
636
+ updateFixes(targets.fixes, result.fixes || []);
637
+ }
638
+
639
+ var fixActions = panel.parentElement
640
+ ? panel.parentElement.querySelector(".fix-actions")
641
+ : document.querySelector(".fix-actions");
642
+ if (fixActions) {
643
+ fixActions.classList.remove("hidden");
644
+ if (result.location) {
645
+ fixActions.dataset.location = result.location;
646
+ }
647
+ }
648
+
649
+ revealAiApply(panel, result);
650
+
651
+ var rootCause = panel.closest(".bug-detail, .container");
652
+ if (rootCause) {
653
+ var message = rootCause.querySelector(".message-box p, .message-box");
654
+ if (message && result.root_cause) {
655
+ if (message.tagName === "P") {
656
+ message.textContent = result.root_cause;
657
+ }
658
+ }
659
+ }
660
+
661
+ if (targets.confidence) {
662
+ var confidenceText = result.confidence + "%";
663
+ if (targets.confidence.classList.contains("confidence-badge")) {
664
+ confidenceText += CONFIDENCE_SUFFIX;
665
+ }
666
+ targets.confidence.textContent = confidenceText;
667
+ }
668
+
669
+ if (targets.notes) {
670
+ if (result.ai_notes) {
671
+ targets.notes.textContent = result.ai_notes;
672
+ targets.notes.classList.remove("hidden");
673
+ } else {
674
+ targets.notes.classList.add("hidden");
675
+ }
676
+ }
677
+
678
+ targets.status.textContent = result.ai_enhanced
679
+ ? AI_ENHANCED_APPLIED
680
+ : SUGGESTION_UPDATED;
681
+ targets.status.className = "ai-status";
682
+ button.classList.add("hidden");
683
+
684
+ if (result.ai_notes || result.fixes) {
685
+ toggleChat(panel, true);
686
+ var messages = chatMessages(panel);
687
+ if (messages && messages.childElementCount === 0) {
688
+ appendChatBubble(
689
+ messages,
690
+ "assistant",
691
+ ASSISTANT_WELCOME
692
+ );
693
+ }
694
+ }
695
+ })
696
+ .catch(function (error) {
697
+ hideLoading(panel);
698
+ targets.status.textContent = error.message;
699
+ targets.status.className = "ai-status error";
700
+ button.disabled = !aiEnabled();
701
+ });
702
+ });
703
+ })();
704
+ </script>
705
+ HTML
706
+ end
707
+ end
708
+ end