claude-mpm 4.2.39__py3-none-any.whl → 4.2.42__py3-none-any.whl
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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +114 -1
- claude_mpm/agents/BASE_OPS.md +156 -1
- claude_mpm/agents/INSTRUCTIONS.md +120 -11
- claude_mpm/agents/WORKFLOW.md +160 -10
- claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
- claude_mpm/agents/templates/react_engineer.json +217 -0
- claude_mpm/agents/templates/web_qa.json +40 -4
- claude_mpm/cli/__init__.py +3 -5
- claude_mpm/commands/mpm-browser-monitor.md +370 -0
- claude_mpm/commands/mpm-monitor.md +177 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
- claude_mpm/dashboard/static/css/dashboard.css +2 -0
- claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
- claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
- claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +307 -19
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
- claude_mpm/dashboard/templates/index.html +62 -99
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/monitor/daemon.py +69 -36
- claude_mpm/services/monitor/daemon_manager.py +186 -29
- claude_mpm/services/monitor/handlers/browser.py +451 -0
- claude_mpm/services/monitor/server.py +272 -5
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/RECORD +35 -29
- claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
- claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
- claude_mpm/agents/templates/test-non-mpm.json +0 -20
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,891 @@
|
|
1
|
+
/**
|
2
|
+
* Diff Viewer Component
|
3
|
+
*
|
4
|
+
* Shows side-by-side or unified diffs for file changes.
|
5
|
+
* Supports syntax highlighting, navigation between changes,
|
6
|
+
* and displays timestamps and operation types.
|
7
|
+
*
|
8
|
+
* Features:
|
9
|
+
* - Side-by-side and unified diff views
|
10
|
+
* - Syntax highlighting for code
|
11
|
+
* - Navigation between multiple changes
|
12
|
+
* - Operation history timeline
|
13
|
+
*/
|
14
|
+
class DiffViewer {
|
15
|
+
constructor() {
|
16
|
+
this.modal = null;
|
17
|
+
this.currentFile = null;
|
18
|
+
this.currentMode = 'side-by-side'; // 'side-by-side' or 'unified'
|
19
|
+
this.initialized = false;
|
20
|
+
|
21
|
+
// Diff computation cache
|
22
|
+
this.diffCache = new Map();
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Initialize the diff viewer
|
27
|
+
*/
|
28
|
+
initialize() {
|
29
|
+
if (this.initialized) return;
|
30
|
+
|
31
|
+
this.createModal();
|
32
|
+
this.setupEventHandlers();
|
33
|
+
this.initialized = true;
|
34
|
+
console.log('DiffViewer initialized');
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Create the modal HTML structure
|
39
|
+
*/
|
40
|
+
createModal() {
|
41
|
+
const modalHTML = `
|
42
|
+
<div class="diff-viewer-modal" id="diff-viewer-modal">
|
43
|
+
<div class="diff-viewer-content">
|
44
|
+
<div class="diff-viewer-header">
|
45
|
+
<div class="diff-viewer-title">
|
46
|
+
<span class="diff-file-icon">📄</span>
|
47
|
+
<span class="diff-file-path" id="diff-file-path">Loading...</span>
|
48
|
+
</div>
|
49
|
+
<div class="diff-viewer-controls">
|
50
|
+
<div class="diff-mode-toggle">
|
51
|
+
<button class="diff-mode-btn active" data-mode="side-by-side">
|
52
|
+
Side by Side
|
53
|
+
</button>
|
54
|
+
<button class="diff-mode-btn" data-mode="unified">
|
55
|
+
Unified
|
56
|
+
</button>
|
57
|
+
</div>
|
58
|
+
<div class="diff-stats" id="diff-stats">
|
59
|
+
<span class="additions">+0</span>
|
60
|
+
<span class="deletions">-0</span>
|
61
|
+
</div>
|
62
|
+
<button class="diff-viewer-close" id="diff-viewer-close">×</button>
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
|
66
|
+
<div class="diff-viewer-subheader">
|
67
|
+
<div class="diff-operations-summary" id="diff-operations-summary">
|
68
|
+
<span class="op-count">0 operations</span>
|
69
|
+
<span class="op-timeline">Timeline</span>
|
70
|
+
</div>
|
71
|
+
<div class="diff-navigation">
|
72
|
+
<button class="diff-nav-btn" id="diff-prev-change" disabled>
|
73
|
+
← Previous
|
74
|
+
</button>
|
75
|
+
<span class="diff-nav-info" id="diff-nav-info">Change 1 of 1</span>
|
76
|
+
<button class="diff-nav-btn" id="diff-next-change" disabled>
|
77
|
+
Next →
|
78
|
+
</button>
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<div class="diff-viewer-body" id="diff-viewer-body">
|
83
|
+
<!-- Diff content will be inserted here -->
|
84
|
+
</div>
|
85
|
+
|
86
|
+
<div class="diff-viewer-footer">
|
87
|
+
<div class="diff-timeline" id="diff-timeline">
|
88
|
+
<!-- Operation timeline will be inserted here -->
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
</div>
|
93
|
+
`;
|
94
|
+
|
95
|
+
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
96
|
+
this.modal = document.getElementById('diff-viewer-modal');
|
97
|
+
|
98
|
+
// Add CSS for the diff viewer
|
99
|
+
this.injectStyles();
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Inject CSS styles for the diff viewer
|
104
|
+
*/
|
105
|
+
injectStyles() {
|
106
|
+
const styleId = 'diff-viewer-styles';
|
107
|
+
if (document.getElementById(styleId)) return;
|
108
|
+
|
109
|
+
const styles = `
|
110
|
+
<style id="${styleId}">
|
111
|
+
.diff-viewer-modal {
|
112
|
+
display: none;
|
113
|
+
position: fixed;
|
114
|
+
top: 0;
|
115
|
+
left: 0;
|
116
|
+
right: 0;
|
117
|
+
bottom: 0;
|
118
|
+
background: rgba(0, 0, 0, 0.5);
|
119
|
+
z-index: 10000;
|
120
|
+
padding: 20px;
|
121
|
+
overflow: auto;
|
122
|
+
}
|
123
|
+
|
124
|
+
.diff-viewer-modal.show {
|
125
|
+
display: flex;
|
126
|
+
align-items: center;
|
127
|
+
justify-content: center;
|
128
|
+
}
|
129
|
+
|
130
|
+
.diff-viewer-content {
|
131
|
+
background: white;
|
132
|
+
border-radius: 8px;
|
133
|
+
width: 90%;
|
134
|
+
max-width: 1400px;
|
135
|
+
height: 90%;
|
136
|
+
display: flex;
|
137
|
+
flex-direction: column;
|
138
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
139
|
+
}
|
140
|
+
|
141
|
+
.diff-viewer-header {
|
142
|
+
display: flex;
|
143
|
+
justify-content: space-between;
|
144
|
+
align-items: center;
|
145
|
+
padding: 16px 20px;
|
146
|
+
border-bottom: 1px solid #e2e8f0;
|
147
|
+
background: #f8fafc;
|
148
|
+
border-radius: 8px 8px 0 0;
|
149
|
+
}
|
150
|
+
|
151
|
+
.diff-viewer-title {
|
152
|
+
display: flex;
|
153
|
+
align-items: center;
|
154
|
+
gap: 8px;
|
155
|
+
font-size: 14px;
|
156
|
+
font-weight: 600;
|
157
|
+
color: #2d3748;
|
158
|
+
}
|
159
|
+
|
160
|
+
.diff-file-icon {
|
161
|
+
font-size: 18px;
|
162
|
+
}
|
163
|
+
|
164
|
+
.diff-file-path {
|
165
|
+
font-family: 'SF Mono', Monaco, monospace;
|
166
|
+
font-size: 13px;
|
167
|
+
}
|
168
|
+
|
169
|
+
.diff-viewer-controls {
|
170
|
+
display: flex;
|
171
|
+
align-items: center;
|
172
|
+
gap: 16px;
|
173
|
+
}
|
174
|
+
|
175
|
+
.diff-mode-toggle {
|
176
|
+
display: flex;
|
177
|
+
gap: 4px;
|
178
|
+
background: #e2e8f0;
|
179
|
+
padding: 2px;
|
180
|
+
border-radius: 4px;
|
181
|
+
}
|
182
|
+
|
183
|
+
.diff-mode-btn {
|
184
|
+
padding: 4px 12px;
|
185
|
+
border: none;
|
186
|
+
background: transparent;
|
187
|
+
cursor: pointer;
|
188
|
+
font-size: 12px;
|
189
|
+
border-radius: 3px;
|
190
|
+
transition: all 0.2s;
|
191
|
+
}
|
192
|
+
|
193
|
+
.diff-mode-btn.active {
|
194
|
+
background: white;
|
195
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
196
|
+
}
|
197
|
+
|
198
|
+
.diff-stats {
|
199
|
+
display: flex;
|
200
|
+
gap: 8px;
|
201
|
+
font-size: 12px;
|
202
|
+
font-family: monospace;
|
203
|
+
}
|
204
|
+
|
205
|
+
.diff-stats .additions {
|
206
|
+
color: #22c55e;
|
207
|
+
}
|
208
|
+
|
209
|
+
.diff-stats .deletions {
|
210
|
+
color: #ef4444;
|
211
|
+
}
|
212
|
+
|
213
|
+
.diff-viewer-close {
|
214
|
+
width: 32px;
|
215
|
+
height: 32px;
|
216
|
+
border: none;
|
217
|
+
background: transparent;
|
218
|
+
font-size: 24px;
|
219
|
+
cursor: pointer;
|
220
|
+
color: #718096;
|
221
|
+
display: flex;
|
222
|
+
align-items: center;
|
223
|
+
justify-content: center;
|
224
|
+
border-radius: 4px;
|
225
|
+
transition: all 0.2s;
|
226
|
+
}
|
227
|
+
|
228
|
+
.diff-viewer-close:hover {
|
229
|
+
background: #e2e8f0;
|
230
|
+
color: #2d3748;
|
231
|
+
}
|
232
|
+
|
233
|
+
.diff-viewer-subheader {
|
234
|
+
display: flex;
|
235
|
+
justify-content: space-between;
|
236
|
+
align-items: center;
|
237
|
+
padding: 12px 20px;
|
238
|
+
border-bottom: 1px solid #e2e8f0;
|
239
|
+
background: #fafbfc;
|
240
|
+
}
|
241
|
+
|
242
|
+
.diff-operations-summary {
|
243
|
+
display: flex;
|
244
|
+
gap: 16px;
|
245
|
+
font-size: 13px;
|
246
|
+
color: #4a5568;
|
247
|
+
}
|
248
|
+
|
249
|
+
.diff-navigation {
|
250
|
+
display: flex;
|
251
|
+
align-items: center;
|
252
|
+
gap: 8px;
|
253
|
+
}
|
254
|
+
|
255
|
+
.diff-nav-btn {
|
256
|
+
padding: 4px 12px;
|
257
|
+
border: 1px solid #cbd5e0;
|
258
|
+
background: white;
|
259
|
+
cursor: pointer;
|
260
|
+
font-size: 12px;
|
261
|
+
border-radius: 4px;
|
262
|
+
transition: all 0.2s;
|
263
|
+
}
|
264
|
+
|
265
|
+
.diff-nav-btn:hover:not(:disabled) {
|
266
|
+
background: #f8fafc;
|
267
|
+
border-color: #4299e1;
|
268
|
+
}
|
269
|
+
|
270
|
+
.diff-nav-btn:disabled {
|
271
|
+
opacity: 0.5;
|
272
|
+
cursor: not-allowed;
|
273
|
+
}
|
274
|
+
|
275
|
+
.diff-nav-info {
|
276
|
+
font-size: 12px;
|
277
|
+
color: #718096;
|
278
|
+
padding: 0 8px;
|
279
|
+
}
|
280
|
+
|
281
|
+
.diff-viewer-body {
|
282
|
+
flex: 1;
|
283
|
+
overflow: auto;
|
284
|
+
padding: 20px;
|
285
|
+
background: #fafbfc;
|
286
|
+
}
|
287
|
+
|
288
|
+
/* Side-by-side diff styles */
|
289
|
+
.diff-side-by-side {
|
290
|
+
display: flex;
|
291
|
+
gap: 16px;
|
292
|
+
height: 100%;
|
293
|
+
}
|
294
|
+
|
295
|
+
.diff-panel {
|
296
|
+
flex: 1;
|
297
|
+
background: white;
|
298
|
+
border: 1px solid #e2e8f0;
|
299
|
+
border-radius: 4px;
|
300
|
+
overflow: hidden;
|
301
|
+
}
|
302
|
+
|
303
|
+
.diff-panel-header {
|
304
|
+
padding: 8px 12px;
|
305
|
+
background: #f8fafc;
|
306
|
+
border-bottom: 1px solid #e2e8f0;
|
307
|
+
font-size: 12px;
|
308
|
+
font-weight: 600;
|
309
|
+
color: #4a5568;
|
310
|
+
}
|
311
|
+
|
312
|
+
.diff-panel-content {
|
313
|
+
overflow: auto;
|
314
|
+
height: calc(100% - 36px);
|
315
|
+
}
|
316
|
+
|
317
|
+
.diff-line {
|
318
|
+
display: flex;
|
319
|
+
font-family: 'SF Mono', Monaco, monospace;
|
320
|
+
font-size: 12px;
|
321
|
+
line-height: 1.5;
|
322
|
+
white-space: pre;
|
323
|
+
}
|
324
|
+
|
325
|
+
.diff-line-number {
|
326
|
+
width: 50px;
|
327
|
+
padding: 0 8px;
|
328
|
+
text-align: right;
|
329
|
+
color: #a0aec0;
|
330
|
+
background: #f8fafc;
|
331
|
+
border-right: 1px solid #e2e8f0;
|
332
|
+
user-select: none;
|
333
|
+
}
|
334
|
+
|
335
|
+
.diff-line-content {
|
336
|
+
flex: 1;
|
337
|
+
padding: 0 12px;
|
338
|
+
overflow-x: auto;
|
339
|
+
}
|
340
|
+
|
341
|
+
.diff-line-added {
|
342
|
+
background: #d4f4dd;
|
343
|
+
}
|
344
|
+
|
345
|
+
.diff-line-added .diff-line-content {
|
346
|
+
background: #e7fced;
|
347
|
+
}
|
348
|
+
|
349
|
+
.diff-line-removed {
|
350
|
+
background: #ffd4d4;
|
351
|
+
}
|
352
|
+
|
353
|
+
.diff-line-removed .diff-line-content {
|
354
|
+
background: #ffeaea;
|
355
|
+
}
|
356
|
+
|
357
|
+
.diff-line-context {
|
358
|
+
color: #4a5568;
|
359
|
+
}
|
360
|
+
|
361
|
+
/* Unified diff styles */
|
362
|
+
.diff-unified {
|
363
|
+
background: white;
|
364
|
+
border: 1px solid #e2e8f0;
|
365
|
+
border-radius: 4px;
|
366
|
+
overflow: auto;
|
367
|
+
}
|
368
|
+
|
369
|
+
.diff-hunk-header {
|
370
|
+
padding: 8px 12px;
|
371
|
+
background: #f1f5f9;
|
372
|
+
color: #475569;
|
373
|
+
font-family: monospace;
|
374
|
+
font-size: 12px;
|
375
|
+
border-bottom: 1px solid #e2e8f0;
|
376
|
+
}
|
377
|
+
|
378
|
+
/* Timeline styles */
|
379
|
+
.diff-viewer-footer {
|
380
|
+
padding: 12px 20px;
|
381
|
+
border-top: 1px solid #e2e8f0;
|
382
|
+
background: #f8fafc;
|
383
|
+
border-radius: 0 0 8px 8px;
|
384
|
+
}
|
385
|
+
|
386
|
+
.diff-timeline {
|
387
|
+
display: flex;
|
388
|
+
gap: 8px;
|
389
|
+
overflow-x: auto;
|
390
|
+
padding: 8px 0;
|
391
|
+
}
|
392
|
+
|
393
|
+
.timeline-item {
|
394
|
+
display: flex;
|
395
|
+
flex-direction: column;
|
396
|
+
align-items: center;
|
397
|
+
gap: 4px;
|
398
|
+
padding: 4px 8px;
|
399
|
+
background: white;
|
400
|
+
border: 1px solid #cbd5e0;
|
401
|
+
border-radius: 4px;
|
402
|
+
cursor: pointer;
|
403
|
+
transition: all 0.2s;
|
404
|
+
min-width: 80px;
|
405
|
+
font-size: 11px;
|
406
|
+
}
|
407
|
+
|
408
|
+
.timeline-item:hover {
|
409
|
+
background: #edf2f7;
|
410
|
+
border-color: #4299e1;
|
411
|
+
}
|
412
|
+
|
413
|
+
.timeline-item.active {
|
414
|
+
background: #4299e1;
|
415
|
+
color: white;
|
416
|
+
border-color: #3182ce;
|
417
|
+
}
|
418
|
+
|
419
|
+
.timeline-operation {
|
420
|
+
font-weight: 600;
|
421
|
+
}
|
422
|
+
|
423
|
+
.timeline-time {
|
424
|
+
color: #718096;
|
425
|
+
font-size: 10px;
|
426
|
+
}
|
427
|
+
|
428
|
+
.timeline-item.active .timeline-time {
|
429
|
+
color: rgba(255, 255, 255, 0.9);
|
430
|
+
}
|
431
|
+
</style>
|
432
|
+
`;
|
433
|
+
|
434
|
+
document.head.insertAdjacentHTML('beforeend', styles);
|
435
|
+
}
|
436
|
+
|
437
|
+
/**
|
438
|
+
* Setup event handlers
|
439
|
+
*/
|
440
|
+
setupEventHandlers() {
|
441
|
+
// Close button
|
442
|
+
document.getElementById('diff-viewer-close').addEventListener('click', () => {
|
443
|
+
this.hide();
|
444
|
+
});
|
445
|
+
|
446
|
+
// Modal background click
|
447
|
+
this.modal.addEventListener('click', (e) => {
|
448
|
+
if (e.target === this.modal) {
|
449
|
+
this.hide();
|
450
|
+
}
|
451
|
+
});
|
452
|
+
|
453
|
+
// Escape key
|
454
|
+
document.addEventListener('keydown', (e) => {
|
455
|
+
if (e.key === 'Escape' && this.modal.classList.contains('show')) {
|
456
|
+
this.hide();
|
457
|
+
}
|
458
|
+
});
|
459
|
+
|
460
|
+
// Mode toggle
|
461
|
+
document.querySelectorAll('.diff-mode-btn').forEach(btn => {
|
462
|
+
btn.addEventListener('click', (e) => {
|
463
|
+
this.setMode(e.target.dataset.mode);
|
464
|
+
});
|
465
|
+
});
|
466
|
+
|
467
|
+
// Navigation
|
468
|
+
document.getElementById('diff-prev-change').addEventListener('click', () => {
|
469
|
+
this.navigateToPreviousChange();
|
470
|
+
});
|
471
|
+
|
472
|
+
document.getElementById('diff-next-change').addEventListener('click', () => {
|
473
|
+
this.navigateToNextChange();
|
474
|
+
});
|
475
|
+
}
|
476
|
+
|
477
|
+
/**
|
478
|
+
* Show the diff viewer for a file
|
479
|
+
* @param {Object} diffData - Diff data from FileChangeTracker
|
480
|
+
*/
|
481
|
+
show(diffData) {
|
482
|
+
if (!this.initialized) {
|
483
|
+
this.initialize();
|
484
|
+
}
|
485
|
+
|
486
|
+
this.currentFile = diffData;
|
487
|
+
this.modal.classList.add('show');
|
488
|
+
|
489
|
+
// Update header
|
490
|
+
document.getElementById('diff-file-path').textContent = diffData.filePath;
|
491
|
+
|
492
|
+
// Generate and display diff
|
493
|
+
this.generateDiff(diffData);
|
494
|
+
|
495
|
+
// Update operations timeline
|
496
|
+
this.updateTimeline(diffData.operations);
|
497
|
+
|
498
|
+
// Update statistics
|
499
|
+
this.updateStatistics(diffData);
|
500
|
+
}
|
501
|
+
|
502
|
+
/**
|
503
|
+
* Hide the diff viewer
|
504
|
+
*/
|
505
|
+
hide() {
|
506
|
+
this.modal.classList.remove('show');
|
507
|
+
this.currentFile = null;
|
508
|
+
}
|
509
|
+
|
510
|
+
/**
|
511
|
+
* Set diff display mode
|
512
|
+
* @param {string} mode - 'side-by-side' or 'unified'
|
513
|
+
*/
|
514
|
+
setMode(mode) {
|
515
|
+
this.currentMode = mode;
|
516
|
+
|
517
|
+
// Update button states
|
518
|
+
document.querySelectorAll('.diff-mode-btn').forEach(btn => {
|
519
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
520
|
+
});
|
521
|
+
|
522
|
+
// Regenerate diff display
|
523
|
+
if (this.currentFile) {
|
524
|
+
this.generateDiff(this.currentFile);
|
525
|
+
}
|
526
|
+
}
|
527
|
+
|
528
|
+
/**
|
529
|
+
* Generate diff display
|
530
|
+
* @param {Object} diffData - Diff data
|
531
|
+
*/
|
532
|
+
generateDiff(diffData) {
|
533
|
+
const { initialContent, currentContent } = diffData;
|
534
|
+
|
535
|
+
// Compute diff
|
536
|
+
const diff = this.computeDiff(initialContent || '', currentContent || '');
|
537
|
+
|
538
|
+
// Display based on mode
|
539
|
+
const body = document.getElementById('diff-viewer-body');
|
540
|
+
if (this.currentMode === 'side-by-side') {
|
541
|
+
body.innerHTML = this.renderSideBySideDiff(diff, initialContent, currentContent);
|
542
|
+
} else {
|
543
|
+
body.innerHTML = this.renderUnifiedDiff(diff);
|
544
|
+
}
|
545
|
+
}
|
546
|
+
|
547
|
+
/**
|
548
|
+
* Compute diff between two strings
|
549
|
+
* @param {string} oldText - Original text
|
550
|
+
* @param {string} newText - New text
|
551
|
+
* @returns {Array} Array of diff chunks
|
552
|
+
*/
|
553
|
+
computeDiff(oldText, newText) {
|
554
|
+
const oldLines = oldText.split('\n');
|
555
|
+
const newLines = newText.split('\n');
|
556
|
+
|
557
|
+
// Simple line-by-line diff algorithm
|
558
|
+
const diff = [];
|
559
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
560
|
+
|
561
|
+
let oldIndex = 0;
|
562
|
+
let newIndex = 0;
|
563
|
+
|
564
|
+
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
565
|
+
const oldLine = oldIndex < oldLines.length ? oldLines[oldIndex] : null;
|
566
|
+
const newLine = newIndex < newLines.length ? newLines[newIndex] : null;
|
567
|
+
|
568
|
+
if (oldLine === newLine) {
|
569
|
+
// Unchanged line
|
570
|
+
diff.push({
|
571
|
+
type: 'unchanged',
|
572
|
+
oldLine: oldLine,
|
573
|
+
newLine: newLine,
|
574
|
+
oldLineNumber: oldIndex + 1,
|
575
|
+
newLineNumber: newIndex + 1
|
576
|
+
});
|
577
|
+
oldIndex++;
|
578
|
+
newIndex++;
|
579
|
+
} else if (oldLine === null) {
|
580
|
+
// Added line
|
581
|
+
diff.push({
|
582
|
+
type: 'added',
|
583
|
+
newLine: newLine,
|
584
|
+
newLineNumber: newIndex + 1
|
585
|
+
});
|
586
|
+
newIndex++;
|
587
|
+
} else if (newLine === null) {
|
588
|
+
// Removed line
|
589
|
+
diff.push({
|
590
|
+
type: 'removed',
|
591
|
+
oldLine: oldLine,
|
592
|
+
oldLineNumber: oldIndex + 1
|
593
|
+
});
|
594
|
+
oldIndex++;
|
595
|
+
} else {
|
596
|
+
// Changed line - try to find matching lines ahead
|
597
|
+
let found = false;
|
598
|
+
|
599
|
+
// Look ahead in new lines for old line
|
600
|
+
for (let i = newIndex + 1; i < Math.min(newIndex + 5, newLines.length); i++) {
|
601
|
+
if (newLines[i] === oldLine) {
|
602
|
+
// Found old line ahead - mark intervening as added
|
603
|
+
for (let j = newIndex; j < i; j++) {
|
604
|
+
diff.push({
|
605
|
+
type: 'added',
|
606
|
+
newLine: newLines[j],
|
607
|
+
newLineNumber: j + 1
|
608
|
+
});
|
609
|
+
}
|
610
|
+
newIndex = i;
|
611
|
+
found = true;
|
612
|
+
break;
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
if (!found) {
|
617
|
+
// Look ahead in old lines for new line
|
618
|
+
for (let i = oldIndex + 1; i < Math.min(oldIndex + 5, oldLines.length); i++) {
|
619
|
+
if (oldLines[i] === newLine) {
|
620
|
+
// Found new line ahead - mark intervening as removed
|
621
|
+
for (let j = oldIndex; j < i; j++) {
|
622
|
+
diff.push({
|
623
|
+
type: 'removed',
|
624
|
+
oldLine: oldLines[j],
|
625
|
+
oldLineNumber: j + 1
|
626
|
+
});
|
627
|
+
}
|
628
|
+
oldIndex = i;
|
629
|
+
found = true;
|
630
|
+
break;
|
631
|
+
}
|
632
|
+
}
|
633
|
+
}
|
634
|
+
|
635
|
+
if (!found) {
|
636
|
+
// True change - show as remove + add
|
637
|
+
diff.push({
|
638
|
+
type: 'removed',
|
639
|
+
oldLine: oldLine,
|
640
|
+
oldLineNumber: oldIndex + 1
|
641
|
+
});
|
642
|
+
diff.push({
|
643
|
+
type: 'added',
|
644
|
+
newLine: newLine,
|
645
|
+
newLineNumber: newIndex + 1
|
646
|
+
});
|
647
|
+
oldIndex++;
|
648
|
+
newIndex++;
|
649
|
+
}
|
650
|
+
}
|
651
|
+
}
|
652
|
+
|
653
|
+
return diff;
|
654
|
+
}
|
655
|
+
|
656
|
+
/**
|
657
|
+
* Render side-by-side diff
|
658
|
+
* @param {Array} diff - Diff chunks
|
659
|
+
* @param {string} oldContent - Original content
|
660
|
+
* @param {string} newContent - New content
|
661
|
+
* @returns {string} HTML
|
662
|
+
*/
|
663
|
+
renderSideBySideDiff(diff, oldContent, newContent) {
|
664
|
+
const oldLines = [];
|
665
|
+
const newLines = [];
|
666
|
+
|
667
|
+
for (const chunk of diff) {
|
668
|
+
if (chunk.type === 'unchanged') {
|
669
|
+
oldLines.push({
|
670
|
+
number: chunk.oldLineNumber,
|
671
|
+
content: chunk.oldLine,
|
672
|
+
type: 'context'
|
673
|
+
});
|
674
|
+
newLines.push({
|
675
|
+
number: chunk.newLineNumber,
|
676
|
+
content: chunk.newLine,
|
677
|
+
type: 'context'
|
678
|
+
});
|
679
|
+
} else if (chunk.type === 'removed') {
|
680
|
+
oldLines.push({
|
681
|
+
number: chunk.oldLineNumber,
|
682
|
+
content: chunk.oldLine,
|
683
|
+
type: 'removed'
|
684
|
+
});
|
685
|
+
newLines.push({
|
686
|
+
number: '',
|
687
|
+
content: '',
|
688
|
+
type: 'empty'
|
689
|
+
});
|
690
|
+
} else if (chunk.type === 'added') {
|
691
|
+
oldLines.push({
|
692
|
+
number: '',
|
693
|
+
content: '',
|
694
|
+
type: 'empty'
|
695
|
+
});
|
696
|
+
newLines.push({
|
697
|
+
number: chunk.newLineNumber,
|
698
|
+
content: chunk.newLine,
|
699
|
+
type: 'added'
|
700
|
+
});
|
701
|
+
}
|
702
|
+
}
|
703
|
+
|
704
|
+
return `
|
705
|
+
<div class="diff-side-by-side">
|
706
|
+
<div class="diff-panel">
|
707
|
+
<div class="diff-panel-header">Original</div>
|
708
|
+
<div class="diff-panel-content">
|
709
|
+
${this.renderDiffLines(oldLines)}
|
710
|
+
</div>
|
711
|
+
</div>
|
712
|
+
<div class="diff-panel">
|
713
|
+
<div class="diff-panel-header">Modified</div>
|
714
|
+
<div class="diff-panel-content">
|
715
|
+
${this.renderDiffLines(newLines)}
|
716
|
+
</div>
|
717
|
+
</div>
|
718
|
+
</div>
|
719
|
+
`;
|
720
|
+
}
|
721
|
+
|
722
|
+
/**
|
723
|
+
* Render diff lines
|
724
|
+
* @param {Array} lines - Lines to render
|
725
|
+
* @returns {string} HTML
|
726
|
+
*/
|
727
|
+
renderDiffLines(lines) {
|
728
|
+
return lines.map(line => {
|
729
|
+
const lineClass = line.type === 'removed' ? 'diff-line-removed' :
|
730
|
+
line.type === 'added' ? 'diff-line-added' :
|
731
|
+
line.type === 'empty' ? 'diff-line-empty' :
|
732
|
+
'diff-line-context';
|
733
|
+
|
734
|
+
return `
|
735
|
+
<div class="diff-line ${lineClass}">
|
736
|
+
<span class="diff-line-number">${line.number}</span>
|
737
|
+
<span class="diff-line-content">${this.escapeHtml(line.content)}</span>
|
738
|
+
</div>
|
739
|
+
`;
|
740
|
+
}).join('');
|
741
|
+
}
|
742
|
+
|
743
|
+
/**
|
744
|
+
* Render unified diff
|
745
|
+
* @param {Array} diff - Diff chunks
|
746
|
+
* @returns {string} HTML
|
747
|
+
*/
|
748
|
+
renderUnifiedDiff(diff) {
|
749
|
+
const lines = [];
|
750
|
+
|
751
|
+
for (const chunk of diff) {
|
752
|
+
if (chunk.type === 'unchanged') {
|
753
|
+
lines.push(`
|
754
|
+
<div class="diff-line diff-line-context">
|
755
|
+
<span class="diff-line-number">${chunk.oldLineNumber}</span>
|
756
|
+
<span class="diff-line-number">${chunk.newLineNumber}</span>
|
757
|
+
<span class="diff-line-content"> ${this.escapeHtml(chunk.oldLine)}</span>
|
758
|
+
</div>
|
759
|
+
`);
|
760
|
+
} else if (chunk.type === 'removed') {
|
761
|
+
lines.push(`
|
762
|
+
<div class="diff-line diff-line-removed">
|
763
|
+
<span class="diff-line-number">${chunk.oldLineNumber}</span>
|
764
|
+
<span class="diff-line-number">-</span>
|
765
|
+
<span class="diff-line-content">-${this.escapeHtml(chunk.oldLine)}</span>
|
766
|
+
</div>
|
767
|
+
`);
|
768
|
+
} else if (chunk.type === 'added') {
|
769
|
+
lines.push(`
|
770
|
+
<div class="diff-line diff-line-added">
|
771
|
+
<span class="diff-line-number">-</span>
|
772
|
+
<span class="diff-line-number">${chunk.newLineNumber}</span>
|
773
|
+
<span class="diff-line-content">+${this.escapeHtml(chunk.newLine)}</span>
|
774
|
+
</div>
|
775
|
+
`);
|
776
|
+
}
|
777
|
+
}
|
778
|
+
|
779
|
+
return `
|
780
|
+
<div class="diff-unified">
|
781
|
+
${lines.join('')}
|
782
|
+
</div>
|
783
|
+
`;
|
784
|
+
}
|
785
|
+
|
786
|
+
/**
|
787
|
+
* Update operations timeline
|
788
|
+
* @param {Array} operations - File operations
|
789
|
+
*/
|
790
|
+
updateTimeline(operations) {
|
791
|
+
const timeline = document.getElementById('diff-timeline');
|
792
|
+
|
793
|
+
if (!operations || operations.length === 0) {
|
794
|
+
timeline.innerHTML = '<div class="timeline-empty">No operations recorded</div>';
|
795
|
+
return;
|
796
|
+
}
|
797
|
+
|
798
|
+
// Sort operations by timestamp
|
799
|
+
const sortedOps = [...operations].sort((a, b) =>
|
800
|
+
new Date(a.timestamp) - new Date(b.timestamp)
|
801
|
+
);
|
802
|
+
|
803
|
+
timeline.innerHTML = sortedOps.map((op, index) => {
|
804
|
+
const time = new Date(op.timestamp);
|
805
|
+
const timeStr = time.toLocaleTimeString();
|
806
|
+
|
807
|
+
return `
|
808
|
+
<div class="timeline-item ${index === 0 ? 'active' : ''}"
|
809
|
+
data-index="${index}">
|
810
|
+
<div class="timeline-operation">${op.operation}</div>
|
811
|
+
<div class="timeline-time">${timeStr}</div>
|
812
|
+
</div>
|
813
|
+
`;
|
814
|
+
}).join('');
|
815
|
+
|
816
|
+
// Add click handlers
|
817
|
+
timeline.querySelectorAll('.timeline-item').forEach(item => {
|
818
|
+
item.addEventListener('click', (e) => {
|
819
|
+
const index = parseInt(e.currentTarget.dataset.index);
|
820
|
+
this.selectOperation(index);
|
821
|
+
});
|
822
|
+
});
|
823
|
+
}
|
824
|
+
|
825
|
+
/**
|
826
|
+
* Select an operation in the timeline
|
827
|
+
* @param {number} index - Operation index
|
828
|
+
*/
|
829
|
+
selectOperation(index) {
|
830
|
+
// Update active state
|
831
|
+
document.querySelectorAll('.timeline-item').forEach((item, i) => {
|
832
|
+
item.classList.toggle('active', i === index);
|
833
|
+
});
|
834
|
+
|
835
|
+
// Could implement showing diff up to this operation
|
836
|
+
console.log('Selected operation:', index);
|
837
|
+
}
|
838
|
+
|
839
|
+
/**
|
840
|
+
* Update statistics
|
841
|
+
* @param {Object} diffData - Diff data
|
842
|
+
*/
|
843
|
+
updateStatistics(diffData) {
|
844
|
+
const diff = this.computeDiff(
|
845
|
+
diffData.initialContent || '',
|
846
|
+
diffData.currentContent || ''
|
847
|
+
);
|
848
|
+
|
849
|
+
const additions = diff.filter(d => d.type === 'added').length;
|
850
|
+
const deletions = diff.filter(d => d.type === 'removed').length;
|
851
|
+
|
852
|
+
document.querySelector('.diff-stats .additions').textContent = `+${additions}`;
|
853
|
+
document.querySelector('.diff-stats .deletions').textContent = `-${deletions}`;
|
854
|
+
|
855
|
+
const opCount = diffData.operations ? diffData.operations.length : 0;
|
856
|
+
document.querySelector('.op-count').textContent =
|
857
|
+
`${opCount} operation${opCount !== 1 ? 's' : ''}`;
|
858
|
+
}
|
859
|
+
|
860
|
+
/**
|
861
|
+
* Navigate to previous change
|
862
|
+
*/
|
863
|
+
navigateToPreviousChange() {
|
864
|
+
console.log('Navigate to previous change');
|
865
|
+
// Implementation would scroll to previous diff chunk
|
866
|
+
}
|
867
|
+
|
868
|
+
/**
|
869
|
+
* Navigate to next change
|
870
|
+
*/
|
871
|
+
navigateToNextChange() {
|
872
|
+
console.log('Navigate to next change');
|
873
|
+
// Implementation would scroll to next diff chunk
|
874
|
+
}
|
875
|
+
|
876
|
+
/**
|
877
|
+
* Escape HTML for safe display
|
878
|
+
* @param {string} text - Text to escape
|
879
|
+
* @returns {string} Escaped text
|
880
|
+
*/
|
881
|
+
escapeHtml(text) {
|
882
|
+
const div = document.createElement('div');
|
883
|
+
div.textContent = text || '';
|
884
|
+
return div.innerHTML;
|
885
|
+
}
|
886
|
+
}
|
887
|
+
|
888
|
+
// Export for use in other modules
|
889
|
+
if (typeof window !== 'undefined') {
|
890
|
+
window.DiffViewer = DiffViewer;
|
891
|
+
}
|