rubyn 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +251 -0
- data/Rakefile +12 -0
- data/exe/rubyn +5 -0
- data/lib/generators/rubyn/install_generator.rb +16 -0
- data/lib/rubyn/cli.rb +85 -0
- data/lib/rubyn/client/api_client.rb +172 -0
- data/lib/rubyn/commands/agent.rb +191 -0
- data/lib/rubyn/commands/base.rb +60 -0
- data/lib/rubyn/commands/config.rb +51 -0
- data/lib/rubyn/commands/dashboard.rb +85 -0
- data/lib/rubyn/commands/index.rb +101 -0
- data/lib/rubyn/commands/init.rb +166 -0
- data/lib/rubyn/commands/refactor.rb +175 -0
- data/lib/rubyn/commands/review.rb +61 -0
- data/lib/rubyn/commands/spec.rb +72 -0
- data/lib/rubyn/commands/usage.rb +56 -0
- data/lib/rubyn/config/credentials.rb +39 -0
- data/lib/rubyn/config/project_config.rb +42 -0
- data/lib/rubyn/config/settings.rb +53 -0
- data/lib/rubyn/context/codebase_indexer.rb +195 -0
- data/lib/rubyn/context/context_builder.rb +36 -0
- data/lib/rubyn/context/file_resolver.rb +235 -0
- data/lib/rubyn/context/project_scanner.rb +132 -0
- data/lib/rubyn/engine/app/assets/images/rubyn/RubynLogo.png +0 -0
- data/lib/rubyn/engine/app/assets/javascripts/rubyn/application.js +579 -0
- data/lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css +1073 -0
- data/lib/rubyn/engine/app/controllers/rubyn/agent_controller.rb +26 -0
- data/lib/rubyn/engine/app/controllers/rubyn/application_controller.rb +44 -0
- data/lib/rubyn/engine/app/controllers/rubyn/dashboard_controller.rb +19 -0
- data/lib/rubyn/engine/app/controllers/rubyn/feedback_controller.rb +17 -0
- data/lib/rubyn/engine/app/controllers/rubyn/files_controller.rb +54 -0
- data/lib/rubyn/engine/app/controllers/rubyn/refactor_controller.rb +56 -0
- data/lib/rubyn/engine/app/controllers/rubyn/reviews_controller.rb +32 -0
- data/lib/rubyn/engine/app/controllers/rubyn/settings_controller.rb +17 -0
- data/lib/rubyn/engine/app/controllers/rubyn/specs_controller.rb +33 -0
- data/lib/rubyn/engine/app/views/layouts/rubyn/application.html.erb +63 -0
- data/lib/rubyn/engine/app/views/rubyn/agent/show.html.erb +22 -0
- data/lib/rubyn/engine/app/views/rubyn/dashboard/index.html.erb +120 -0
- data/lib/rubyn/engine/app/views/rubyn/files/index.html.erb +45 -0
- data/lib/rubyn/engine/app/views/rubyn/refactor/show.html.erb +28 -0
- data/lib/rubyn/engine/app/views/rubyn/reviews/show.html.erb +28 -0
- data/lib/rubyn/engine/app/views/rubyn/settings/show.html.erb +42 -0
- data/lib/rubyn/engine/app/views/rubyn/specs/show.html.erb +28 -0
- data/lib/rubyn/engine/config/routes.rb +13 -0
- data/lib/rubyn/engine/engine.rb +18 -0
- data/lib/rubyn/output/diff_renderer.rb +106 -0
- data/lib/rubyn/output/formatter.rb +123 -0
- data/lib/rubyn/output/spinner.rb +26 -0
- data/lib/rubyn/tools/base_tool.rb +74 -0
- data/lib/rubyn/tools/bundle_add.rb +77 -0
- data/lib/rubyn/tools/create_file.rb +32 -0
- data/lib/rubyn/tools/delete_file.rb +29 -0
- data/lib/rubyn/tools/executor.rb +68 -0
- data/lib/rubyn/tools/find_files.rb +33 -0
- data/lib/rubyn/tools/find_references.rb +72 -0
- data/lib/rubyn/tools/git_commit.rb +65 -0
- data/lib/rubyn/tools/git_create_branch.rb +58 -0
- data/lib/rubyn/tools/git_diff.rb +42 -0
- data/lib/rubyn/tools/git_log.rb +43 -0
- data/lib/rubyn/tools/git_status.rb +26 -0
- data/lib/rubyn/tools/list_directory.rb +82 -0
- data/lib/rubyn/tools/move_file.rb +35 -0
- data/lib/rubyn/tools/patch_file.rb +47 -0
- data/lib/rubyn/tools/rails_generate.rb +40 -0
- data/lib/rubyn/tools/rails_migrate.rb +55 -0
- data/lib/rubyn/tools/rails_routes.rb +35 -0
- data/lib/rubyn/tools/read_file.rb +45 -0
- data/lib/rubyn/tools/registry.rb +28 -0
- data/lib/rubyn/tools/run_command.rb +48 -0
- data/lib/rubyn/tools/run_tests.rb +52 -0
- data/lib/rubyn/tools/search_files.rb +82 -0
- data/lib/rubyn/tools/write_file.rb +30 -0
- data/lib/rubyn/version.rb +5 -0
- data/lib/rubyn/version_checker.rb +74 -0
- data/lib/rubyn.rb +95 -0
- data/sig/rubyn.rbs +4 -0
- metadata +379 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// Rubyn Dashboard JavaScript
|
|
2
|
+
(function() {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var pendingRequests = 0;
|
|
6
|
+
|
|
7
|
+
function trackRequest(promise) {
|
|
8
|
+
pendingRequests++;
|
|
9
|
+
return promise.finally(function() { pendingRequests--; });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
window.addEventListener("beforeunload", function(e) {
|
|
13
|
+
if (pendingRequests > 0) {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
e.returnValue = "";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
20
|
+
initChatForm();
|
|
21
|
+
initToolPages();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function initChatForm() {
|
|
25
|
+
var form = document.getElementById("chat-form");
|
|
26
|
+
if (!form) return;
|
|
27
|
+
|
|
28
|
+
var input = document.getElementById("message-input");
|
|
29
|
+
|
|
30
|
+
// Submit on Enter (Shift+Enter for newline)
|
|
31
|
+
input.addEventListener("keydown", function(e) {
|
|
32
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
form.dispatchEvent(new Event("submit"));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
form.addEventListener("submit", function(e) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
var message = input.value.trim();
|
|
41
|
+
if (!message) return;
|
|
42
|
+
|
|
43
|
+
hideEmptyState();
|
|
44
|
+
appendMessage("you", message);
|
|
45
|
+
input.value = "";
|
|
46
|
+
input.focus();
|
|
47
|
+
sendMessage(message);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hideEmptyState() {
|
|
52
|
+
var empty = document.getElementById("empty-state");
|
|
53
|
+
if (empty) empty.remove();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function appendMessage(role, content) {
|
|
57
|
+
var container = document.getElementById("messages");
|
|
58
|
+
if (!container) return;
|
|
59
|
+
|
|
60
|
+
var div = document.createElement("div");
|
|
61
|
+
div.className = "rubyn-message rubyn-message-" + role;
|
|
62
|
+
|
|
63
|
+
var label = document.createElement("div");
|
|
64
|
+
label.className = "rubyn-message-label";
|
|
65
|
+
label.textContent = role === "you" ? "You" : "Rubyn";
|
|
66
|
+
|
|
67
|
+
var body = document.createElement("div");
|
|
68
|
+
body.className = "rubyn-message-body";
|
|
69
|
+
|
|
70
|
+
if (role === "rubyn" && content.includes("```")) {
|
|
71
|
+
body.innerHTML = formatMessageWithCode(content);
|
|
72
|
+
highlightCodeBlocks(body);
|
|
73
|
+
} else {
|
|
74
|
+
body.textContent = content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
div.appendChild(label);
|
|
78
|
+
div.appendChild(body);
|
|
79
|
+
container.appendChild(div);
|
|
80
|
+
container.scrollTop = container.scrollHeight;
|
|
81
|
+
|
|
82
|
+
return div;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendThinking() {
|
|
86
|
+
var container = document.getElementById("messages");
|
|
87
|
+
if (!container) return;
|
|
88
|
+
|
|
89
|
+
var div = document.createElement("div");
|
|
90
|
+
div.className = "rubyn-message rubyn-message-rubyn";
|
|
91
|
+
div.id = "thinking-indicator";
|
|
92
|
+
|
|
93
|
+
var label = document.createElement("div");
|
|
94
|
+
label.className = "rubyn-message-label";
|
|
95
|
+
label.textContent = "Rubyn";
|
|
96
|
+
|
|
97
|
+
var dots = document.createElement("div");
|
|
98
|
+
dots.className = "rubyn-dots";
|
|
99
|
+
dots.innerHTML = "<span></span><span></span><span></span>";
|
|
100
|
+
|
|
101
|
+
div.appendChild(label);
|
|
102
|
+
div.appendChild(dots);
|
|
103
|
+
container.appendChild(div);
|
|
104
|
+
container.scrollTop = container.scrollHeight;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeThinking() {
|
|
108
|
+
var el = document.getElementById("thinking-indicator");
|
|
109
|
+
if (el) el.remove();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var conversationId = null;
|
|
113
|
+
|
|
114
|
+
function sendMessage(message) {
|
|
115
|
+
appendThinking();
|
|
116
|
+
|
|
117
|
+
var csrfToken = document.querySelector('meta[name="csrf-token"]');
|
|
118
|
+
var headers = { "Content-Type": "application/json" };
|
|
119
|
+
if (csrfToken) headers["X-CSRF-Token"] = csrfToken.content;
|
|
120
|
+
|
|
121
|
+
trackRequest(
|
|
122
|
+
fetch(mountPath() + "/agent", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: headers,
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
message: message,
|
|
127
|
+
conversation_id: conversationId
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
.then(function(res) { return res.json(); })
|
|
131
|
+
.then(function(data) {
|
|
132
|
+
removeThinking();
|
|
133
|
+
if (data.error) {
|
|
134
|
+
appendMessage("rubyn", "Error: " + data.error);
|
|
135
|
+
} else {
|
|
136
|
+
conversationId = data.conversation_id || conversationId;
|
|
137
|
+
appendMessage("rubyn", data.response || data.content || "No response");
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.catch(function(err) {
|
|
141
|
+
removeThinking();
|
|
142
|
+
appendMessage("rubyn", "Connection error: " + err.message);
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- Tool pages (refactor, spec, review) ----
|
|
148
|
+
|
|
149
|
+
var TOOL_PAGES = {
|
|
150
|
+
"diff-container": "refactor",
|
|
151
|
+
"spec-container": "specs",
|
|
152
|
+
"review-container": "reviews"
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function initToolPages() {
|
|
156
|
+
Object.keys(TOOL_PAGES).forEach(function(containerId) {
|
|
157
|
+
var container = document.getElementById(containerId);
|
|
158
|
+
if (!container) return;
|
|
159
|
+
|
|
160
|
+
var resource = TOOL_PAGES[containerId];
|
|
161
|
+
var fileParam = new URLSearchParams(window.location.search).get("file");
|
|
162
|
+
if (!fileParam) return;
|
|
163
|
+
|
|
164
|
+
// Restore from sessionStorage if available
|
|
165
|
+
var cacheKey = "rubyn:" + resource + ":" + fileParam;
|
|
166
|
+
var cached = sessionStorage.getItem(cacheKey);
|
|
167
|
+
if (cached) {
|
|
168
|
+
var headers = buildHeaders();
|
|
169
|
+
var cachedId = sessionStorage.getItem(cacheKey + ":id");
|
|
170
|
+
var cachedCredits = sessionStorage.getItem(cacheKey + ":credits");
|
|
171
|
+
renderToolResponse(container, resource, fileParam, cached, headers, cachedId ? parseInt(cachedId) : null, cachedCredits ? parseFloat(cachedCredits) : null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
runTool(container, resource, fileParam);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildHeaders() {
|
|
180
|
+
var csrfToken = document.querySelector('meta[name="csrf-token"]');
|
|
181
|
+
var headers = { "Content-Type": "application/json" };
|
|
182
|
+
if (csrfToken) headers["X-CSRF-Token"] = csrfToken.content;
|
|
183
|
+
return headers;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function runTool(container, resource, file) {
|
|
187
|
+
var headers = buildHeaders();
|
|
188
|
+
|
|
189
|
+
trackRequest(
|
|
190
|
+
fetch(rubynMountPath() + "/" + resource, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: headers,
|
|
193
|
+
body: JSON.stringify({ file: file })
|
|
194
|
+
})
|
|
195
|
+
.then(function(res) { return res.json(); })
|
|
196
|
+
.then(function(data) {
|
|
197
|
+
if (data.error) {
|
|
198
|
+
container.innerHTML = '<div class="rubyn-tool-empty"><p>Error: ' + escapeHtml(data.error) + '</p></div>';
|
|
199
|
+
} else {
|
|
200
|
+
var response = data.response || "No response";
|
|
201
|
+
var interactionId = data.interaction_id;
|
|
202
|
+
var creditsUsed = data.credits_used;
|
|
203
|
+
// Cache the response and interaction ID so they survive page navigation
|
|
204
|
+
var cacheKey = "rubyn:" + resource + ":" + file;
|
|
205
|
+
sessionStorage.setItem(cacheKey, response);
|
|
206
|
+
if (interactionId) sessionStorage.setItem(cacheKey + ":id", String(interactionId));
|
|
207
|
+
if (creditsUsed != null) sessionStorage.setItem(cacheKey + ":credits", String(creditsUsed));
|
|
208
|
+
renderToolResponse(container, resource, file, response, headers, interactionId, creditsUsed);
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.catch(function(err) {
|
|
212
|
+
container.innerHTML = '<div class="rubyn-tool-empty"><p>Connection error: ' + escapeHtml(err.message) + '</p></div>';
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderToolResponse(container, resource, file, response, headers, interactionId, creditsUsed) {
|
|
218
|
+
var codeBlocks = extractCodeBlocks(response);
|
|
219
|
+
|
|
220
|
+
if (resource === "refactor" && codeBlocks.length > 0) {
|
|
221
|
+
renderRefactorResponse(container, file, response, codeBlocks, headers, interactionId);
|
|
222
|
+
} else if (resource === "specs" && codeBlocks.length > 0) {
|
|
223
|
+
renderSpecResponse(container, file, response, codeBlocks, headers);
|
|
224
|
+
} else {
|
|
225
|
+
container.innerHTML = '<pre class="rubyn-tool-output"><code>' + escapeHtml(response) + '</code></pre>';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Show credits used and feedback in a footer bar
|
|
229
|
+
var footer = document.createElement("div");
|
|
230
|
+
footer.className = "rubyn-tool-footer";
|
|
231
|
+
|
|
232
|
+
if (creditsUsed != null) {
|
|
233
|
+
var badge = document.createElement("span");
|
|
234
|
+
badge.className = "rubyn-credits-badge";
|
|
235
|
+
badge.textContent = creditsUsed + (creditsUsed === 1 ? " credit used" : " credits used");
|
|
236
|
+
footer.appendChild(badge);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (interactionId) {
|
|
240
|
+
appendFeedbackButtons(footer, interactionId, headers);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (footer.children.length > 0) {
|
|
244
|
+
container.appendChild(footer);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function appendFeedbackButtons(parent, interactionId, headers) {
|
|
249
|
+
var wrapper = document.createElement("div");
|
|
250
|
+
wrapper.className = "rubyn-feedback";
|
|
251
|
+
wrapper.innerHTML =
|
|
252
|
+
'<span class="rubyn-feedback-label">How did Rubyn do?</span>' +
|
|
253
|
+
'<button class="rubyn-feedback-btn" data-rating="thumbs_up" title="Helpful">' +
|
|
254
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>' +
|
|
255
|
+
'</button>' +
|
|
256
|
+
'<button class="rubyn-feedback-btn" data-rating="thumbs_down" title="Not helpful">' +
|
|
257
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10zM17 2h2.4a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H17"/></svg>' +
|
|
258
|
+
'</button>';
|
|
259
|
+
|
|
260
|
+
wrapper.querySelectorAll(".rubyn-feedback-btn").forEach(function(btn) {
|
|
261
|
+
btn.addEventListener("click", function() {
|
|
262
|
+
var rating = btn.getAttribute("data-rating");
|
|
263
|
+
submitFeedback(interactionId, rating, headers, wrapper);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
parent.appendChild(wrapper);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function submitFeedback(interactionId, rating, headers, wrapper) {
|
|
271
|
+
wrapper.querySelectorAll(".rubyn-feedback-btn").forEach(function(b) { b.disabled = true; });
|
|
272
|
+
|
|
273
|
+
fetch(rubynMountPath() + "/feedback", {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: headers,
|
|
276
|
+
body: JSON.stringify({ interaction_id: interactionId, rating: rating })
|
|
277
|
+
})
|
|
278
|
+
.then(function(res) { return res.json(); })
|
|
279
|
+
.then(function(data) {
|
|
280
|
+
if (data.success) {
|
|
281
|
+
var label = rating === "thumbs_up" ? "Thanks for the feedback!" : "Sorry about that. We'll improve.";
|
|
282
|
+
wrapper.innerHTML = '<span class="rubyn-feedback-label rubyn-feedback-thanks">' + label + '</span>';
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
.catch(function() {
|
|
286
|
+
wrapper.querySelectorAll(".rubyn-feedback-btn").forEach(function(b) { b.disabled = false; });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function renderRefactorResponse(container, file, response, codeBlocks, headers, interactionId) {
|
|
291
|
+
var html = '';
|
|
292
|
+
var fileHeaders = extractFileHeaders(response);
|
|
293
|
+
|
|
294
|
+
// Build the file-to-code mapping
|
|
295
|
+
// Headers and code blocks are in the same order — zip them together
|
|
296
|
+
var fileChanges = codeBlocks.map(function(code, i) {
|
|
297
|
+
var header = fileHeaders[i];
|
|
298
|
+
return {
|
|
299
|
+
path: header ? header.path : file,
|
|
300
|
+
tag: header ? header.tag : null,
|
|
301
|
+
code: code
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Show each code block with its file path, tag, and individual apply button
|
|
306
|
+
fileChanges.forEach(function(change, i) {
|
|
307
|
+
html += '<div class="rubyn-code-block">';
|
|
308
|
+
html += '<div class="rubyn-code-block-header">';
|
|
309
|
+
if (change.tag) {
|
|
310
|
+
var tagClass = change.tag === "NEW" ? "rubyn-tag-new" : "rubyn-tag-modified";
|
|
311
|
+
html += '<span class="rubyn-file-tag ' + tagClass + '">' + change.tag + '</span>';
|
|
312
|
+
}
|
|
313
|
+
html += '<span class="rubyn-tool-filepath">' + escapeHtml(change.path) + '</span>';
|
|
314
|
+
html += '<div class="rubyn-code-block-actions">';
|
|
315
|
+
html += '<button class="rubyn-btn-sm rubyn-apply-one" data-index="' + i + '">Apply</button>';
|
|
316
|
+
html += '<button class="rubyn-btn-sm rubyn-copy-btn" data-index="' + i + '">Copy</button>';
|
|
317
|
+
html += '</div>';
|
|
318
|
+
html += '</div>';
|
|
319
|
+
html += '<pre class="rubyn-tool-output"><code>' + escapeHtml(change.code) + '</code></pre>';
|
|
320
|
+
html += '</div>';
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Action bar
|
|
324
|
+
html += '<div class="rubyn-tool-actions">';
|
|
325
|
+
if (fileChanges.length > 1) {
|
|
326
|
+
html += '<button class="rubyn-btn" id="apply-all-btn">Apply All (' + fileChanges.length + ' files)</button>';
|
|
327
|
+
} else {
|
|
328
|
+
html += '<button class="rubyn-btn" id="apply-all-btn">Apply Changes</button>';
|
|
329
|
+
}
|
|
330
|
+
html += '<button class="rubyn-btn rubyn-btn--ghost" id="clear-cache-btn">Discard</button>';
|
|
331
|
+
html += '<button class="rubyn-btn rubyn-btn--ghost" id="toggle-explanation-btn">Show Explanation</button>';
|
|
332
|
+
html += '</div>';
|
|
333
|
+
|
|
334
|
+
// Collapsible explanation
|
|
335
|
+
html += '<div class="rubyn-explanation rubyn-explanation--collapsed" id="explanation-section">';
|
|
336
|
+
html += '<pre class="rubyn-tool-output"><code>' + escapeHtml(response) + '</code></pre>';
|
|
337
|
+
html += '</div>';
|
|
338
|
+
|
|
339
|
+
container.innerHTML = html;
|
|
340
|
+
highlightCodeBlocks(container);
|
|
341
|
+
|
|
342
|
+
// Wire up Apply All
|
|
343
|
+
var applyAllBtn = document.getElementById("apply-all-btn");
|
|
344
|
+
applyAllBtn.addEventListener("click", function() {
|
|
345
|
+
applyAllBtn.disabled = true;
|
|
346
|
+
applyAllBtn.textContent = "Applying...";
|
|
347
|
+
applyAllFiles(fileChanges, headers, applyAllBtn, container, file);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Wire up Discard (clears cache and reloads)
|
|
351
|
+
document.getElementById("clear-cache-btn").addEventListener("click", function() {
|
|
352
|
+
var cacheKey = "rubyn:refactor:" + file;
|
|
353
|
+
sessionStorage.removeItem(cacheKey);
|
|
354
|
+
window.location.reload();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Wire up explanation toggle
|
|
358
|
+
var toggleBtn = document.getElementById("toggle-explanation-btn");
|
|
359
|
+
var explanationSection = document.getElementById("explanation-section");
|
|
360
|
+
toggleBtn.addEventListener("click", function() {
|
|
361
|
+
explanationSection.classList.toggle("rubyn-explanation--collapsed");
|
|
362
|
+
toggleBtn.textContent = explanationSection.classList.contains("rubyn-explanation--collapsed")
|
|
363
|
+
? "Show Explanation" : "Hide Explanation";
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Wire up individual apply buttons
|
|
367
|
+
container.querySelectorAll(".rubyn-apply-one").forEach(function(btn) {
|
|
368
|
+
btn.addEventListener("click", function() {
|
|
369
|
+
var idx = parseInt(btn.getAttribute("data-index"));
|
|
370
|
+
var change = fileChanges[idx];
|
|
371
|
+
applyRefactor(change.path, change.code, headers, btn);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Wire up copy buttons
|
|
376
|
+
container.querySelectorAll(".rubyn-copy-btn").forEach(function(btn) {
|
|
377
|
+
btn.addEventListener("click", function() {
|
|
378
|
+
var idx = parseInt(btn.getAttribute("data-index"));
|
|
379
|
+
navigator.clipboard.writeText(codeBlocks[idx]);
|
|
380
|
+
btn.textContent = "Copied!";
|
|
381
|
+
setTimeout(function() { btn.textContent = "Copy"; }, 2000);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function applyAllFiles(fileChanges, headers, button, container, sourceFile) {
|
|
387
|
+
var applied = 0;
|
|
388
|
+
var failed = 0;
|
|
389
|
+
var total = fileChanges.length;
|
|
390
|
+
|
|
391
|
+
fileChanges.forEach(function(change, i) {
|
|
392
|
+
trackRequest(
|
|
393
|
+
fetch(rubynMountPath() + "/refactor", {
|
|
394
|
+
method: "PATCH",
|
|
395
|
+
headers: headers,
|
|
396
|
+
body: JSON.stringify({ file: change.path, code: change.code })
|
|
397
|
+
})
|
|
398
|
+
.then(function(res) { return res.json(); })
|
|
399
|
+
.then(function(data) {
|
|
400
|
+
if (data.success) {
|
|
401
|
+
applied++;
|
|
402
|
+
// Mark the individual block as applied
|
|
403
|
+
var applyBtn = container.querySelector('.rubyn-apply-one[data-index="' + i + '"]');
|
|
404
|
+
if (applyBtn) {
|
|
405
|
+
applyBtn.textContent = "Applied";
|
|
406
|
+
applyBtn.className = "rubyn-btn-sm rubyn-btn--success";
|
|
407
|
+
applyBtn.disabled = true;
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
failed++;
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
.catch(function() { failed++; })
|
|
414
|
+
.finally(function() {
|
|
415
|
+
if (applied + failed === total) {
|
|
416
|
+
if (failed === 0) {
|
|
417
|
+
button.textContent = "All " + applied + " files applied";
|
|
418
|
+
button.className = "rubyn-btn rubyn-btn--success";
|
|
419
|
+
// Clear cached response — changes are on disk now
|
|
420
|
+
sessionStorage.removeItem("rubyn:refactor:" + sourceFile);
|
|
421
|
+
} else {
|
|
422
|
+
button.textContent = applied + " applied, " + failed + " failed";
|
|
423
|
+
button.disabled = false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderSpecResponse(container, file, response, codeBlocks, headers) {
|
|
432
|
+
var specPath = deriveSpecPath(file);
|
|
433
|
+
var html = '';
|
|
434
|
+
|
|
435
|
+
html += '<div class="rubyn-code-block">';
|
|
436
|
+
html += '<div class="rubyn-code-block-header">';
|
|
437
|
+
html += '<span class="rubyn-tool-filepath">' + escapeHtml(specPath) + '</span>';
|
|
438
|
+
html += '<button class="rubyn-btn-sm rubyn-copy-btn" id="copy-spec-btn">Copy</button>';
|
|
439
|
+
html += '</div>';
|
|
440
|
+
html += '<pre class="rubyn-tool-output"><code>' + escapeHtml(codeBlocks[0]) + '</code></pre>';
|
|
441
|
+
html += '</div>';
|
|
442
|
+
|
|
443
|
+
html += '<div class="rubyn-tool-actions">';
|
|
444
|
+
html += '<button class="rubyn-btn" id="write-spec-btn">Write to ' + escapeHtml(specPath) + '</button>';
|
|
445
|
+
html += '</div>';
|
|
446
|
+
|
|
447
|
+
if (codeBlocks.length > 1 || response.length > codeBlocks[0].length + 50) {
|
|
448
|
+
html += '<div class="rubyn-explanation rubyn-explanation--collapsed" id="explanation-section">';
|
|
449
|
+
html += '<pre class="rubyn-tool-output"><code>' + escapeHtml(response) + '</code></pre>';
|
|
450
|
+
html += '</div>';
|
|
451
|
+
html += '<button class="rubyn-btn-sm" id="toggle-explanation-btn" style="margin-top:0.5rem">Show Full Response</button>';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
container.innerHTML = html;
|
|
455
|
+
highlightCodeBlocks(container);
|
|
456
|
+
|
|
457
|
+
document.getElementById("write-spec-btn").addEventListener("click", function() {
|
|
458
|
+
applyRefactor(specPath, codeBlocks[0], headers, this);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
document.getElementById("copy-spec-btn").addEventListener("click", function() {
|
|
462
|
+
navigator.clipboard.writeText(codeBlocks[0]);
|
|
463
|
+
this.textContent = "Copied!";
|
|
464
|
+
var btn = this;
|
|
465
|
+
setTimeout(function() { btn.textContent = "Copy"; }, 2000);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
var expToggle = document.getElementById("toggle-explanation-btn");
|
|
469
|
+
if (expToggle) {
|
|
470
|
+
expToggle.addEventListener("click", function() {
|
|
471
|
+
var section = document.getElementById("explanation-section");
|
|
472
|
+
section.classList.toggle("rubyn-explanation--collapsed");
|
|
473
|
+
expToggle.textContent = section.classList.contains("rubyn-explanation--collapsed")
|
|
474
|
+
? "Show Full Response" : "Hide Full Response";
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function extractFileHeaders(text) {
|
|
480
|
+
var headers = [];
|
|
481
|
+
|
|
482
|
+
// Match path headers directly above ```ruby blocks
|
|
483
|
+
// Handles: `[NEW] path.rb`, `path.rb`, #### path.rb, etc.
|
|
484
|
+
var regex = /(?:\[NEW\]\s*)?`?([a-zA-Z0-9_\/\.\-]+\.rb)`?\s*\n```ruby/g;
|
|
485
|
+
var match;
|
|
486
|
+
while ((match = regex.exec(text)) !== null) {
|
|
487
|
+
var isNew = match[0].indexOf("[NEW]") !== -1;
|
|
488
|
+
headers.push({ tag: isNew ? "NEW" : null, path: match[1] });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return headers;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function extractCodeBlocks(text) {
|
|
495
|
+
var blocks = [];
|
|
496
|
+
var regex = /```ruby\n([\s\S]*?)```/g;
|
|
497
|
+
var match;
|
|
498
|
+
while ((match = regex.exec(text)) !== null) {
|
|
499
|
+
blocks.push(match[1]);
|
|
500
|
+
}
|
|
501
|
+
return blocks;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function deriveSpecPath(filePath) {
|
|
505
|
+
return filePath.replace(/^app\//, "spec/").replace(/\.rb$/, "_spec.rb");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function applyRefactor(file, code, headers, button) {
|
|
509
|
+
button.disabled = true;
|
|
510
|
+
button.textContent = "Applying...";
|
|
511
|
+
|
|
512
|
+
trackRequest(
|
|
513
|
+
fetch(rubynMountPath() + "/refactor", {
|
|
514
|
+
method: "PATCH",
|
|
515
|
+
headers: headers,
|
|
516
|
+
body: JSON.stringify({ file: file, code: code })
|
|
517
|
+
})
|
|
518
|
+
.then(function(res) { return res.json(); })
|
|
519
|
+
.then(function(data) {
|
|
520
|
+
if (data.success) {
|
|
521
|
+
button.textContent = data.message || "Applied";
|
|
522
|
+
button.className = "rubyn-btn rubyn-btn--success";
|
|
523
|
+
// Clear cache for this file
|
|
524
|
+
sessionStorage.removeItem("rubyn:refactor:" + file);
|
|
525
|
+
sessionStorage.removeItem("rubyn:specs:" + file);
|
|
526
|
+
} else {
|
|
527
|
+
button.textContent = "Failed: " + (data.error || "Unknown error");
|
|
528
|
+
button.disabled = false;
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
.catch(function(err) {
|
|
532
|
+
button.textContent = "Error: " + err.message;
|
|
533
|
+
button.disabled = false;
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function escapeHtml(text) {
|
|
539
|
+
var div = document.createElement("div");
|
|
540
|
+
div.textContent = text;
|
|
541
|
+
return div.innerHTML;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Convert markdown-style code blocks to <pre><code> and escape the rest
|
|
545
|
+
function formatMessageWithCode(text) {
|
|
546
|
+
var parts = text.split(/(```\w*\n[\s\S]*?```)/g);
|
|
547
|
+
return parts.map(function(part) {
|
|
548
|
+
var match = part.match(/^```(\w*)\n([\s\S]*?)```$/);
|
|
549
|
+
if (match) {
|
|
550
|
+
var lang = match[1] || "ruby";
|
|
551
|
+
return '<pre class="rubyn-tool-output"><code class="language-' + lang + '">' + escapeHtml(match[2]) + '</code></pre>';
|
|
552
|
+
}
|
|
553
|
+
return '<p>' + escapeHtml(part).replace(/\n/g, '<br>') + '</p>';
|
|
554
|
+
}).join('');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function highlightCodeBlocks(container) {
|
|
558
|
+
if (typeof hljs === "undefined") return;
|
|
559
|
+
container.querySelectorAll("pre code").forEach(function(block) {
|
|
560
|
+
// Add language class for highlight.js
|
|
561
|
+
if (!block.className) block.className = "language-ruby";
|
|
562
|
+
hljs.highlightElement(block);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function rubynMountPath() {
|
|
567
|
+
var path = window.location.pathname;
|
|
568
|
+
var segments = ["agent", "refactor", "specs", "reviews", "files", "settings"];
|
|
569
|
+
for (var i = 0; i < segments.length; i++) {
|
|
570
|
+
var idx = path.indexOf("/" + segments[i]);
|
|
571
|
+
if (idx > 0) return path.substring(0, idx);
|
|
572
|
+
}
|
|
573
|
+
return "/rubyn";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function mountPath() {
|
|
577
|
+
return rubynMountPath();
|
|
578
|
+
}
|
|
579
|
+
})();
|