@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.
Files changed (66) hide show
  1. package/README.md +25 -0
  2. package/dist/add.d.ts +5 -0
  3. package/dist/add.d.ts.map +1 -0
  4. package/dist/add.js +98 -0
  5. package/dist/add.js.map +1 -0
  6. package/dist/commit.d.ts +1 -1
  7. package/dist/commit.d.ts.map +1 -1
  8. package/dist/commit.js +37 -4
  9. package/dist/commit.js.map +1 -1
  10. package/dist/config.d.ts +11 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +64 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/git.d.ts +12 -2
  15. package/dist/git.d.ts.map +1 -1
  16. package/dist/git.js +200 -8
  17. package/dist/git.js.map +1 -1
  18. package/dist/index.js +149 -51
  19. package/dist/index.js.map +1 -1
  20. package/dist/openai.d.ts +3 -0
  21. package/dist/openai.d.ts.map +1 -1
  22. package/dist/openai.js +18 -0
  23. package/dist/openai.js.map +1 -1
  24. package/dist/prompts.d.ts +2 -0
  25. package/dist/prompts.d.ts.map +1 -1
  26. package/dist/prompts.js +46 -0
  27. package/dist/prompts.js.map +1 -1
  28. package/dist/set-editor.d.ts +5 -0
  29. package/dist/set-editor.d.ts.map +1 -0
  30. package/dist/set-editor.js +59 -0
  31. package/dist/set-editor.js.map +1 -0
  32. package/dist/summary.d.ts +5 -0
  33. package/dist/summary.d.ts.map +1 -0
  34. package/dist/summary.js +175 -0
  35. package/dist/summary.js.map +1 -0
  36. package/dist/types.d.ts +25 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/ui.d.ts +2 -0
  39. package/dist/ui.d.ts.map +1 -0
  40. package/dist/ui.js +2102 -0
  41. package/dist/ui.js.map +1 -0
  42. package/dist/update.d.ts +9 -0
  43. package/dist/update.d.ts.map +1 -0
  44. package/dist/update.js +68 -0
  45. package/dist/update.js.map +1 -0
  46. package/dist/user-management.d.ts +10 -0
  47. package/dist/user-management.d.ts.map +1 -0
  48. package/dist/user-management.js +175 -0
  49. package/dist/user-management.js.map +1 -0
  50. package/dist/users.d.ts +9 -0
  51. package/dist/users.d.ts.map +1 -0
  52. package/dist/users.js +129 -0
  53. package/dist/users.js.map +1 -0
  54. package/dist/utils/commit-file.d.ts +30 -0
  55. package/dist/utils/commit-file.d.ts.map +1 -0
  56. package/dist/utils/commit-file.js +192 -0
  57. package/dist/utils/commit-file.js.map +1 -0
  58. package/dist/utils/editor.d.ts +29 -0
  59. package/dist/utils/editor.d.ts.map +1 -0
  60. package/dist/utils/editor.js +245 -0
  61. package/dist/utils/editor.js.map +1 -0
  62. package/dist/utils/validation.d.ts +24 -0
  63. package/dist/utils/validation.d.ts.map +1 -0
  64. package/dist/utils/validation.js +81 -0
  65. package/dist/utils/validation.js.map +1 -0
  66. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') || '';
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