@mehmetsagir/git-ai 0.0.20 → 0.0.26
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.
- package/README.md +25 -0
- package/dist/add.d.ts +5 -0
- package/dist/add.d.ts.map +1 -0
- package/dist/add.js +98 -0
- package/dist/add.js.map +1 -0
- package/dist/commit.d.ts +1 -1
- package/dist/commit.d.ts.map +1 -1
- package/dist/commit.js +37 -4
- package/dist/commit.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +12 -2
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +200 -8
- package/dist/git.js.map +1 -1
- package/dist/index.js +149 -51
- package/dist/index.js.map +1 -1
- package/dist/openai.d.ts +3 -0
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +18 -0
- package/dist/openai.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +46 -0
- package/dist/prompts.js.map +1 -1
- package/dist/set-editor.d.ts +5 -0
- package/dist/set-editor.d.ts.map +1 -0
- package/dist/set-editor.js +59 -0
- package/dist/set-editor.js.map +1 -0
- package/dist/summary.d.ts +5 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +175 -0
- package/dist/summary.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +2 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +2102 -0
- package/dist/ui.js.map +1 -0
- package/dist/update.d.ts +9 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +68 -0
- package/dist/update.js.map +1 -0
- package/dist/user-management.d.ts +10 -0
- package/dist/user-management.d.ts.map +1 -0
- package/dist/user-management.js +175 -0
- package/dist/user-management.js.map +1 -0
- package/dist/users.d.ts +9 -0
- package/dist/users.d.ts.map +1 -0
- package/dist/users.js +129 -0
- package/dist/users.js.map +1 -0
- package/dist/utils/commit-file.d.ts +30 -0
- package/dist/utils/commit-file.d.ts.map +1 -0
- package/dist/utils/commit-file.js +192 -0
- package/dist/utils/commit-file.js.map +1 -0
- package/dist/utils/editor.d.ts +29 -0
- package/dist/utils/editor.d.ts.map +1 -0
- package/dist/utils/editor.js +245 -0
- package/dist/utils/editor.js.map +1 -0
- package/dist/utils/validation.d.ts +24 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +81 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +3 -2
package/dist/ui.js
ADDED
|
@@ -0,0 +1,2102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runUI = runUI;
|
|
40
|
+
const http = __importStar(require("http"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const git = __importStar(require("./git"));
|
|
45
|
+
const openai = __importStar(require("./openai"));
|
|
46
|
+
const config_1 = require("./config");
|
|
47
|
+
const hunk_parser_1 = require("./utils/hunk-parser");
|
|
48
|
+
// SSE clients for real-time updates
|
|
49
|
+
const sseClients = new Set();
|
|
50
|
+
// File watcher with debounce
|
|
51
|
+
let fileWatcher = null;
|
|
52
|
+
let watchDebounceTimer = null;
|
|
53
|
+
function notifyClients() {
|
|
54
|
+
// Debounce notifications
|
|
55
|
+
if (watchDebounceTimer)
|
|
56
|
+
clearTimeout(watchDebounceTimer);
|
|
57
|
+
watchDebounceTimer = setTimeout(() => {
|
|
58
|
+
sseClients.forEach((client) => {
|
|
59
|
+
try {
|
|
60
|
+
client.write(`data: refresh\n\n`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
sseClients.delete(client);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}, 300);
|
|
67
|
+
}
|
|
68
|
+
function startFileWatcher() {
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
try {
|
|
71
|
+
fileWatcher = fs.watch(cwd, { recursive: true }, (_eventType, filename) => {
|
|
72
|
+
// Ignore .git directory and node_modules
|
|
73
|
+
if (filename && (filename.startsWith(".git") || filename.includes("node_modules"))) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
notifyClients();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Fallback: no file watching
|
|
81
|
+
console.log(chalk_1.default.yellow("File watching not available"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function stopFileWatcher() {
|
|
85
|
+
if (fileWatcher) {
|
|
86
|
+
fileWatcher.close();
|
|
87
|
+
fileWatcher = null;
|
|
88
|
+
}
|
|
89
|
+
if (watchDebounceTimer) {
|
|
90
|
+
clearTimeout(watchDebounceTimer);
|
|
91
|
+
watchDebounceTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const PORT = 3848;
|
|
95
|
+
async function getFileDiff(file, status, staged) {
|
|
96
|
+
try {
|
|
97
|
+
// For NEW files, always read file content directly to ensure full content
|
|
98
|
+
if (status === "new") {
|
|
99
|
+
const filePath = path.resolve(process.cwd(), file);
|
|
100
|
+
if (fs.existsSync(filePath)) {
|
|
101
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
102
|
+
const lines = content.split("\n");
|
|
103
|
+
// Remove trailing empty line if exists
|
|
104
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
105
|
+
lines.pop();
|
|
106
|
+
}
|
|
107
|
+
const diffLines = lines.map((line) => `+${line}`).join("\n");
|
|
108
|
+
return `diff --git a/${file} b/${file}
|
|
109
|
+
new file mode 100644
|
|
110
|
+
--- /dev/null
|
|
111
|
+
+++ b/${file}
|
|
112
|
+
@@ -0,0 +1,${lines.length} @@
|
|
113
|
+
${diffLines}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// For modified/deleted files, get diff from git
|
|
117
|
+
let fullDiff;
|
|
118
|
+
if (staged === true) {
|
|
119
|
+
fullDiff = await git.getStagedDiff();
|
|
120
|
+
}
|
|
121
|
+
else if (staged === false) {
|
|
122
|
+
fullDiff = await git.getUnstagedDiff();
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
fullDiff = await git.getFullDiff();
|
|
126
|
+
}
|
|
127
|
+
const parts = fullDiff.split(/(?=diff --git )/);
|
|
128
|
+
// Look for exact match in diff header
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
// Check if this diff is for our file (exact match in header)
|
|
131
|
+
const headerMatch = part.match(/^diff --git a\/(.+?) b\/(.+?)[\r\n]/);
|
|
132
|
+
if (headerMatch && (headerMatch[1] === file || headerMatch[2] === file)) {
|
|
133
|
+
return part;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function getHtml() {
|
|
143
|
+
return `<!DOCTYPE html>
|
|
144
|
+
<html>
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="UTF-8">
|
|
147
|
+
<title>git-ai - Commit Manager</title>
|
|
148
|
+
<style>
|
|
149
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
150
|
+
body {
|
|
151
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
152
|
+
background: #1e1e1e;
|
|
153
|
+
color: #cccccc;
|
|
154
|
+
height: 100vh;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Header */
|
|
161
|
+
.app-header {
|
|
162
|
+
padding: 12px 20px;
|
|
163
|
+
background: #252526;
|
|
164
|
+
border-bottom: 1px solid #3c3c3c;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: space-between;
|
|
168
|
+
}
|
|
169
|
+
.app-title {
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: #fff;
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
}
|
|
177
|
+
.app-title span {
|
|
178
|
+
color: #4ec9b0;
|
|
179
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
180
|
+
}
|
|
181
|
+
.header-actions {
|
|
182
|
+
display: flex;
|
|
183
|
+
gap: 8px;
|
|
184
|
+
}
|
|
185
|
+
.btn {
|
|
186
|
+
padding: 8px 16px;
|
|
187
|
+
border: none;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
font-weight: 500;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
display: inline-flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 6px;
|
|
195
|
+
transition: background 0.15s, opacity 0.15s;
|
|
196
|
+
}
|
|
197
|
+
.btn:disabled {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
cursor: not-allowed;
|
|
200
|
+
}
|
|
201
|
+
.btn-primary {
|
|
202
|
+
background: #0e639c;
|
|
203
|
+
color: #fff;
|
|
204
|
+
}
|
|
205
|
+
.btn-primary:hover:not(:disabled) {
|
|
206
|
+
background: #1177bb;
|
|
207
|
+
}
|
|
208
|
+
.btn-success {
|
|
209
|
+
background: #238636;
|
|
210
|
+
color: #fff;
|
|
211
|
+
}
|
|
212
|
+
.btn-success:hover:not(:disabled) {
|
|
213
|
+
background: #2ea043;
|
|
214
|
+
}
|
|
215
|
+
.btn-secondary {
|
|
216
|
+
background: #3c3c3c;
|
|
217
|
+
color: #fff;
|
|
218
|
+
}
|
|
219
|
+
.btn-secondary:hover:not(:disabled) {
|
|
220
|
+
background: #4c4c4c;
|
|
221
|
+
}
|
|
222
|
+
.btn svg {
|
|
223
|
+
width: 14px;
|
|
224
|
+
height: 14px;
|
|
225
|
+
pointer-events: none;
|
|
226
|
+
}
|
|
227
|
+
.btn * {
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Main Container */
|
|
232
|
+
.container {
|
|
233
|
+
display: flex;
|
|
234
|
+
flex: 1;
|
|
235
|
+
overflow: hidden;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Sidebar - File List */
|
|
239
|
+
.sidebar {
|
|
240
|
+
width: 320px;
|
|
241
|
+
background: #252526;
|
|
242
|
+
border-right: 1px solid #3c3c3c;
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: column;
|
|
245
|
+
flex-shrink: 0;
|
|
246
|
+
}
|
|
247
|
+
.sidebar-header {
|
|
248
|
+
padding: 12px 16px;
|
|
249
|
+
background: #2d2d2d;
|
|
250
|
+
border-bottom: 1px solid #3c3c3c;
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
}
|
|
255
|
+
.sidebar-title {
|
|
256
|
+
font-size: 11px;
|
|
257
|
+
text-transform: uppercase;
|
|
258
|
+
letter-spacing: 0.5px;
|
|
259
|
+
color: #bbbbbb;
|
|
260
|
+
}
|
|
261
|
+
.file-count {
|
|
262
|
+
font-size: 11px;
|
|
263
|
+
color: #6e7681;
|
|
264
|
+
}
|
|
265
|
+
.file-sections {
|
|
266
|
+
flex: 1;
|
|
267
|
+
overflow-y: auto;
|
|
268
|
+
}
|
|
269
|
+
.file-section {
|
|
270
|
+
border-bottom: 1px solid #3c3c3c;
|
|
271
|
+
}
|
|
272
|
+
.section-header {
|
|
273
|
+
padding: 8px 12px;
|
|
274
|
+
background: #2d2d2d;
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
gap: 6px;
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
user-select: none;
|
|
280
|
+
}
|
|
281
|
+
.section-header:hover {
|
|
282
|
+
background: #333333;
|
|
283
|
+
}
|
|
284
|
+
.section-chevron {
|
|
285
|
+
width: 16px;
|
|
286
|
+
height: 16px;
|
|
287
|
+
transition: transform 0.15s;
|
|
288
|
+
flex-shrink: 0;
|
|
289
|
+
}
|
|
290
|
+
.section-chevron.collapsed {
|
|
291
|
+
transform: rotate(-90deg);
|
|
292
|
+
}
|
|
293
|
+
.section-title {
|
|
294
|
+
font-size: 11px;
|
|
295
|
+
text-transform: uppercase;
|
|
296
|
+
letter-spacing: 0.5px;
|
|
297
|
+
color: #bbbbbb;
|
|
298
|
+
flex: 1;
|
|
299
|
+
}
|
|
300
|
+
.section-count {
|
|
301
|
+
font-size: 11px;
|
|
302
|
+
color: #6e7681;
|
|
303
|
+
background: #3c3c3c;
|
|
304
|
+
padding: 2px 6px;
|
|
305
|
+
border-radius: 10px;
|
|
306
|
+
min-width: 20px;
|
|
307
|
+
text-align: center;
|
|
308
|
+
}
|
|
309
|
+
.section-action {
|
|
310
|
+
width: 20px;
|
|
311
|
+
height: 20px;
|
|
312
|
+
padding: 2px;
|
|
313
|
+
background: none;
|
|
314
|
+
border: none;
|
|
315
|
+
color: #8b949e;
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
border-radius: 3px;
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: center;
|
|
321
|
+
}
|
|
322
|
+
.section-action:hover {
|
|
323
|
+
background: #3c3c3c;
|
|
324
|
+
color: #e1e1e1;
|
|
325
|
+
}
|
|
326
|
+
.section-action svg {
|
|
327
|
+
width: 14px;
|
|
328
|
+
height: 14px;
|
|
329
|
+
}
|
|
330
|
+
.file-list {
|
|
331
|
+
overflow-y: auto;
|
|
332
|
+
}
|
|
333
|
+
.file-list.collapsed {
|
|
334
|
+
display: none;
|
|
335
|
+
}
|
|
336
|
+
.file-item {
|
|
337
|
+
padding: 8px 16px;
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 10px;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
border-bottom: 1px solid #2d2d2d;
|
|
343
|
+
transition: background 0.1s;
|
|
344
|
+
}
|
|
345
|
+
.file-item:hover {
|
|
346
|
+
background: #2a2d2e;
|
|
347
|
+
}
|
|
348
|
+
.file-item.selected {
|
|
349
|
+
background: #094771;
|
|
350
|
+
}
|
|
351
|
+
.custom-checkbox {
|
|
352
|
+
width: 18px;
|
|
353
|
+
height: 18px;
|
|
354
|
+
border: 2px solid #6e7681;
|
|
355
|
+
border-radius: 3px;
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
justify-content: center;
|
|
360
|
+
transition: all 0.15s;
|
|
361
|
+
flex-shrink: 0;
|
|
362
|
+
background: rgba(255, 255, 255, 0.05);
|
|
363
|
+
}
|
|
364
|
+
.custom-checkbox:hover {
|
|
365
|
+
border-color: #58a6ff;
|
|
366
|
+
background: rgba(88, 166, 255, 0.1);
|
|
367
|
+
}
|
|
368
|
+
.custom-checkbox.checked {
|
|
369
|
+
background: #0e639c;
|
|
370
|
+
border-color: #0e639c;
|
|
371
|
+
}
|
|
372
|
+
.file-item.selected .custom-checkbox {
|
|
373
|
+
border-color: #8b949e;
|
|
374
|
+
}
|
|
375
|
+
.file-item.selected .custom-checkbox.checked {
|
|
376
|
+
border-color: #0e639c;
|
|
377
|
+
}
|
|
378
|
+
.stage-btn {
|
|
379
|
+
width: 22px;
|
|
380
|
+
height: 22px;
|
|
381
|
+
padding: 3px;
|
|
382
|
+
background: none;
|
|
383
|
+
border: none;
|
|
384
|
+
color: #6e7681;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
border-radius: 3px;
|
|
387
|
+
display: flex;
|
|
388
|
+
align-items: center;
|
|
389
|
+
justify-content: center;
|
|
390
|
+
opacity: 0;
|
|
391
|
+
transition: opacity 0.15s, background 0.15s;
|
|
392
|
+
}
|
|
393
|
+
.file-item:hover .stage-btn {
|
|
394
|
+
opacity: 1;
|
|
395
|
+
}
|
|
396
|
+
.stage-btn:hover {
|
|
397
|
+
background: #3c3c3c;
|
|
398
|
+
color: #e1e1e1;
|
|
399
|
+
}
|
|
400
|
+
.stage-btn svg {
|
|
401
|
+
width: 14px;
|
|
402
|
+
height: 14px;
|
|
403
|
+
}
|
|
404
|
+
.custom-checkbox svg {
|
|
405
|
+
width: 12px;
|
|
406
|
+
height: 12px;
|
|
407
|
+
color: #fff;
|
|
408
|
+
opacity: 0;
|
|
409
|
+
transform: scale(0.5);
|
|
410
|
+
transition: all 0.15s;
|
|
411
|
+
}
|
|
412
|
+
.custom-checkbox.checked svg {
|
|
413
|
+
opacity: 1;
|
|
414
|
+
transform: scale(1);
|
|
415
|
+
}
|
|
416
|
+
.file-info {
|
|
417
|
+
flex: 1;
|
|
418
|
+
min-width: 0;
|
|
419
|
+
}
|
|
420
|
+
.file-name {
|
|
421
|
+
font-size: 13px;
|
|
422
|
+
white-space: nowrap;
|
|
423
|
+
overflow: hidden;
|
|
424
|
+
text-overflow: ellipsis;
|
|
425
|
+
}
|
|
426
|
+
.file-path {
|
|
427
|
+
font-size: 11px;
|
|
428
|
+
color: #6e7681;
|
|
429
|
+
white-space: nowrap;
|
|
430
|
+
overflow: hidden;
|
|
431
|
+
text-overflow: ellipsis;
|
|
432
|
+
}
|
|
433
|
+
.file-status {
|
|
434
|
+
width: 18px;
|
|
435
|
+
height: 18px;
|
|
436
|
+
border-radius: 3px;
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
justify-content: center;
|
|
440
|
+
font-size: 10px;
|
|
441
|
+
font-weight: 600;
|
|
442
|
+
flex-shrink: 0;
|
|
443
|
+
}
|
|
444
|
+
.file-status.new { background: #238636; color: #fff; }
|
|
445
|
+
.file-status.modified { background: #9e6a03; color: #fff; }
|
|
446
|
+
.file-status.deleted { background: #da3633; color: #fff; }
|
|
447
|
+
.file-status.renamed { background: #8957e5; color: #fff; }
|
|
448
|
+
|
|
449
|
+
.select-actions {
|
|
450
|
+
padding: 8px 16px;
|
|
451
|
+
background: #2d2d2d;
|
|
452
|
+
border-bottom: 1px solid #3c3c3c;
|
|
453
|
+
display: flex;
|
|
454
|
+
gap: 8px;
|
|
455
|
+
}
|
|
456
|
+
.select-actions button {
|
|
457
|
+
padding: 4px 8px;
|
|
458
|
+
font-size: 11px;
|
|
459
|
+
background: none;
|
|
460
|
+
border: 1px solid #3c3c3c;
|
|
461
|
+
color: #bbbbbb;
|
|
462
|
+
border-radius: 3px;
|
|
463
|
+
cursor: pointer;
|
|
464
|
+
transition: background 0.15s;
|
|
465
|
+
}
|
|
466
|
+
.select-actions button:hover {
|
|
467
|
+
background: #3c3c3c;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Main Panel */
|
|
471
|
+
.main-panel {
|
|
472
|
+
flex: 1;
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
overflow: hidden;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Diff Viewer */
|
|
479
|
+
.diff-panel {
|
|
480
|
+
flex: 1;
|
|
481
|
+
overflow: hidden;
|
|
482
|
+
display: flex;
|
|
483
|
+
flex-direction: column;
|
|
484
|
+
}
|
|
485
|
+
.diff-header {
|
|
486
|
+
padding: 10px 16px;
|
|
487
|
+
background: #2d2d2d;
|
|
488
|
+
border-bottom: 1px solid #3c3c3c;
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
justify-content: space-between;
|
|
492
|
+
}
|
|
493
|
+
.diff-filename {
|
|
494
|
+
font-size: 13px;
|
|
495
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
496
|
+
color: #dcdcaa;
|
|
497
|
+
}
|
|
498
|
+
.diff-view-toggle {
|
|
499
|
+
display: flex;
|
|
500
|
+
background: #1e1e1e;
|
|
501
|
+
border-radius: 4px;
|
|
502
|
+
overflow: hidden;
|
|
503
|
+
}
|
|
504
|
+
.diff-view-toggle button {
|
|
505
|
+
padding: 4px 10px;
|
|
506
|
+
border: none;
|
|
507
|
+
background: none;
|
|
508
|
+
color: #8b949e;
|
|
509
|
+
font-size: 11px;
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
display: flex;
|
|
512
|
+
align-items: center;
|
|
513
|
+
gap: 4px;
|
|
514
|
+
transition: all 0.15s;
|
|
515
|
+
}
|
|
516
|
+
.diff-view-toggle button:hover {
|
|
517
|
+
color: #e1e1e1;
|
|
518
|
+
}
|
|
519
|
+
.diff-view-toggle button.active {
|
|
520
|
+
background: #0e639c;
|
|
521
|
+
color: #fff;
|
|
522
|
+
}
|
|
523
|
+
.diff-view-toggle button svg {
|
|
524
|
+
width: 14px;
|
|
525
|
+
height: 14px;
|
|
526
|
+
}
|
|
527
|
+
.diff-content {
|
|
528
|
+
flex: 1;
|
|
529
|
+
overflow: auto;
|
|
530
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
531
|
+
font-size: 13px;
|
|
532
|
+
line-height: 20px;
|
|
533
|
+
display: flex;
|
|
534
|
+
flex-direction: column;
|
|
535
|
+
}
|
|
536
|
+
/* Unified diff view */
|
|
537
|
+
.diff-unified .diff-line {
|
|
538
|
+
padding: 0 16px;
|
|
539
|
+
white-space: pre;
|
|
540
|
+
min-height: 20px;
|
|
541
|
+
}
|
|
542
|
+
.diff-unified .diff-line.add { background: #2ea04326; color: #3fb950; }
|
|
543
|
+
.diff-unified .diff-line.del { background: #f8514926; color: #f85149; }
|
|
544
|
+
.diff-unified .diff-line.hunk { background: #388bfd26; color: #58a6ff; }
|
|
545
|
+
.diff-unified .diff-line.header { color: #6e7681; }
|
|
546
|
+
.diff-unified .line-num {
|
|
547
|
+
display: inline-block;
|
|
548
|
+
width: 40px;
|
|
549
|
+
color: #6e7681;
|
|
550
|
+
text-align: right;
|
|
551
|
+
margin-right: 16px;
|
|
552
|
+
user-select: none;
|
|
553
|
+
}
|
|
554
|
+
/* Split diff view */
|
|
555
|
+
.diff-split {
|
|
556
|
+
display: flex;
|
|
557
|
+
height: 100%;
|
|
558
|
+
}
|
|
559
|
+
.diff-split-pane {
|
|
560
|
+
flex: 1;
|
|
561
|
+
overflow: auto;
|
|
562
|
+
border-right: 1px solid #3c3c3c;
|
|
563
|
+
}
|
|
564
|
+
.diff-split-pane:last-child {
|
|
565
|
+
border-right: none;
|
|
566
|
+
}
|
|
567
|
+
.diff-split-pane-header {
|
|
568
|
+
padding: 8px 16px;
|
|
569
|
+
background: #2d2d2d;
|
|
570
|
+
border-bottom: 1px solid #3c3c3c;
|
|
571
|
+
font-size: 11px;
|
|
572
|
+
color: #8b949e;
|
|
573
|
+
position: sticky;
|
|
574
|
+
top: 0;
|
|
575
|
+
z-index: 1;
|
|
576
|
+
}
|
|
577
|
+
.diff-split-pane-content {
|
|
578
|
+
min-height: 100%;
|
|
579
|
+
}
|
|
580
|
+
.diff-split .diff-line {
|
|
581
|
+
padding: 0 16px;
|
|
582
|
+
white-space: pre;
|
|
583
|
+
min-height: 20px;
|
|
584
|
+
}
|
|
585
|
+
.diff-split .diff-line.add { background: #2ea04326; color: #3fb950; }
|
|
586
|
+
.diff-split .diff-line.del { background: #f8514926; color: #f85149; }
|
|
587
|
+
.diff-split .diff-line.empty { background: #2d2d2d; }
|
|
588
|
+
.diff-split .diff-line.hunk { background: #388bfd26; color: #58a6ff; }
|
|
589
|
+
.diff-split .line-num {
|
|
590
|
+
display: inline-block;
|
|
591
|
+
width: 40px;
|
|
592
|
+
color: #6e7681;
|
|
593
|
+
text-align: right;
|
|
594
|
+
margin-right: 16px;
|
|
595
|
+
user-select: none;
|
|
596
|
+
}
|
|
597
|
+
/* Legacy support */
|
|
598
|
+
.diff-line {
|
|
599
|
+
padding: 0 16px;
|
|
600
|
+
white-space: pre;
|
|
601
|
+
min-height: 20px;
|
|
602
|
+
}
|
|
603
|
+
.diff-line.add { background: #2ea04326; color: #3fb950; }
|
|
604
|
+
.diff-line.del { background: #f8514926; color: #f85149; }
|
|
605
|
+
.diff-line.hunk { background: #388bfd26; color: #58a6ff; }
|
|
606
|
+
.diff-line.header { color: #6e7681; }
|
|
607
|
+
.line-num {
|
|
608
|
+
display: inline-block;
|
|
609
|
+
width: 40px;
|
|
610
|
+
color: #6e7681;
|
|
611
|
+
text-align: right;
|
|
612
|
+
margin-right: 16px;
|
|
613
|
+
user-select: none;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.empty-state {
|
|
617
|
+
flex: 1;
|
|
618
|
+
display: flex;
|
|
619
|
+
flex-direction: column;
|
|
620
|
+
align-items: center;
|
|
621
|
+
justify-content: center;
|
|
622
|
+
color: #6e7681;
|
|
623
|
+
gap: 12px;
|
|
624
|
+
min-height: 100%;
|
|
625
|
+
}
|
|
626
|
+
.empty-state svg {
|
|
627
|
+
width: 48px;
|
|
628
|
+
height: 48px;
|
|
629
|
+
opacity: 0.5;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* Commit Plan View (in main panel) */
|
|
633
|
+
.commit-plan-view {
|
|
634
|
+
flex: 1;
|
|
635
|
+
overflow-y: auto;
|
|
636
|
+
padding: 16px;
|
|
637
|
+
}
|
|
638
|
+
.commit-plan-header {
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
justify-content: space-between;
|
|
642
|
+
margin-bottom: 16px;
|
|
643
|
+
padding-bottom: 12px;
|
|
644
|
+
border-bottom: 1px solid #3c3c3c;
|
|
645
|
+
}
|
|
646
|
+
.commit-plan-title {
|
|
647
|
+
font-size: 14px;
|
|
648
|
+
font-weight: 600;
|
|
649
|
+
color: #e1e1e1;
|
|
650
|
+
}
|
|
651
|
+
.commit-plan-actions {
|
|
652
|
+
display: flex;
|
|
653
|
+
gap: 8px;
|
|
654
|
+
}
|
|
655
|
+
.commit-group {
|
|
656
|
+
background: #252526;
|
|
657
|
+
border: 1px solid #3c3c3c;
|
|
658
|
+
border-radius: 6px;
|
|
659
|
+
margin-bottom: 16px;
|
|
660
|
+
overflow: hidden;
|
|
661
|
+
}
|
|
662
|
+
.commit-group:last-child {
|
|
663
|
+
margin-bottom: 0;
|
|
664
|
+
}
|
|
665
|
+
.group-header {
|
|
666
|
+
padding: 12px 16px;
|
|
667
|
+
background: #2d2d2d;
|
|
668
|
+
display: flex;
|
|
669
|
+
align-items: center;
|
|
670
|
+
gap: 12px;
|
|
671
|
+
border-bottom: 1px solid #3c3c3c;
|
|
672
|
+
cursor: pointer;
|
|
673
|
+
user-select: none;
|
|
674
|
+
}
|
|
675
|
+
.group-header:hover {
|
|
676
|
+
background: #333333;
|
|
677
|
+
}
|
|
678
|
+
.group-chevron {
|
|
679
|
+
width: 16px;
|
|
680
|
+
height: 16px;
|
|
681
|
+
color: #8b949e;
|
|
682
|
+
transition: transform 0.15s;
|
|
683
|
+
flex-shrink: 0;
|
|
684
|
+
}
|
|
685
|
+
.group-chevron.collapsed {
|
|
686
|
+
transform: rotate(-90deg);
|
|
687
|
+
}
|
|
688
|
+
.group-content {
|
|
689
|
+
display: block;
|
|
690
|
+
}
|
|
691
|
+
.group-content.collapsed {
|
|
692
|
+
display: none;
|
|
693
|
+
}
|
|
694
|
+
.group-number {
|
|
695
|
+
width: 28px;
|
|
696
|
+
height: 28px;
|
|
697
|
+
background: #0e639c;
|
|
698
|
+
color: #fff;
|
|
699
|
+
border-radius: 50%;
|
|
700
|
+
display: flex;
|
|
701
|
+
align-items: center;
|
|
702
|
+
justify-content: center;
|
|
703
|
+
font-size: 13px;
|
|
704
|
+
font-weight: 600;
|
|
705
|
+
flex-shrink: 0;
|
|
706
|
+
}
|
|
707
|
+
.group-message {
|
|
708
|
+
font-size: 14px;
|
|
709
|
+
color: #4ec9b0;
|
|
710
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
711
|
+
}
|
|
712
|
+
.group-file-section {
|
|
713
|
+
border-bottom: 1px solid #3c3c3c;
|
|
714
|
+
}
|
|
715
|
+
.group-file-section:last-child {
|
|
716
|
+
border-bottom: none;
|
|
717
|
+
}
|
|
718
|
+
.group-file-header {
|
|
719
|
+
padding: 10px 16px;
|
|
720
|
+
background: #1e1e1e;
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: center;
|
|
723
|
+
gap: 8px;
|
|
724
|
+
font-size: 12px;
|
|
725
|
+
color: #dcdcaa;
|
|
726
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
727
|
+
cursor: pointer;
|
|
728
|
+
user-select: none;
|
|
729
|
+
}
|
|
730
|
+
.group-file-header:hover {
|
|
731
|
+
background: #252526;
|
|
732
|
+
}
|
|
733
|
+
.group-file-chevron {
|
|
734
|
+
width: 14px;
|
|
735
|
+
height: 14px;
|
|
736
|
+
color: #6e7681;
|
|
737
|
+
transition: transform 0.15s;
|
|
738
|
+
flex-shrink: 0;
|
|
739
|
+
}
|
|
740
|
+
.group-file-chevron.collapsed {
|
|
741
|
+
transform: rotate(-90deg);
|
|
742
|
+
}
|
|
743
|
+
.group-file-icon {
|
|
744
|
+
width: 14px;
|
|
745
|
+
height: 14px;
|
|
746
|
+
color: #6e7681;
|
|
747
|
+
}
|
|
748
|
+
.group-file-diff {
|
|
749
|
+
background: #1e1e1e;
|
|
750
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
751
|
+
font-size: 12px;
|
|
752
|
+
line-height: 18px;
|
|
753
|
+
}
|
|
754
|
+
.group-file-diff.collapsed {
|
|
755
|
+
display: none;
|
|
756
|
+
}
|
|
757
|
+
.group-file-diff .diff-line {
|
|
758
|
+
padding: 0 16px;
|
|
759
|
+
white-space: pre;
|
|
760
|
+
}
|
|
761
|
+
.group-file-diff .diff-line.add { background: #2ea04326; color: #3fb950; }
|
|
762
|
+
.group-file-diff .diff-line.del { background: #f8514926; color: #f85149; }
|
|
763
|
+
.group-file-diff .diff-line-num {
|
|
764
|
+
display: inline-block;
|
|
765
|
+
width: 35px;
|
|
766
|
+
color: #6e7681;
|
|
767
|
+
text-align: right;
|
|
768
|
+
margin-right: 12px;
|
|
769
|
+
user-select: none;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* Toast */
|
|
773
|
+
.toast {
|
|
774
|
+
position: fixed;
|
|
775
|
+
top: 20px;
|
|
776
|
+
right: 20px;
|
|
777
|
+
padding: 12px 20px;
|
|
778
|
+
border-radius: 6px;
|
|
779
|
+
font-size: 13px;
|
|
780
|
+
color: #fff;
|
|
781
|
+
opacity: 0;
|
|
782
|
+
transform: translateY(-10px);
|
|
783
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
784
|
+
z-index: 1000;
|
|
785
|
+
}
|
|
786
|
+
.toast.show {
|
|
787
|
+
opacity: 1;
|
|
788
|
+
transform: translateY(0);
|
|
789
|
+
}
|
|
790
|
+
.toast.success { background: #238636; }
|
|
791
|
+
.toast.error { background: #da3633; }
|
|
792
|
+
|
|
793
|
+
/* Loader */
|
|
794
|
+
.loader-overlay {
|
|
795
|
+
position: fixed;
|
|
796
|
+
top: 0;
|
|
797
|
+
left: 0;
|
|
798
|
+
right: 0;
|
|
799
|
+
bottom: 0;
|
|
800
|
+
background: rgba(0, 0, 0, 0.6);
|
|
801
|
+
display: flex;
|
|
802
|
+
flex-direction: column;
|
|
803
|
+
align-items: center;
|
|
804
|
+
justify-content: center;
|
|
805
|
+
gap: 16px;
|
|
806
|
+
z-index: 999;
|
|
807
|
+
opacity: 0;
|
|
808
|
+
visibility: hidden;
|
|
809
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
810
|
+
}
|
|
811
|
+
.loader-overlay.show {
|
|
812
|
+
opacity: 1;
|
|
813
|
+
visibility: visible;
|
|
814
|
+
}
|
|
815
|
+
.loader {
|
|
816
|
+
width: 40px;
|
|
817
|
+
height: 40px;
|
|
818
|
+
border: 3px solid #3c3c3c;
|
|
819
|
+
border-top-color: #58a6ff;
|
|
820
|
+
border-radius: 50%;
|
|
821
|
+
animation: spin 0.8s linear infinite;
|
|
822
|
+
}
|
|
823
|
+
.loader-text {
|
|
824
|
+
color: #e1e1e1;
|
|
825
|
+
font-size: 13px;
|
|
826
|
+
}
|
|
827
|
+
@keyframes spin {
|
|
828
|
+
to { transform: rotate(360deg); }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* Footer */
|
|
832
|
+
.sidebar-footer {
|
|
833
|
+
padding: 12px 16px;
|
|
834
|
+
border-top: 1px solid #3c3c3c;
|
|
835
|
+
background: #1e1e1e;
|
|
836
|
+
text-align: center;
|
|
837
|
+
}
|
|
838
|
+
.sidebar-footer-brand {
|
|
839
|
+
font-size: 12px;
|
|
840
|
+
font-weight: 600;
|
|
841
|
+
color: #e1e1e1;
|
|
842
|
+
margin-bottom: 4px;
|
|
843
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
844
|
+
}
|
|
845
|
+
.sidebar-footer a {
|
|
846
|
+
display: inline-flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
gap: 6px;
|
|
849
|
+
color: #6e7681;
|
|
850
|
+
text-decoration: none;
|
|
851
|
+
font-size: 11px;
|
|
852
|
+
transition: color 0.15s;
|
|
853
|
+
}
|
|
854
|
+
.sidebar-footer a:hover {
|
|
855
|
+
color: #58a6ff;
|
|
856
|
+
}
|
|
857
|
+
.sidebar-footer a svg {
|
|
858
|
+
width: 14px;
|
|
859
|
+
height: 14px;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/* Scrollbar */
|
|
863
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
864
|
+
::-webkit-scrollbar-track { background: #1e1e1e; }
|
|
865
|
+
::-webkit-scrollbar-thumb { background: #424242; border-radius: 5px; }
|
|
866
|
+
::-webkit-scrollbar-thumb:hover { background: #4f4f4f; }
|
|
867
|
+
</style>
|
|
868
|
+
</head>
|
|
869
|
+
<body>
|
|
870
|
+
<header class="app-header">
|
|
871
|
+
<div class="app-title">
|
|
872
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
873
|
+
<circle cx="12" cy="12" r="4"/>
|
|
874
|
+
<path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
|
|
875
|
+
</svg>
|
|
876
|
+
<span>git-ai</span> Commit Manager
|
|
877
|
+
</div>
|
|
878
|
+
<div class="header-actions">
|
|
879
|
+
<button class="btn btn-success" onclick="showCommitPanel()" id="commitPlanBtn" style="display: none;">
|
|
880
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
881
|
+
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
|
882
|
+
<path d="M9 5a2 2 0 012-2h2a2 2 0 012 2v0a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
883
|
+
<path d="M9 12h6M9 16h6"/>
|
|
884
|
+
</svg>
|
|
885
|
+
Commit Plan
|
|
886
|
+
</button>
|
|
887
|
+
<button class="btn btn-secondary" onclick="refreshFiles()">
|
|
888
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
889
|
+
<path d="M23 4v6h-6M1 20v-6h6"/>
|
|
890
|
+
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
|
891
|
+
</svg>
|
|
892
|
+
Refresh
|
|
893
|
+
</button>
|
|
894
|
+
<button class="btn btn-primary" onclick="analyzeSelected()" id="analyzeBtn" disabled>
|
|
895
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
896
|
+
<path d="M12 2a10 10 0 1 0 10 10H12V2z"/>
|
|
897
|
+
<path d="M12 2a10 10 0 0 1 10 10"/>
|
|
898
|
+
</svg>
|
|
899
|
+
Analyze with AI
|
|
900
|
+
</button>
|
|
901
|
+
</div>
|
|
902
|
+
</header>
|
|
903
|
+
|
|
904
|
+
<div class="container">
|
|
905
|
+
<aside class="sidebar">
|
|
906
|
+
<div class="sidebar-header">
|
|
907
|
+
<span class="sidebar-title">Source Control</span>
|
|
908
|
+
<span class="file-count" id="fileCount">0 files</span>
|
|
909
|
+
</div>
|
|
910
|
+
<div class="select-actions">
|
|
911
|
+
<button onclick="selectAll()">Select All</button>
|
|
912
|
+
<button onclick="selectNone()">Select None</button>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="file-sections">
|
|
915
|
+
<!-- Staged Changes Section -->
|
|
916
|
+
<div class="file-section" id="stagedSection" style="display: none;">
|
|
917
|
+
<div class="section-header" onclick="toggleSection('staged')">
|
|
918
|
+
<svg class="section-chevron" id="stagedChevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
919
|
+
<path d="M6 9l6 6 6-6"/>
|
|
920
|
+
</svg>
|
|
921
|
+
<span class="section-title">Staged Changes</span>
|
|
922
|
+
<span class="section-count" id="stagedCount">0</span>
|
|
923
|
+
<button class="section-action" onclick="event.stopPropagation(); unstageAllFiles()" title="Unstage All">
|
|
924
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>
|
|
925
|
+
</button>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="file-list" id="stagedList"></div>
|
|
928
|
+
</div>
|
|
929
|
+
<!-- Unstaged Changes Section -->
|
|
930
|
+
<div class="file-section" id="unstagedSection">
|
|
931
|
+
<div class="section-header" onclick="toggleSection('unstaged')">
|
|
932
|
+
<svg class="section-chevron" id="unstagedChevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
933
|
+
<path d="M6 9l6 6 6-6"/>
|
|
934
|
+
</svg>
|
|
935
|
+
<span class="section-title">Changes</span>
|
|
936
|
+
<span class="section-count" id="unstagedCount">0</span>
|
|
937
|
+
<button class="section-action" onclick="event.stopPropagation(); stageAllFiles()" title="Stage All">
|
|
938
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
|
939
|
+
</button>
|
|
940
|
+
</div>
|
|
941
|
+
<div class="file-list" id="unstagedList">
|
|
942
|
+
<div class="empty-state">
|
|
943
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
944
|
+
<path d="M5 13l4 4L19 7"/>
|
|
945
|
+
</svg>
|
|
946
|
+
<div>Working tree clean</div>
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
<div class="sidebar-footer">
|
|
952
|
+
<div class="sidebar-footer-brand">git-ai</div>
|
|
953
|
+
<a href="https://github.com/mehmetsagir/git-ai" target="_blank">
|
|
954
|
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
955
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
956
|
+
</svg>
|
|
957
|
+
Open source on GitHub
|
|
958
|
+
</a>
|
|
959
|
+
</div>
|
|
960
|
+
</aside>
|
|
961
|
+
|
|
962
|
+
<main class="main-panel">
|
|
963
|
+
<div class="diff-panel">
|
|
964
|
+
<div class="diff-header" id="diffHeader">
|
|
965
|
+
<span class="diff-filename" id="diffFilename">Select a file to view changes</span>
|
|
966
|
+
<div class="diff-view-toggle" id="diffViewToggle" style="display: none;">
|
|
967
|
+
<button class="active" onclick="setDiffView('unified')" id="btnUnified">
|
|
968
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
969
|
+
<path d="M4 6h16M4 12h16M4 18h16"/>
|
|
970
|
+
</svg>
|
|
971
|
+
Unified
|
|
972
|
+
</button>
|
|
973
|
+
<button onclick="setDiffView('split')" id="btnSplit">
|
|
974
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
975
|
+
<rect x="3" y="3" width="7" height="18" rx="1"/>
|
|
976
|
+
<rect x="14" y="3" width="7" height="18" rx="1"/>
|
|
977
|
+
</svg>
|
|
978
|
+
Split
|
|
979
|
+
</button>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
<div class="diff-content" id="diffContent">
|
|
983
|
+
<div class="empty-state">
|
|
984
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
985
|
+
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
986
|
+
</svg>
|
|
987
|
+
<div>Select a file to view diff</div>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
</main>
|
|
992
|
+
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<div class="toast" id="toast"></div>
|
|
996
|
+
<div class="loader-overlay" id="loader">
|
|
997
|
+
<div class="loader"></div>
|
|
998
|
+
<div class="loader-text" id="loaderText">Loading...</div>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<script>
|
|
1002
|
+
let files = [];
|
|
1003
|
+
let selectedFiles = new Set();
|
|
1004
|
+
let currentFile = null;
|
|
1005
|
+
let currentFileStaged = null;
|
|
1006
|
+
let currentDiff = null;
|
|
1007
|
+
let diffViewMode = 'unified';
|
|
1008
|
+
let commitGroups = null;
|
|
1009
|
+
let isLoading = false;
|
|
1010
|
+
let eventSource = null;
|
|
1011
|
+
let viewingCommitPlan = false;
|
|
1012
|
+
|
|
1013
|
+
// Initialize
|
|
1014
|
+
init();
|
|
1015
|
+
|
|
1016
|
+
function init() {
|
|
1017
|
+
// Set up event delegation once
|
|
1018
|
+
// Event delegation for both staged and unstaged lists
|
|
1019
|
+
const fileSections = document.querySelector('.file-sections');
|
|
1020
|
+
fileSections.addEventListener('click', function(e) {
|
|
1021
|
+
const item = e.target.closest('.file-item');
|
|
1022
|
+
if (!item) return;
|
|
1023
|
+
|
|
1024
|
+
const file = item.dataset.file;
|
|
1025
|
+
const staged = item.dataset.staged === 'true';
|
|
1026
|
+
const isCheckbox = e.target.closest('[data-checkbox]');
|
|
1027
|
+
const isStageBtn = e.target.closest('[data-stage-action]');
|
|
1028
|
+
|
|
1029
|
+
if (isStageBtn) {
|
|
1030
|
+
const action = isStageBtn.dataset.stageAction;
|
|
1031
|
+
if (action === 'stage') {
|
|
1032
|
+
stageFile(file);
|
|
1033
|
+
} else {
|
|
1034
|
+
unstageFile(file);
|
|
1035
|
+
}
|
|
1036
|
+
} else if (isCheckbox) {
|
|
1037
|
+
toggleFile(file, staged);
|
|
1038
|
+
} else {
|
|
1039
|
+
viewFile(file, staged);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Initial load
|
|
1044
|
+
refreshFiles();
|
|
1045
|
+
|
|
1046
|
+
// Connect to SSE for real-time updates
|
|
1047
|
+
connectSSE();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function connectSSE() {
|
|
1051
|
+
if (eventSource) {
|
|
1052
|
+
eventSource.close();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
eventSource = new EventSource('/api/events');
|
|
1056
|
+
|
|
1057
|
+
eventSource.onmessage = function(event) {
|
|
1058
|
+
if (event.data === 'refresh') {
|
|
1059
|
+
handleFileChange();
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
eventSource.onerror = function() {
|
|
1064
|
+
// Reconnect after 3 seconds
|
|
1065
|
+
setTimeout(connectSSE, 3000);
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function handleFileChange() {
|
|
1070
|
+
if (isLoading) return;
|
|
1071
|
+
try {
|
|
1072
|
+
const res = await fetch('/api/files?t=' + Date.now(), { cache: 'no-store' });
|
|
1073
|
+
if (!res.ok) return;
|
|
1074
|
+
const newFiles = await res.json();
|
|
1075
|
+
|
|
1076
|
+
// Check if files changed (include staged status)
|
|
1077
|
+
const oldFileSet = new Set(files.map(f => f.file + ':' + f.status + ':' + f.staged));
|
|
1078
|
+
const newFileSet = new Set(newFiles.map(f => f.file + ':' + f.status + ':' + f.staged));
|
|
1079
|
+
|
|
1080
|
+
const hasFileListChanges = oldFileSet.size !== newFileSet.size ||
|
|
1081
|
+
[...oldFileSet].some(f => !newFileSet.has(f)) ||
|
|
1082
|
+
[...newFileSet].some(f => !oldFileSet.has(f));
|
|
1083
|
+
|
|
1084
|
+
if (hasFileListChanges) {
|
|
1085
|
+
files = newFiles;
|
|
1086
|
+
// Clean up selectedFiles - remove files that no longer exist
|
|
1087
|
+
const existingFiles = new Set(files.map(f => f.file));
|
|
1088
|
+
selectedFiles = new Set([...selectedFiles].filter(f => existingFiles.has(f)));
|
|
1089
|
+
|
|
1090
|
+
renderFileList();
|
|
1091
|
+
updateAnalyzeButton();
|
|
1092
|
+
|
|
1093
|
+
// Update diff if current file no longer exists with same staged status
|
|
1094
|
+
const currentFileExists = files.some(f => f.file === currentFile && f.staged === currentFileStaged);
|
|
1095
|
+
if (currentFile && !currentFileExists) {
|
|
1096
|
+
currentFile = null;
|
|
1097
|
+
currentFileStaged = null;
|
|
1098
|
+
currentDiff = null;
|
|
1099
|
+
document.getElementById('diffFilename').textContent = 'Select a file to view changes';
|
|
1100
|
+
document.getElementById('diffViewToggle').style.display = 'none';
|
|
1101
|
+
document.getElementById('diffContent').innerHTML = \`
|
|
1102
|
+
<div class="empty-state">
|
|
1103
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1104
|
+
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
1105
|
+
</svg>
|
|
1106
|
+
<div>Select a file to view diff</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
\`;
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
} else {
|
|
1112
|
+
files = newFiles;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Refresh current file's diff
|
|
1116
|
+
if (currentFile) {
|
|
1117
|
+
const fileInfo = files.find(f => f.file === currentFile && f.staged === currentFileStaged);
|
|
1118
|
+
if (fileInfo) {
|
|
1119
|
+
const diffRes = await fetch('/api/diff?file=' + encodeURIComponent(currentFile) + '&status=' + encodeURIComponent(fileInfo.status) + '&staged=' + currentFileStaged + '&t=' + Date.now(), { cache: 'no-store' });
|
|
1120
|
+
if (diffRes.ok) {
|
|
1121
|
+
const newDiff = await diffRes.text();
|
|
1122
|
+
if (newDiff !== currentDiff) {
|
|
1123
|
+
currentDiff = newDiff;
|
|
1124
|
+
document.getElementById('diffViewToggle').style.display = currentDiff ? 'flex' : 'none';
|
|
1125
|
+
renderDiff(currentDiff);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
// Silently ignore errors
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function refreshFiles() {
|
|
1136
|
+
showLoader('Loading changes...');
|
|
1137
|
+
try {
|
|
1138
|
+
const res = await fetch('/api/files?t=' + Date.now(), { cache: 'no-store' });
|
|
1139
|
+
if (!res.ok) {
|
|
1140
|
+
throw new Error('HTTP ' + res.status);
|
|
1141
|
+
}
|
|
1142
|
+
files = await res.json();
|
|
1143
|
+
renderFileList();
|
|
1144
|
+
updateAnalyzeButton();
|
|
1145
|
+
if (files.length === 0) {
|
|
1146
|
+
document.getElementById('diffFilename').textContent = 'No changes detected';
|
|
1147
|
+
document.getElementById('diffViewToggle').style.display = 'none';
|
|
1148
|
+
document.getElementById('diffContent').innerHTML = \`
|
|
1149
|
+
<div class="empty-state">
|
|
1150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1151
|
+
<path d="M5 13l4 4L19 7"/>
|
|
1152
|
+
</svg>
|
|
1153
|
+
<div>Working tree clean</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
\`;
|
|
1156
|
+
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
console.error('Failed to load files:', err);
|
|
1159
|
+
showToast('Failed to load files: ' + err.message, 'error');
|
|
1160
|
+
}
|
|
1161
|
+
hideLoader();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function renderFileList() {
|
|
1165
|
+
const stagedFiles = files.filter(f => f.staged);
|
|
1166
|
+
const unstagedFiles = files.filter(f => !f.staged);
|
|
1167
|
+
const totalFiles = new Set(files.map(f => f.file)).size;
|
|
1168
|
+
|
|
1169
|
+
document.getElementById('fileCount').textContent = totalFiles + ' file' + (totalFiles !== 1 ? 's' : '');
|
|
1170
|
+
|
|
1171
|
+
// Staged section
|
|
1172
|
+
const stagedSection = document.getElementById('stagedSection');
|
|
1173
|
+
const stagedList = document.getElementById('stagedList');
|
|
1174
|
+
const stagedCount = document.getElementById('stagedCount');
|
|
1175
|
+
|
|
1176
|
+
if (stagedFiles.length > 0) {
|
|
1177
|
+
stagedSection.style.display = 'block';
|
|
1178
|
+
stagedCount.textContent = stagedFiles.length;
|
|
1179
|
+
stagedList.innerHTML = stagedFiles.map(f => renderFileItem(f, true)).join('');
|
|
1180
|
+
} else {
|
|
1181
|
+
stagedSection.style.display = 'none';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Unstaged section
|
|
1185
|
+
const unstagedList = document.getElementById('unstagedList');
|
|
1186
|
+
const unstagedCount = document.getElementById('unstagedCount');
|
|
1187
|
+
unstagedCount.textContent = unstagedFiles.length;
|
|
1188
|
+
|
|
1189
|
+
if (unstagedFiles.length === 0 && stagedFiles.length === 0) {
|
|
1190
|
+
unstagedList.innerHTML = \`
|
|
1191
|
+
<div class="empty-state">
|
|
1192
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1193
|
+
<path d="M5 13l4 4L19 7"/>
|
|
1194
|
+
</svg>
|
|
1195
|
+
<div>Working tree clean</div>
|
|
1196
|
+
</div>
|
|
1197
|
+
\`;
|
|
1198
|
+
} else if (unstagedFiles.length === 0) {
|
|
1199
|
+
unstagedList.innerHTML = \`
|
|
1200
|
+
<div class="empty-state" style="padding: 16px;">
|
|
1201
|
+
<div style="font-size: 12px;">No unstaged changes</div>
|
|
1202
|
+
</div>
|
|
1203
|
+
\`;
|
|
1204
|
+
} else {
|
|
1205
|
+
unstagedList.innerHTML = unstagedFiles.map(f => renderFileItem(f, false)).join('');
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function renderFileItem(f, isStaged) {
|
|
1210
|
+
const statusLabel = { new: 'A', modified: 'M', deleted: 'D', renamed: 'R' }[f.status] || 'M';
|
|
1211
|
+
const fileName = f.file.split('/').pop();
|
|
1212
|
+
const filePath = f.file.includes('/') ? f.file.substring(0, f.file.lastIndexOf('/')) : '';
|
|
1213
|
+
const fileKey = f.file + ':' + (isStaged ? 'staged' : 'unstaged');
|
|
1214
|
+
const isSelected = selectedFiles.has(fileKey);
|
|
1215
|
+
const isActive = currentFile === f.file && currentFileStaged === isStaged;
|
|
1216
|
+
const checkIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 13l4 4L19 7"/></svg>';
|
|
1217
|
+
const stageIcon = isStaged
|
|
1218
|
+
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>'
|
|
1219
|
+
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>';
|
|
1220
|
+
|
|
1221
|
+
return \`
|
|
1222
|
+
<div class="file-item \${isActive ? 'selected' : ''}" data-file="\${escapeHtml(f.file)}" data-status="\${f.status}" data-staged="\${isStaged}">
|
|
1223
|
+
<div class="custom-checkbox \${isSelected ? 'checked' : ''}" data-checkbox="true">\${checkIcon}</div>
|
|
1224
|
+
<div class="file-status \${f.status}">\${statusLabel}</div>
|
|
1225
|
+
<div class="file-info">
|
|
1226
|
+
<div class="file-name">\${escapeHtml(fileName)}</div>
|
|
1227
|
+
\${filePath ? \`<div class="file-path">\${escapeHtml(filePath)}</div>\` : ''}
|
|
1228
|
+
</div>
|
|
1229
|
+
<button class="stage-btn" data-stage-action="\${isStaged ? 'unstage' : 'stage'}" title="\${isStaged ? 'Unstage' : 'Stage'}">\${stageIcon}</button>
|
|
1230
|
+
</div>
|
|
1231
|
+
\`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function toggleFile(file, staged) {
|
|
1235
|
+
const fileKey = file + ':' + (staged ? 'staged' : 'unstaged');
|
|
1236
|
+
if (selectedFiles.has(fileKey)) {
|
|
1237
|
+
selectedFiles.delete(fileKey);
|
|
1238
|
+
} else {
|
|
1239
|
+
selectedFiles.add(fileKey);
|
|
1240
|
+
}
|
|
1241
|
+
renderFileList();
|
|
1242
|
+
updateAnalyzeButton();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function stageFile(file) {
|
|
1246
|
+
try {
|
|
1247
|
+
await fetch('/api/stage', {
|
|
1248
|
+
method: 'POST',
|
|
1249
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1250
|
+
body: JSON.stringify({ file })
|
|
1251
|
+
});
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
showToast('Failed to stage file', 'error');
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function unstageFile(file) {
|
|
1258
|
+
try {
|
|
1259
|
+
await fetch('/api/unstage', {
|
|
1260
|
+
method: 'POST',
|
|
1261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1262
|
+
body: JSON.stringify({ file })
|
|
1263
|
+
});
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
showToast('Failed to unstage file', 'error');
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
async function stageAllFiles() {
|
|
1270
|
+
const unstagedFiles = files.filter(f => !f.staged);
|
|
1271
|
+
for (const f of unstagedFiles) {
|
|
1272
|
+
await stageFile(f.file);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function unstageAllFiles() {
|
|
1277
|
+
const stagedFiles = files.filter(f => f.staged);
|
|
1278
|
+
for (const f of stagedFiles) {
|
|
1279
|
+
await unstageFile(f.file);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function toggleSection(section) {
|
|
1284
|
+
const chevron = document.getElementById(section + 'Chevron');
|
|
1285
|
+
const list = document.getElementById(section + 'List');
|
|
1286
|
+
chevron.classList.toggle('collapsed');
|
|
1287
|
+
list.classList.toggle('collapsed');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function selectAll() {
|
|
1291
|
+
files.forEach(f => {
|
|
1292
|
+
const fileKey = f.file + ':' + (f.staged ? 'staged' : 'unstaged');
|
|
1293
|
+
selectedFiles.add(fileKey);
|
|
1294
|
+
});
|
|
1295
|
+
renderFileList();
|
|
1296
|
+
updateAnalyzeButton();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function selectNone() {
|
|
1300
|
+
selectedFiles.clear();
|
|
1301
|
+
renderFileList();
|
|
1302
|
+
updateAnalyzeButton();
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function updateAnalyzeButton() {
|
|
1306
|
+
const btn = document.getElementById('analyzeBtn');
|
|
1307
|
+
btn.disabled = selectedFiles.size === 0;
|
|
1308
|
+
// Count unique files (not file:staged keys)
|
|
1309
|
+
const uniqueFiles = new Set([...selectedFiles].map(k => k.split(':')[0]));
|
|
1310
|
+
btn.textContent = selectedFiles.size > 0
|
|
1311
|
+
? \`Analyze \${selectedFiles.size} file\${selectedFiles.size > 1 ? 's' : ''}\`
|
|
1312
|
+
: 'Analyze with AI';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function viewFile(file, staged) {
|
|
1316
|
+
currentFile = file;
|
|
1317
|
+
currentFileStaged = staged;
|
|
1318
|
+
viewingCommitPlan = false;
|
|
1319
|
+
renderFileList();
|
|
1320
|
+
|
|
1321
|
+
// Show commit plan button if we have a plan
|
|
1322
|
+
if (commitGroups && commitGroups.length > 0) {
|
|
1323
|
+
document.getElementById('commitPlanBtn').style.display = 'inline-flex';
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const label = staged ? ' (staged)' : '';
|
|
1327
|
+
document.getElementById('diffFilename').textContent = file + label;
|
|
1328
|
+
document.getElementById('diffContent').innerHTML = '<div class="empty-state"><div class="loader"></div></div>';
|
|
1329
|
+
|
|
1330
|
+
// Find file status
|
|
1331
|
+
const fileInfo = files.find(f => f.file === file && f.staged === staged);
|
|
1332
|
+
const status = fileInfo ? fileInfo.status : '';
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
const res = await fetch('/api/diff?file=' + encodeURIComponent(file) + '&status=' + encodeURIComponent(status) + '&staged=' + staged + '&t=' + Date.now(), { cache: 'no-store' });
|
|
1336
|
+
currentDiff = await res.text();
|
|
1337
|
+
document.getElementById('diffViewToggle').style.display = currentDiff ? 'flex' : 'none';
|
|
1338
|
+
renderDiff(currentDiff);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
document.getElementById('diffContent').innerHTML = '<div class="empty-state"><div>Failed to load diff</div></div>';
|
|
1341
|
+
document.getElementById('diffViewToggle').style.display = 'none';
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function setDiffView(mode) {
|
|
1346
|
+
diffViewMode = mode;
|
|
1347
|
+
document.getElementById('btnUnified').classList.toggle('active', mode === 'unified');
|
|
1348
|
+
document.getElementById('btnSplit').classList.toggle('active', mode === 'split');
|
|
1349
|
+
if (currentDiff) {
|
|
1350
|
+
renderDiff(currentDiff);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function renderDiff(diff) {
|
|
1355
|
+
const container = document.getElementById('diffContent');
|
|
1356
|
+
|
|
1357
|
+
if (!diff) {
|
|
1358
|
+
container.innerHTML = '<div class="empty-state"><div>No diff available</div></div>';
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (diffViewMode === 'split') {
|
|
1363
|
+
renderSplitDiff(diff, container);
|
|
1364
|
+
} else {
|
|
1365
|
+
renderUnifiedDiff(diff, container);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function renderUnifiedDiff(diff, container) {
|
|
1370
|
+
const lines = diff.split('\\n');
|
|
1371
|
+
const isNewFile = diff.includes('new file mode') || diff.includes('--- /dev/null');
|
|
1372
|
+
let html = '<div class="diff-unified">';
|
|
1373
|
+
let lineNum = 0;
|
|
1374
|
+
|
|
1375
|
+
// For new files, show plain content
|
|
1376
|
+
if (isNewFile) {
|
|
1377
|
+
let inContent = false;
|
|
1378
|
+
for (const line of lines) {
|
|
1379
|
+
if (line.startsWith('@@')) {
|
|
1380
|
+
inContent = true;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (!inContent) continue;
|
|
1384
|
+
if (line.startsWith('+')) {
|
|
1385
|
+
lineNum++;
|
|
1386
|
+
const content = line.substring(1);
|
|
1387
|
+
html += \`<div class="diff-line add"><span class="line-num">\${lineNum}</span>\${escapeHtml(content)}</div>\`;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
} else {
|
|
1391
|
+
// Regular diff view - skip header lines and hunk headers
|
|
1392
|
+
for (const line of lines) {
|
|
1393
|
+
if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
1394
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
1395
|
+
line.startsWith('new file') || line.startsWith('deleted file')) {
|
|
1396
|
+
continue; // Skip header lines
|
|
1397
|
+
} else if (line.startsWith('@@')) {
|
|
1398
|
+
// Parse line number but don't display hunk header
|
|
1399
|
+
const match = line.match(/@@ -(\\d+)/);
|
|
1400
|
+
if (match) lineNum = parseInt(match[1]) - 1;
|
|
1401
|
+
} else if (line.startsWith('-')) {
|
|
1402
|
+
lineNum++;
|
|
1403
|
+
html += \`<div class="diff-line del"><span class="line-num">\${lineNum}</span>\${escapeHtml(line)}</div>\`;
|
|
1404
|
+
} else if (line.startsWith('+')) {
|
|
1405
|
+
html += \`<div class="diff-line add"><span class="line-num"></span>\${escapeHtml(line)}</div>\`;
|
|
1406
|
+
} else {
|
|
1407
|
+
lineNum++;
|
|
1408
|
+
html += \`<div class="diff-line"><span class="line-num">\${lineNum}</span>\${escapeHtml(line)}</div>\`;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
html += '</div>';
|
|
1414
|
+
container.innerHTML = html;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function renderSplitDiff(diff, container) {
|
|
1418
|
+
const lines = diff.split('\\n');
|
|
1419
|
+
const isNewFile = diff.includes('new file mode') || diff.includes('--- /dev/null');
|
|
1420
|
+
|
|
1421
|
+
// For new files, show single pane with content
|
|
1422
|
+
if (isNewFile) {
|
|
1423
|
+
let rightLines = [];
|
|
1424
|
+
let lineNum = 0;
|
|
1425
|
+
let inContent = false;
|
|
1426
|
+
|
|
1427
|
+
for (const line of lines) {
|
|
1428
|
+
if (line.startsWith('@@')) {
|
|
1429
|
+
inContent = true;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (!inContent) continue;
|
|
1433
|
+
if (line.startsWith('+')) {
|
|
1434
|
+
lineNum++;
|
|
1435
|
+
rightLines.push({ type: 'add', content: line.substring(1), num: lineNum });
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const renderPane = (lines, title) => {
|
|
1440
|
+
let html = \`<div class="diff-split-pane-header">\${title}</div><div class="diff-split-pane-content">\`;
|
|
1441
|
+
for (const line of lines) {
|
|
1442
|
+
const numHtml = \`<span class="line-num">\${line.num}</span>\`;
|
|
1443
|
+
html += \`<div class="diff-line \${line.type}">\${numHtml}\${escapeHtml(line.content)}</div>\`;
|
|
1444
|
+
}
|
|
1445
|
+
html += '</div>';
|
|
1446
|
+
return html;
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
container.innerHTML = \`
|
|
1450
|
+
<div class="diff-split">
|
|
1451
|
+
<div class="diff-split-pane" style="flex: 1;">\${renderPane(rightLines, 'New File')}</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
\`;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
let leftLines = [];
|
|
1458
|
+
let rightLines = [];
|
|
1459
|
+
let leftLineNum = 0;
|
|
1460
|
+
let rightLineNum = 0;
|
|
1461
|
+
let inHunk = false;
|
|
1462
|
+
|
|
1463
|
+
for (const line of lines) {
|
|
1464
|
+
if (line.startsWith('@@')) {
|
|
1465
|
+
// Parse line numbers but don't display hunk header
|
|
1466
|
+
const match = line.match(/@@ -(\\d+)(?:,\\d+)? \\+(\\d+)/);
|
|
1467
|
+
if (match) {
|
|
1468
|
+
leftLineNum = parseInt(match[1]) - 1;
|
|
1469
|
+
rightLineNum = parseInt(match[2]) - 1;
|
|
1470
|
+
}
|
|
1471
|
+
inHunk = true;
|
|
1472
|
+
} else if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
1473
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
1474
|
+
line.startsWith('new file') || line.startsWith('deleted file')) {
|
|
1475
|
+
// Skip header lines in split view
|
|
1476
|
+
} else if (line.startsWith('-')) {
|
|
1477
|
+
leftLineNum++;
|
|
1478
|
+
leftLines.push({ type: 'del', content: line.substring(1), num: leftLineNum });
|
|
1479
|
+
rightLines.push({ type: 'empty', content: '', num: '' });
|
|
1480
|
+
} else if (line.startsWith('+')) {
|
|
1481
|
+
rightLineNum++;
|
|
1482
|
+
leftLines.push({ type: 'empty', content: '', num: '' });
|
|
1483
|
+
rightLines.push({ type: 'add', content: line.substring(1), num: rightLineNum });
|
|
1484
|
+
} else if (inHunk) {
|
|
1485
|
+
leftLineNum++;
|
|
1486
|
+
rightLineNum++;
|
|
1487
|
+
leftLines.push({ type: 'context', content: line.substring(1) || line, num: leftLineNum });
|
|
1488
|
+
rightLines.push({ type: 'context', content: line.substring(1) || line, num: rightLineNum });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Pair up consecutive del/add as modifications
|
|
1493
|
+
const pairedLeft = [];
|
|
1494
|
+
const pairedRight = [];
|
|
1495
|
+
let i = 0;
|
|
1496
|
+
while (i < leftLines.length) {
|
|
1497
|
+
// Collect consecutive dels
|
|
1498
|
+
const dels = [];
|
|
1499
|
+
while (i < leftLines.length && leftLines[i].type === 'del' && rightLines[i].type === 'empty') {
|
|
1500
|
+
dels.push(leftLines[i]);
|
|
1501
|
+
i++;
|
|
1502
|
+
}
|
|
1503
|
+
// Collect consecutive adds
|
|
1504
|
+
const adds = [];
|
|
1505
|
+
while (i < leftLines.length && leftLines[i].type === 'empty' && rightLines[i].type === 'add') {
|
|
1506
|
+
adds.push(rightLines[i]);
|
|
1507
|
+
i++;
|
|
1508
|
+
}
|
|
1509
|
+
// Pair them up
|
|
1510
|
+
const maxLen = Math.max(dels.length, adds.length);
|
|
1511
|
+
for (let j = 0; j < maxLen; j++) {
|
|
1512
|
+
pairedLeft.push(dels[j] || { type: 'empty', content: '', num: '' });
|
|
1513
|
+
pairedRight.push(adds[j] || { type: 'empty', content: '', num: '' });
|
|
1514
|
+
}
|
|
1515
|
+
// If no dels or adds, just push the current line
|
|
1516
|
+
if (dels.length === 0 && adds.length === 0 && i < leftLines.length) {
|
|
1517
|
+
pairedLeft.push(leftLines[i]);
|
|
1518
|
+
pairedRight.push(rightLines[i]);
|
|
1519
|
+
i++;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const renderPane = (lines, title) => {
|
|
1524
|
+
let html = \`<div class="diff-split-pane-header">\${title}</div><div class="diff-split-pane-content">\`;
|
|
1525
|
+
for (const line of lines) {
|
|
1526
|
+
const numHtml = line.num ? \`<span class="line-num">\${line.num}</span>\` : '<span class="line-num"></span>';
|
|
1527
|
+
html += \`<div class="diff-line \${line.type}">\${numHtml}\${escapeHtml(line.content)}</div>\`;
|
|
1528
|
+
}
|
|
1529
|
+
html += '</div>';
|
|
1530
|
+
return html;
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
container.innerHTML = \`
|
|
1534
|
+
<div class="diff-split">
|
|
1535
|
+
<div class="diff-split-pane">\${renderPane(pairedLeft, 'Original')}</div>
|
|
1536
|
+
<div class="diff-split-pane">\${renderPane(pairedRight, 'Modified')}</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
\`;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
async function analyzeSelected() {
|
|
1542
|
+
if (selectedFiles.size === 0) return;
|
|
1543
|
+
if (isLoading) return;
|
|
1544
|
+
|
|
1545
|
+
isLoading = true;
|
|
1546
|
+
showLoader('Analyzing changes with AI...');
|
|
1547
|
+
|
|
1548
|
+
// Extract unique file names from selected file keys (format: "file:staged" or "file:unstaged")
|
|
1549
|
+
const fileNames = [...new Set([...selectedFiles].map(key => {
|
|
1550
|
+
const parts = key.split(':');
|
|
1551
|
+
parts.pop(); // Remove "staged" or "unstaged"
|
|
1552
|
+
return parts.join(':'); // Rejoin in case filename has colons
|
|
1553
|
+
}))];
|
|
1554
|
+
|
|
1555
|
+
try {
|
|
1556
|
+
const res = await fetch('/api/analyze', {
|
|
1557
|
+
method: 'POST',
|
|
1558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1559
|
+
body: JSON.stringify({ files: fileNames })
|
|
1560
|
+
});
|
|
1561
|
+
const data = await res.json();
|
|
1562
|
+
|
|
1563
|
+
if (data.error) {
|
|
1564
|
+
showToast(data.error, 'error');
|
|
1565
|
+
} else {
|
|
1566
|
+
commitGroups = data.groups;
|
|
1567
|
+
showCommitPanel();
|
|
1568
|
+
}
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
console.error('Analyze error:', err);
|
|
1571
|
+
showToast('Failed to analyze: ' + (err.message || err), 'error');
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
hideLoader();
|
|
1575
|
+
isLoading = false;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Store parsed diffs for hunk extraction
|
|
1579
|
+
let parsedDiffs = {};
|
|
1580
|
+
|
|
1581
|
+
// Parse diff into hunks
|
|
1582
|
+
function parseDiffIntoHunks(diff) {
|
|
1583
|
+
const hunks = [];
|
|
1584
|
+
const lines = diff.split('\\n');
|
|
1585
|
+
let currentHunk = null;
|
|
1586
|
+
let hunkIndex = -1;
|
|
1587
|
+
|
|
1588
|
+
for (const line of lines) {
|
|
1589
|
+
if (line.startsWith('diff --git') || line.startsWith('index ') ||
|
|
1590
|
+
line.startsWith('---') || line.startsWith('+++') ||
|
|
1591
|
+
line.startsWith('new file') || line.startsWith('deleted file')) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
if (line.startsWith('@@')) {
|
|
1596
|
+
// Start new hunk
|
|
1597
|
+
if (currentHunk) {
|
|
1598
|
+
hunks.push(currentHunk);
|
|
1599
|
+
}
|
|
1600
|
+
hunkIndex++;
|
|
1601
|
+
const match = line.match(/@@ -(\\d+)/);
|
|
1602
|
+
currentHunk = {
|
|
1603
|
+
index: hunkIndex,
|
|
1604
|
+
startLine: match ? parseInt(match[1]) : 1,
|
|
1605
|
+
lines: []
|
|
1606
|
+
};
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (currentHunk) {
|
|
1611
|
+
currentHunk.lines.push(line);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (currentHunk) {
|
|
1616
|
+
hunks.push(currentHunk);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
return hunks;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Render specific hunks
|
|
1623
|
+
function renderHunks(hunks, hunkIndices) {
|
|
1624
|
+
let html = '';
|
|
1625
|
+
let selectedHunks = hunks;
|
|
1626
|
+
|
|
1627
|
+
// If hunkIndices specified, filter to those hunks
|
|
1628
|
+
if (hunkIndices && hunkIndices.length > 0) {
|
|
1629
|
+
const filtered = hunks.filter(h => hunkIndices.includes(h.index));
|
|
1630
|
+
// Only use filtered if we found matches, otherwise show all
|
|
1631
|
+
if (filtered.length > 0) {
|
|
1632
|
+
selectedHunks = filtered;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
for (const hunk of selectedHunks) {
|
|
1637
|
+
let lineNum = hunk.startLine - 1;
|
|
1638
|
+
for (const line of hunk.lines) {
|
|
1639
|
+
if (line.startsWith('-')) {
|
|
1640
|
+
lineNum++;
|
|
1641
|
+
html += '<div class="diff-line del"><span class="diff-line-num">' + lineNum + '</span>' + escapeHtml(line) + '</div>';
|
|
1642
|
+
} else if (line.startsWith('+')) {
|
|
1643
|
+
html += '<div class="diff-line add"><span class="diff-line-num"></span>' + escapeHtml(line) + '</div>';
|
|
1644
|
+
} else {
|
|
1645
|
+
lineNum++;
|
|
1646
|
+
html += '<div class="diff-line"><span class="diff-line-num">' + lineNum + '</span>' + escapeHtml(line) + '</div>';
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return html;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Toggle file diff visibility
|
|
1655
|
+
function toggleFileDiff(elementId) {
|
|
1656
|
+
const diffEl = document.getElementById(elementId);
|
|
1657
|
+
const chevron = diffEl.previousElementSibling.querySelector('.group-file-chevron');
|
|
1658
|
+
diffEl.classList.toggle('collapsed');
|
|
1659
|
+
chevron.classList.toggle('collapsed');
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Toggle commit group visibility
|
|
1663
|
+
function toggleCommitGroup(groupId) {
|
|
1664
|
+
const content = document.getElementById(groupId);
|
|
1665
|
+
const header = content.previousElementSibling;
|
|
1666
|
+
const chevron = header.querySelector('.group-chevron');
|
|
1667
|
+
content.classList.toggle('collapsed');
|
|
1668
|
+
chevron.classList.toggle('collapsed');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
async function showCommitPanel() {
|
|
1672
|
+
if (!commitGroups || commitGroups.length === 0) return;
|
|
1673
|
+
|
|
1674
|
+
const diffContent = document.getElementById('diffContent');
|
|
1675
|
+
const diffViewToggle = document.getElementById('diffViewToggle');
|
|
1676
|
+
|
|
1677
|
+
// We're now viewing the commit plan
|
|
1678
|
+
viewingCommitPlan = true;
|
|
1679
|
+
|
|
1680
|
+
// Hide commit plan button (we're already viewing it)
|
|
1681
|
+
document.getElementById('commitPlanBtn').style.display = 'none';
|
|
1682
|
+
|
|
1683
|
+
// Hide diff view toggle
|
|
1684
|
+
diffViewToggle.style.display = 'none';
|
|
1685
|
+
|
|
1686
|
+
// Update header
|
|
1687
|
+
document.getElementById('diffFilename').textContent = 'Commit Plan';
|
|
1688
|
+
|
|
1689
|
+
// Icons
|
|
1690
|
+
const fileIcon = '<svg class="group-file-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg>';
|
|
1691
|
+
const chevronIcon = '<svg class="group-file-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>';
|
|
1692
|
+
|
|
1693
|
+
let html = '<div class="commit-plan-view">';
|
|
1694
|
+
html += '<div class="commit-plan-header">';
|
|
1695
|
+
html += '<span class="commit-plan-title">' + commitGroups.length + ' commit(s) will be created</span>';
|
|
1696
|
+
html += '<div class="commit-plan-actions">';
|
|
1697
|
+
html += '<button class="btn btn-secondary" onclick="hideCommitPanel()">Cancel</button>';
|
|
1698
|
+
html += '<button class="btn btn-success" onclick="executeCommits()">';
|
|
1699
|
+
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 13l4 4L19 7"/></svg>';
|
|
1700
|
+
html += 'Create Commits</button>';
|
|
1701
|
+
html += '</div></div>';
|
|
1702
|
+
|
|
1703
|
+
// First, load all diffs and parse them
|
|
1704
|
+
const allFiles = new Set();
|
|
1705
|
+
for (const g of commitGroups) {
|
|
1706
|
+
const groupFiles = g.files || [...new Set((g.hunks || []).map(h => h.file))];
|
|
1707
|
+
groupFiles.forEach(f => allFiles.add(f));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Load and parse all diffs
|
|
1711
|
+
parsedDiffs = {};
|
|
1712
|
+
for (const file of allFiles) {
|
|
1713
|
+
const fileInfo = files.find(f => f.file === file);
|
|
1714
|
+
const status = fileInfo ? fileInfo.status : 'modified';
|
|
1715
|
+
try {
|
|
1716
|
+
const res = await fetch('/api/diff?file=' + encodeURIComponent(file) + '&status=' + status + '&t=' + Date.now());
|
|
1717
|
+
const diff = await res.text();
|
|
1718
|
+
if (diff) {
|
|
1719
|
+
parsedDiffs[file] = parseDiffIntoHunks(diff);
|
|
1720
|
+
}
|
|
1721
|
+
} catch (e) {
|
|
1722
|
+
parsedDiffs[file] = [];
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Build each commit group with file diffs
|
|
1727
|
+
const groupChevron = '<svg class="group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>';
|
|
1728
|
+
|
|
1729
|
+
for (let i = 0; i < commitGroups.length; i++) {
|
|
1730
|
+
const g = commitGroups[i];
|
|
1731
|
+
const groupHunks = g.hunks || [];
|
|
1732
|
+
const groupFiles = g.files || [...new Set(groupHunks.map(h => h.file))];
|
|
1733
|
+
const groupContentId = 'groupContent_' + i;
|
|
1734
|
+
|
|
1735
|
+
html += '<div class="commit-group">';
|
|
1736
|
+
html += '<div class="group-header" onclick="toggleCommitGroup(\\'' + groupContentId + '\\')">';
|
|
1737
|
+
html += groupChevron;
|
|
1738
|
+
html += '<span class="group-number">' + (i + 1) + '</span>';
|
|
1739
|
+
html += '<span class="group-message">' + escapeHtml(g.commitMessage) + '</span>';
|
|
1740
|
+
html += '<span style="color: #6e7681; margin-left: auto; font-size: 11px;">' + groupFiles.length + ' file(s)</span>';
|
|
1741
|
+
html += '</div>';
|
|
1742
|
+
|
|
1743
|
+
html += '<div class="group-content" id="' + groupContentId + '">';
|
|
1744
|
+
|
|
1745
|
+
// Show diff for each file with only the relevant hunks
|
|
1746
|
+
for (const file of groupFiles) {
|
|
1747
|
+
const diffId = 'commitDiff_' + i + '_' + file.replace(/[^a-zA-Z0-9]/g, '_');
|
|
1748
|
+
|
|
1749
|
+
// Get hunk indices for this file in this group
|
|
1750
|
+
const fileHunks = groupHunks.filter(h => h.file === file);
|
|
1751
|
+
const hunkIndices = fileHunks.length > 0 ? fileHunks.map(h => h.hunkIndex) : null;
|
|
1752
|
+
|
|
1753
|
+
// Render only the relevant hunks
|
|
1754
|
+
const fileDiffHunks = parsedDiffs[file] || [];
|
|
1755
|
+
const diffHtml = renderHunks(fileDiffHunks, hunkIndices);
|
|
1756
|
+
|
|
1757
|
+
html += '<div class="group-file-section">';
|
|
1758
|
+
html += '<div class="group-file-header" onclick="event.stopPropagation(); toggleFileDiff(\\'' + diffId + '\\')">';
|
|
1759
|
+
html += chevronIcon + fileIcon + escapeHtml(file);
|
|
1760
|
+
if (hunkIndices) {
|
|
1761
|
+
html += '<span style="color: #6e7681; margin-left: auto; font-size: 11px;">' + hunkIndices.length + ' hunk(s)</span>';
|
|
1762
|
+
}
|
|
1763
|
+
html += '</div>';
|
|
1764
|
+
html += '<div class="group-file-diff" id="' + diffId + '">';
|
|
1765
|
+
html += diffHtml || '<div style="padding: 8px 16px; color: #6e7681;">No changes</div>';
|
|
1766
|
+
html += '</div>';
|
|
1767
|
+
html += '</div>';
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
html += '</div>'; // group-content
|
|
1771
|
+
html += '</div>'; // commit-group
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
html += '</div>';
|
|
1775
|
+
diffContent.innerHTML = html;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function hideCommitPanel() {
|
|
1779
|
+
commitGroups = null;
|
|
1780
|
+
currentFile = null;
|
|
1781
|
+
currentFileStaged = null;
|
|
1782
|
+
currentDiff = null;
|
|
1783
|
+
viewingCommitPlan = false;
|
|
1784
|
+
|
|
1785
|
+
// Hide commit plan button
|
|
1786
|
+
document.getElementById('commitPlanBtn').style.display = 'none';
|
|
1787
|
+
|
|
1788
|
+
document.getElementById('diffFilename').textContent = 'Select a file to view changes';
|
|
1789
|
+
document.getElementById('diffViewToggle').style.display = 'none';
|
|
1790
|
+
document.getElementById('diffContent').innerHTML = \`
|
|
1791
|
+
<div class="empty-state">
|
|
1792
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1793
|
+
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
1794
|
+
</svg>
|
|
1795
|
+
<div>Select a file to view diff</div>
|
|
1796
|
+
</div>
|
|
1797
|
+
\`;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
async function executeCommits() {
|
|
1801
|
+
if (!commitGroups || commitGroups.length === 0) return;
|
|
1802
|
+
if (isLoading) return;
|
|
1803
|
+
|
|
1804
|
+
isLoading = true;
|
|
1805
|
+
showLoader('Creating commits...');
|
|
1806
|
+
|
|
1807
|
+
try {
|
|
1808
|
+
const res = await fetch('/api/commit', {
|
|
1809
|
+
method: 'POST',
|
|
1810
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1811
|
+
body: JSON.stringify({ groups: commitGroups })
|
|
1812
|
+
});
|
|
1813
|
+
const data = await res.json();
|
|
1814
|
+
|
|
1815
|
+
if (data.error) {
|
|
1816
|
+
showToast(data.error, 'error');
|
|
1817
|
+
} else {
|
|
1818
|
+
showToast(\`Successfully created \${data.committed} commit(s)!\`, 'success');
|
|
1819
|
+
hideCommitPanel();
|
|
1820
|
+
selectedFiles.clear();
|
|
1821
|
+
await refreshFiles();
|
|
1822
|
+
}
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
showToast('Failed to create commits', 'error');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
hideLoader();
|
|
1828
|
+
isLoading = false;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function showToast(message, type = 'success') {
|
|
1832
|
+
const toast = document.getElementById('toast');
|
|
1833
|
+
toast.textContent = message;
|
|
1834
|
+
toast.className = 'toast ' + type + ' show';
|
|
1835
|
+
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function showLoader(text = 'Loading...') {
|
|
1839
|
+
document.getElementById('loaderText').textContent = text;
|
|
1840
|
+
document.getElementById('loader').classList.add('show');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function hideLoader() {
|
|
1844
|
+
document.getElementById('loader').classList.remove('show');
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function escapeHtml(text) {
|
|
1848
|
+
return text?.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') || '';
|
|
1849
|
+
}
|
|
1850
|
+
</script>
|
|
1851
|
+
</body>
|
|
1852
|
+
</html>`;
|
|
1853
|
+
}
|
|
1854
|
+
function openBrowser(url) {
|
|
1855
|
+
const { exec } = require("child_process");
|
|
1856
|
+
const cmd = process.platform === "darwin"
|
|
1857
|
+
? `open "${url}"`
|
|
1858
|
+
: process.platform === "win32"
|
|
1859
|
+
? `start "${url}"`
|
|
1860
|
+
: `xdg-open "${url}"`;
|
|
1861
|
+
exec(cmd, (err) => {
|
|
1862
|
+
if (err) {
|
|
1863
|
+
console.log(chalk_1.default.yellow(`Open manually: ${url}`));
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
async function runUI() {
|
|
1868
|
+
console.log(chalk_1.default.blue.bold("\n🎨 Git AI - Commit Manager\n"));
|
|
1869
|
+
if (!(await git.isGitRepository())) {
|
|
1870
|
+
console.log(chalk_1.default.red("❌ Not a git repository\n"));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const apiKey = await (0, config_1.getOpenAIKey)();
|
|
1874
|
+
if (!apiKey) {
|
|
1875
|
+
console.log(chalk_1.default.red("❌ OpenAI API key not configured. Run: git-ai setup\n"));
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const server = http.createServer(async (req, res) => {
|
|
1879
|
+
const url = req.url || "/";
|
|
1880
|
+
const method = req.method || "GET";
|
|
1881
|
+
// CORS headers
|
|
1882
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1883
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1884
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1885
|
+
if (method === "OPTIONS") {
|
|
1886
|
+
res.writeHead(200);
|
|
1887
|
+
res.end();
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
// SSE: Real-time file change notifications
|
|
1891
|
+
if (url === "/api/events" && method === "GET") {
|
|
1892
|
+
res.writeHead(200, {
|
|
1893
|
+
"Content-Type": "text/event-stream",
|
|
1894
|
+
"Cache-Control": "no-cache",
|
|
1895
|
+
"Connection": "keep-alive",
|
|
1896
|
+
});
|
|
1897
|
+
res.write(`data: connected\n\n`);
|
|
1898
|
+
sseClients.add(res);
|
|
1899
|
+
req.on("close", () => {
|
|
1900
|
+
sseClients.delete(res);
|
|
1901
|
+
});
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
// API: Get changed files
|
|
1905
|
+
if (url.startsWith("/api/files") && method === "GET") {
|
|
1906
|
+
try {
|
|
1907
|
+
const files = await git.getChangedFiles();
|
|
1908
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1909
|
+
res.end(JSON.stringify(files));
|
|
1910
|
+
}
|
|
1911
|
+
catch (err) {
|
|
1912
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1913
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1914
|
+
}
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
// API: Stage a file
|
|
1918
|
+
if (url === "/api/stage" && method === "POST") {
|
|
1919
|
+
let body = "";
|
|
1920
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
1921
|
+
req.on("end", async () => {
|
|
1922
|
+
try {
|
|
1923
|
+
const { file } = JSON.parse(body);
|
|
1924
|
+
await git.stageFile(file);
|
|
1925
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1926
|
+
res.end(JSON.stringify({ success: true }));
|
|
1927
|
+
notifyClients();
|
|
1928
|
+
}
|
|
1929
|
+
catch (err) {
|
|
1930
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1931
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
// API: Unstage a file
|
|
1937
|
+
if (url === "/api/unstage" && method === "POST") {
|
|
1938
|
+
let body = "";
|
|
1939
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
1940
|
+
req.on("end", async () => {
|
|
1941
|
+
try {
|
|
1942
|
+
const { file } = JSON.parse(body);
|
|
1943
|
+
await git.unstageFile(file);
|
|
1944
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1945
|
+
res.end(JSON.stringify({ success: true }));
|
|
1946
|
+
notifyClients();
|
|
1947
|
+
}
|
|
1948
|
+
catch (err) {
|
|
1949
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1950
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
// API: Get diff for a file
|
|
1956
|
+
if (url.startsWith("/api/diff") && method === "GET") {
|
|
1957
|
+
const params = new URLSearchParams(url.split("?")[1] || "");
|
|
1958
|
+
const file = params.get("file") || "";
|
|
1959
|
+
const status = params.get("status") || "";
|
|
1960
|
+
const stagedParam = params.get("staged");
|
|
1961
|
+
const staged = stagedParam === "true" ? true : stagedParam === "false" ? false : undefined;
|
|
1962
|
+
try {
|
|
1963
|
+
const diff = await getFileDiff(file, status, staged);
|
|
1964
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
1965
|
+
res.end(diff);
|
|
1966
|
+
}
|
|
1967
|
+
catch (err) {
|
|
1968
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1969
|
+
res.end("");
|
|
1970
|
+
}
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
// API: Analyze selected files
|
|
1974
|
+
if (url === "/api/analyze" && method === "POST") {
|
|
1975
|
+
let body = "";
|
|
1976
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
1977
|
+
req.on("end", async () => {
|
|
1978
|
+
try {
|
|
1979
|
+
const { files: selectedFiles } = JSON.parse(body);
|
|
1980
|
+
// Get file list to check status
|
|
1981
|
+
const changedFiles = await git.getChangedFiles();
|
|
1982
|
+
// Get diff for each selected file
|
|
1983
|
+
const diffs = [];
|
|
1984
|
+
for (const file of selectedFiles) {
|
|
1985
|
+
const fileInfo = changedFiles.find((f) => f.file === file);
|
|
1986
|
+
const status = fileInfo ? fileInfo.status : "modified";
|
|
1987
|
+
const diff = await getFileDiff(file, status);
|
|
1988
|
+
if (diff) {
|
|
1989
|
+
diffs.push(diff);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
const rawDiff = diffs.join("\n");
|
|
1993
|
+
if (!rawDiff) {
|
|
1994
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1995
|
+
res.end(JSON.stringify({ error: "No diff found for selected files" }));
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
// Parse diff into hunks using hunk-parser (same as commit.ts)
|
|
1999
|
+
let fileDiffs = rawDiff.trim() ? (0, hunk_parser_1.parseDiff)(rawDiff) : [];
|
|
2000
|
+
// Add untracked/new files that weren't in diff
|
|
2001
|
+
const parsedFiles = new Set(fileDiffs.map(f => f.file));
|
|
2002
|
+
for (const file of selectedFiles) {
|
|
2003
|
+
if (!parsedFiles.has(file)) {
|
|
2004
|
+
const fileInfo = changedFiles.find((f) => f.file === file);
|
|
2005
|
+
if (fileInfo) {
|
|
2006
|
+
fileDiffs.push({
|
|
2007
|
+
file: fileInfo.file,
|
|
2008
|
+
isNew: fileInfo.status === "new",
|
|
2009
|
+
isDeleted: fileInfo.status === "deleted",
|
|
2010
|
+
isBinary: fileInfo.isBinary,
|
|
2011
|
+
hunks: [{
|
|
2012
|
+
file: fileInfo.file,
|
|
2013
|
+
index: 0,
|
|
2014
|
+
header: fileInfo.status === "new" ? "[NEW]" : "[FILE]",
|
|
2015
|
+
content: "",
|
|
2016
|
+
summary: fileInfo.status === "new" ? "New file" :
|
|
2017
|
+
fileInfo.status === "deleted" ? "Deleted file" : "Modified file"
|
|
2018
|
+
}]
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (fileDiffs.length === 0) {
|
|
2024
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2025
|
+
res.end(JSON.stringify({ error: "No changes found for selected files" }));
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
// Format for AI (with hunk indices)
|
|
2029
|
+
const formattedDiff = (0, hunk_parser_1.formatForAI)(fileDiffs);
|
|
2030
|
+
const stats = (0, hunk_parser_1.getStats)(fileDiffs);
|
|
2031
|
+
// Analyze with AI
|
|
2032
|
+
const result = await openai.analyzeAndGroup(formattedDiff, stats, apiKey);
|
|
2033
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2034
|
+
res.end(JSON.stringify(result));
|
|
2035
|
+
}
|
|
2036
|
+
catch (err) {
|
|
2037
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2038
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
// API: Execute commits
|
|
2044
|
+
if (url === "/api/commit" && method === "POST") {
|
|
2045
|
+
let body = "";
|
|
2046
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
2047
|
+
req.on("end", async () => {
|
|
2048
|
+
try {
|
|
2049
|
+
const { groups } = JSON.parse(body);
|
|
2050
|
+
let committed = 0;
|
|
2051
|
+
for (const group of groups) {
|
|
2052
|
+
// Get files from either 'files' array or extract from 'hunks'
|
|
2053
|
+
const files = group.files || [...new Set((group.hunks || []).map((h) => h.file))];
|
|
2054
|
+
// Stage files for this group
|
|
2055
|
+
await git.unstageAll();
|
|
2056
|
+
await git.stageFiles(files);
|
|
2057
|
+
// Create commit
|
|
2058
|
+
const message = group.commitBody
|
|
2059
|
+
? `${group.commitMessage}\n\n${group.commitBody}`
|
|
2060
|
+
: group.commitMessage;
|
|
2061
|
+
await git.createCommit(message);
|
|
2062
|
+
committed++;
|
|
2063
|
+
}
|
|
2064
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2065
|
+
res.end(JSON.stringify({ success: true, committed }));
|
|
2066
|
+
}
|
|
2067
|
+
catch (err) {
|
|
2068
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2069
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
// Default: serve HTML
|
|
2075
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2076
|
+
res.end(getHtml());
|
|
2077
|
+
});
|
|
2078
|
+
server.on("error", (err) => {
|
|
2079
|
+
if (err.code === "EADDRINUSE") {
|
|
2080
|
+
console.log(chalk_1.default.red(`\n❌ Port ${PORT} is already in use.`));
|
|
2081
|
+
console.log(chalk_1.default.gray(`Run: lsof -ti:${PORT} | xargs kill -9\n`));
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
console.log(chalk_1.default.red(`\n❌ Server error: ${err.message}\n`));
|
|
2085
|
+
}
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
});
|
|
2088
|
+
server.listen(PORT, () => {
|
|
2089
|
+
const url = `http://localhost:${PORT}`;
|
|
2090
|
+
console.log(chalk_1.default.green(`✓ Server running at ${url}`));
|
|
2091
|
+
console.log(chalk_1.default.gray("Press Ctrl+C to stop\n"));
|
|
2092
|
+
startFileWatcher();
|
|
2093
|
+
openBrowser(url);
|
|
2094
|
+
});
|
|
2095
|
+
process.on("SIGINT", () => {
|
|
2096
|
+
stopFileWatcher();
|
|
2097
|
+
console.log(chalk_1.default.yellow("\n\n👋 Server stopped\n"));
|
|
2098
|
+
server.close();
|
|
2099
|
+
process.exit(0);
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
//# sourceMappingURL=ui.js.map
|