glad-web 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/web/git.js ADDED
@@ -0,0 +1,533 @@
1
+ let currentGitTab = 'changes';
2
+ let currentDirPath = '';
3
+
4
+ function switchGitTab(tab) {
5
+ currentGitTab = tab;
6
+ document.getElementById('tab-changes').classList.toggle('active', tab === 'changes');
7
+ document.getElementById('tab-directories').classList.toggle('active', tab === 'directories');
8
+ document.getElementById('tab-graph').classList.toggle('active', tab === 'graph');
9
+ if (tab === 'changes') {
10
+ loadGitStatus();
11
+ } else if (tab === 'directories') {
12
+ loadDirectories(currentDirPath);
13
+ } else if (tab === 'graph') {
14
+ loadGitGraph();
15
+ }
16
+ }
17
+
18
+
19
+ let currentCommitDiffHTML = '';
20
+ let currentCommitHash = '';
21
+
22
+ window.loadCommitDiff = async function(hash) {
23
+ if (!activeSessionId) return;
24
+ const content = document.getElementById('git-content');
25
+ content.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-dim);">Loading diff...</div>';
26
+ currentCommitHash = hash;
27
+
28
+ try {
29
+ const res = await fetch(`/api/sessions/${activeSessionId}/git-show/${hash}`);
30
+ const data = await res.json();
31
+ if (data.success) {
32
+ let fileBlocks = [];
33
+ let currentBlock = { name: 'Commit Details', lines: [] };
34
+ fileBlocks.push(currentBlock);
35
+
36
+ const lines = data.stdout.split('\n');
37
+ for (const line of lines) {
38
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\//);
39
+ if (diffMatch) {
40
+ currentBlock = { name: diffMatch[1], lines: [] };
41
+ fileBlocks.push(currentBlock);
42
+ }
43
+ currentBlock.lines.push(line);
44
+ }
45
+
46
+ let diffHTML = '';
47
+ for (const block of fileBlocks) {
48
+ if (block.lines.length === 0 || (block.lines.length === 1 && !block.lines[0])) continue;
49
+
50
+ let blockContent = '';
51
+ let addCount = 0;
52
+ let subCount = 0;
53
+ for (const line of block.lines) {
54
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
55
+ if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; addCount++; }
56
+ else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; subCount++; }
57
+ else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
58
+ else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('commit') || line.startsWith('Author') || line.startsWith('Date')) color = '#fff';
59
+
60
+ blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${line.replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' '}</div>`;
61
+ }
62
+
63
+ const isOpen = block.name === 'Commit Details';
64
+ const statHTML = block.name !== 'Commit Details' ? `<span style="margin-left: 12px; font-family: monospace; font-size: 12px;"><span style="color:#4ade80;">+${addCount}</span> <span style="color:#f87171; margin-left:6px;">-${subCount}</span></span>` : '';
65
+ diffHTML += `
66
+ <details ${isOpen ? 'open' : ''} style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; overflow: hidden;">
67
+ <summary style="background: #1e1e1e; padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none;">
68
+ ${block.name === 'Commit Details' ? '📝 ' : '📄 '}${block.name.replace(/</g, '&lt;').replace(/>/g, '&gt;')}${statHTML}
69
+ </summary>
70
+ <div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0;">
71
+ ${blockContent}
72
+ </div>
73
+ </details>
74
+ `;
75
+ }
76
+ currentCommitDiffHTML = diffHTML;
77
+ renderCommitDiffFullView();
78
+ } else {
79
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Error loading diff</p>`;
80
+ }
81
+ } catch (e) {
82
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Network error</p>`;
83
+ }
84
+ };
85
+
86
+ function renderCommitDiffFullView() {
87
+ const content = document.getElementById('git-content');
88
+ let html = `
89
+ <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
90
+ <button class="icon-btn" onclick="switchGitTab('graph')" style="color: var(--primary); margin-right: 12px; font-weight:600; font-size:14px; display:flex; align-items:center;">
91
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> Back
92
+ </button>
93
+ <div style="flex:1; min-width:0;">
94
+ <div style="font-weight:600; font-size:15px; font-family: monospace;">Commit: ${currentCommitHash}</div>
95
+ </div>
96
+ </div>
97
+ <div style="padding: 10px;">
98
+ ${currentCommitDiffHTML}
99
+ </div>`;
100
+ content.innerHTML = html;
101
+ }
102
+
103
+ async function loadGitGraph() {
104
+
105
+ if (!activeSessionId) return;
106
+ const container = document.getElementById('git-content');
107
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-dim);">Loading graph...</div>';
108
+ try {
109
+ const res = await fetch(`/api/sessions/${activeSessionId}/git-log`);
110
+ const data = await res.json();
111
+ if (data.success) {
112
+ const renderer = new GitGraphRenderer(container);
113
+ renderer.render(data.commits);
114
+ } else {
115
+ container.innerHTML = `<div style="padding: 20px; color: #ff3b30;">Error: ${data.error}</div>`;
116
+ }
117
+ } catch (e) {
118
+ container.innerHTML = `<div style="padding: 20px; color: #ff3b30;">Failed to load graph</div>`;
119
+ }
120
+ }
121
+
122
+ async function loadDirectories(path = '') {
123
+ currentDirPath = path;
124
+ const content = document.getElementById('git-content');
125
+ content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading...</p>';
126
+ if (!activeSessionId) return;
127
+ try {
128
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/fs/dir?path=${encodeURIComponent(path)}`);
129
+ const data = await res.json();
130
+ if (!data.success) throw new Error(data.error);
131
+
132
+ let html = '<div>';
133
+ if (path !== '') {
134
+ const parts = path.split('/');
135
+ parts.pop();
136
+ const parentPath = parts.join('/');
137
+ html += `<div class="list-item" onclick="loadDirectories(decodePathValue('${encodePathValue(parentPath)}'))">
138
+ <svg class="dir-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
139
+ <div style="font-size:14px; font-weight:500;">..</div>
140
+ </div>`;
141
+ }
142
+
143
+ if (data.files.length === 0) {
144
+ html += '<p style="color:var(--text-dim); text-align:center; padding: 20px;">Empty directory</p>';
145
+ } else {
146
+ data.files.forEach(f => {
147
+ const fullPath = path ? `${path}/${f.name}` : f.name;
148
+ const encodedPath = encodePathValue(fullPath);
149
+ const escapedName = escapeHtml(f.name);
150
+ let colorStyle = '';
151
+ let badgeHtml = '';
152
+ if (f.gitStatus) {
153
+ let color = '#fff';
154
+ let label = f.gitStatus.trim();
155
+ if (label.includes('U') || label.includes('?')) { colorStyle = 'color: #4ade80;'; color = '#4ade80'; if(label === '??') label = 'U'; }
156
+ else if (label.includes('M')) { colorStyle = 'color: #f59e0b;'; color = '#f59e0b'; }
157
+ else if (label.includes('D')) { colorStyle = 'color: #f87171;'; color = '#f87171'; }
158
+ badgeHtml = `<span style="color:${color}; font-weight:700; font-size:10px; border:1px solid ${color}; padding:1px 4px; border-radius:3px; opacity:0.7; margin-left: auto;">${label}</span>`;
159
+ }
160
+
161
+ if (f.isDirectory) {
162
+ html += `<div class="list-item" onclick="loadDirectories(decodePathValue('${encodedPath}'))">
163
+ <svg class="dir-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>
164
+ <div style="font-size:14px; font-weight:500; ${colorStyle}">${escapedName}</div>
165
+ ${badgeHtml}
166
+ </div>`;
167
+ } else {
168
+ html += `<div class="list-item" onclick="showFileDetails(decodePathValue('${encodedPath}'), false)">
169
+ <svg class="file-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
170
+ <div style="font-size:14px; font-weight:500; ${colorStyle}">${escapedName}</div>
171
+ ${badgeHtml}
172
+ </div>`;
173
+ }
174
+ });
175
+ }
176
+ html += '</div>';
177
+ content.innerHTML = html;
178
+ } catch (e) {
179
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
180
+ }
181
+ }
182
+
183
+ async function loadGitStatus() {
184
+ const content = document.getElementById('git-content');
185
+ content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading...</p>';
186
+ if (!activeSessionId) return;
187
+ try {
188
+ const [resStatus, resUnstaged, resStaged] = await Promise.all([
189
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-status`),
190
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=false`).catch(() => ({ok:false})),
191
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=true`).catch(() => ({ok:false}))
192
+ ]);
193
+
194
+ const data = await resStatus.json();
195
+ if (!data.success) {
196
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Git error: ${data.error}</p>`;
197
+ return;
198
+ }
199
+ const files = Array.isArray(data.files) ? data.files : [];
200
+
201
+ if (files.length === 0) {
202
+ content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">No changes detected.</p>';
203
+ return;
204
+ }
205
+
206
+ let statsMap = {};
207
+ const parseNumstat = (output) => {
208
+ if (!output) return;
209
+ output.split('\n').forEach(line => {
210
+ const parts = line.split('\t');
211
+ if (parts.length >= 3) {
212
+ const added = parseInt(parts[0]) || 0;
213
+ const removed = parseInt(parts[1]) || 0;
214
+ const file = parts.slice(2).join('\t');
215
+ if (!statsMap[file]) statsMap[file] = { added: 0, removed: 0 };
216
+ statsMap[file].added += added;
217
+ statsMap[file].removed += removed;
218
+ }
219
+ });
220
+ };
221
+
222
+ if (resUnstaged.ok) {
223
+ const unstagedData = await resUnstaged.json();
224
+ if (unstagedData.success) parseNumstat(unstagedData.stdout);
225
+ }
226
+ if (resStaged.ok) {
227
+ const stagedData = await resStaged.json();
228
+ if (stagedData.success) parseNumstat(stagedData.stdout);
229
+ }
230
+
231
+ let html = '<div style="padding:10px;">';
232
+ files.forEach((f, idx) => {
233
+ const encodedPath = encodePathValue(f.path);
234
+ const escapedPath = escapeHtml(f.path);
235
+ let color = '#fff';
236
+ let label = f.status;
237
+ if (label.includes('M')) { color = '#f59e0b'; }
238
+ else if (label.includes('A') || label === '??') { color = '#4ade80'; if(label === '??') label = 'U'; }
239
+ else if (label.includes('D')) { color = '#f87171'; }
240
+ const isUntracked = f.status === '??';
241
+ const hasStaged = !isUntracked && f.status[0] && f.status[0] !== ' ';
242
+ const hasUnstaged = !isUntracked && f.status[1] && f.status[1] !== ' ';
243
+
244
+ let statHTML = '';
245
+ if (statsMap[f.path]) {
246
+ const { added, removed } = statsMap[f.path];
247
+ if (added > 0 || removed > 0) {
248
+ statHTML = `<span style="margin-left: 12px; font-family: monospace; font-size: 12px; white-space: nowrap;"><span style="color:#4ade80;">+${added}</span> <span style="color:#f87171; margin-left:6px;">-${removed}</span></span>`;
249
+ }
250
+ }
251
+
252
+ html += `<div style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; background: #1e1e1e; overflow: hidden;">
253
+ <div onclick="toggleInlineDiff('${encodedPath}', 'inline-diff-${idx}', ${!!hasStaged}, ${!!hasUnstaged}, ${isUntracked})" style="padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none; display: flex; align-items: center;">
254
+ <div style="flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">📄 ${escapedPath}${statHTML}</div>
255
+ <span style="color:${color}; font-weight:700; font-size:10px; border:1px solid ${color}; padding:1px 4px; border-radius:3px; opacity:0.8; flex-shrink: 0; margin-left: 8px;">${label}</span>
256
+ </div>
257
+ <div id="inline-diff-${idx}" style="display: none;" data-loaded="false"></div>
258
+ </div>`;
259
+ });
260
+ html += '</div>';
261
+ content.innerHTML = html;
262
+ } catch (e) {
263
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
264
+ }
265
+ }
266
+
267
+ function buildFileDiffUrl(path, staged) {
268
+ return `/api/sessions/${activeSessionId}/git-diff-file?path=${encodeURIComponent(path)}&staged=${staged ? 'true' : 'false'}`;
269
+ }
270
+
271
+ async function loadFileChangeData(path, options = {}) {
272
+ const hasStaged = !!options.hasStaged;
273
+ const hasUnstaged = !!options.hasUnstaged;
274
+ const isUntracked = !!options.isUntracked;
275
+ const diffRequests = [];
276
+
277
+ if (hasStaged) {
278
+ diffRequests.push({
279
+ label: 'Staged changes',
280
+ promise: fetchWithTimeout(buildFileDiffUrl(path, true)).catch(() => ({ok:false}))
281
+ });
282
+ }
283
+ if (hasUnstaged || (!hasStaged && !isUntracked)) {
284
+ diffRequests.push({
285
+ label: 'Unstaged changes',
286
+ promise: fetchWithTimeout(buildFileDiffUrl(path, false)).catch(() => ({ok:false}))
287
+ });
288
+ }
289
+
290
+ const [diffResponses, fileRes] = await Promise.all([
291
+ Promise.all(diffRequests.map(item => item.promise)),
292
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/file?path=${encodeURIComponent(path)}`).catch(() => ({ok:false}))
293
+ ]);
294
+
295
+ const diffParts = [];
296
+ for (let i = 0; i < diffResponses.length; i++) {
297
+ const res = diffResponses[i];
298
+ const data = res.ok ? await res.json() : { success: false };
299
+ if (data.success && data.stdout) {
300
+ diffParts.push({ label: diffRequests[i].label, stdout: data.stdout });
301
+ }
302
+ }
303
+
304
+ const fileData = fileRes.ok ? await fileRes.json() : { success: false };
305
+ const content = fileData.success ? fileData.content : '';
306
+ let diff = diffParts.map(part => (
307
+ diffParts.length > 1 ? `# ${part.label}\n${part.stdout}` : part.stdout
308
+ )).join('\n');
309
+ if (!diff && isUntracked && content) diff = 'Untracked file:\n\n' + content;
310
+
311
+ return {
312
+ diff,
313
+ content,
314
+ renderRawDiff: diffParts.length > 1
315
+ };
316
+ }
317
+
318
+ window.toggleInlineDiff = async function(encodedPath, containerId, hasStaged = false, hasUnstaged = true, isUntracked = false) {
319
+ const container = document.getElementById(containerId);
320
+ if (container.style.display === 'block') {
321
+ container.style.display = 'none';
322
+ return;
323
+ }
324
+ container.style.display = 'block';
325
+ if (container.dataset.loaded === 'true') return;
326
+
327
+ const path = decodePathValue(encodedPath);
328
+ container.innerHTML = '<div style="padding: 10px; color: var(--text-dim); text-align: center; font-size: 12px;">Loading...</div>';
329
+
330
+ try {
331
+ const { diff: currentFileDiff, content: currentFileContent } = await loadFileChangeData(path, {
332
+ hasStaged,
333
+ hasUnstaged,
334
+ isUntracked
335
+ });
336
+
337
+ if (!currentFileDiff && !currentFileContent) {
338
+ container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">No diff available</div>`;
339
+ return;
340
+ }
341
+
342
+ let blockContent = '';
343
+ currentFileDiff.split('\n').forEach(line => {
344
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
345
+ if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
346
+ else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
347
+ else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
348
+
349
+ blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${escapeHtml(line) || ' '}</div>`;
350
+ });
351
+
352
+ container.dataset.loaded = 'true';
353
+ container.innerHTML = `
354
+ <div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0; border-top: 1px solid #333; max-height: 400px;">
355
+ ${blockContent}
356
+ </div>
357
+ <div style="padding: 8px; background: #1a1a1a; border-top: 1px solid #333; text-align: center;">
358
+ <button onclick="showFileDetails(decodePathValue('${encodedPath}'), true, ${!!hasStaged}, ${!!hasUnstaged}, ${!!isUntracked})" style="background: var(--primary); border: none; color: #fff; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
359
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
360
+ View Full File
361
+ </button>
362
+ </div>
363
+ `;
364
+ } catch (e) {
365
+ container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">Error: ${e.message}</div>`;
366
+ }
367
+ };
368
+
369
+ let currentFilePath = '', currentFileDiff = '', currentFileContent = '', currentFileMode = 'diff', currentFileWrap = false, currentFontSize = 12, currentFileRenderRawDiff = false;
370
+
371
+ function changeFontSize(delta) {
372
+ if (delta === 0) currentFontSize = 12;
373
+ else currentFontSize = Math.max(8, Math.min(32, currentFontSize + delta));
374
+ renderFileDetails();
375
+ }
376
+
377
+ async function showFileDetails(path, isFromChanges = true, hasStaged = false, hasUnstaged = true, isUntracked = false) {
378
+ currentFilePath = path;
379
+ currentFileMode = isFromChanges ? 'diff' : 'file';
380
+ currentFileRenderRawDiff = false;
381
+ const content = document.getElementById('git-content');
382
+ content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading details...</p>';
383
+ try {
384
+ const detailData = await loadFileChangeData(path, {
385
+ hasStaged: isFromChanges ? hasStaged : false,
386
+ hasUnstaged: isFromChanges ? hasUnstaged : true,
387
+ isUntracked: isFromChanges ? isUntracked : false
388
+ });
389
+ currentFileDiff = detailData.diff;
390
+ currentFileContent = detailData.content;
391
+ currentFileRenderRawDiff = detailData.renderRawDiff;
392
+ if (!currentFileDiff && !currentFileContent) {
393
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Failed to load details.</p>`;
394
+ return;
395
+ }
396
+ if (!currentFileDiff && !isFromChanges) currentFileMode = 'file';
397
+ renderFileDetails();
398
+ } catch (e) { content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`; }
399
+ }
400
+
401
+ function toggleFileMode() { currentFileMode = currentFileMode === 'diff' ? 'file' : 'diff'; renderFileDetails(); }
402
+ function toggleFileWrap() { currentFileWrap = !currentFileWrap; renderFileDetails(); }
403
+
404
+ function renderFileDetails() {
405
+ const content = document.getElementById('git-content');
406
+ let html = `
407
+ <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
408
+ <button class="icon-btn" onclick="switchGitTab(currentGitTab)" style="color: var(--primary); margin-right: 12px; font-weight:600; font-size:14px; display:flex; align-items:center; flex-shrink: 0;">
409
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> Back
410
+ </button>
411
+ <div style="flex:1; min-width:0; margin-right: 12px;">
412
+ <div style="font-weight:600; font-size:15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(currentFilePath)}">${escapeHtml(currentFilePath.split('/').pop() || currentFilePath)}</div>
413
+ </div>`;
414
+
415
+ if (currentFileDiff && currentFileContent) {
416
+ html += `
417
+ <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
418
+ <div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
419
+ <button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
420
+ <button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
421
+ <button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
422
+ </div>
423
+ <button onclick="toggleFileMode()" style="background: ${currentFileMode === 'diff' ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
424
+ Diff
425
+ </button>
426
+ <button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
427
+ Wrap
428
+ </button>
429
+ </div>`;
430
+ } else {
431
+ html += `
432
+ <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
433
+ <div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
434
+ <button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
435
+ <button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
436
+ <button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
437
+ </div>
438
+ <button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
439
+ Wrap
440
+ </button>
441
+ </div>`;
442
+ }
443
+
444
+ html += `</div>`;
445
+
446
+ const wrapStyle = currentFileWrap ? 'white-space: pre-wrap; word-break: break-all;' : 'white-space: pre;';
447
+ const innerWrapStyle = currentFileWrap ? '' : 'min-width: max-content;';
448
+ html += `<div style="padding: 10px;">`;
449
+ html += `<div style="background:#0d0d0d; border-radius:8px; overflow-x:auto; font-family:monospace; font-size:${currentFontSize}px; line-height:1.5; padding: 12px 0; margin: 0; -webkit-text-size-adjust: 100%; text-size-adjust: 100%;">`;
450
+ html += `<div style="${innerWrapStyle}">`;
451
+
452
+ let mergedLines = [];
453
+ const lines = currentFileContent ? currentFileContent.replace(/\r\n/g, '\n').split('\n') : [];
454
+
455
+ if (currentFileMode === 'diff' && currentFileDiff && !currentFileDiff.startsWith('Untracked file:')) {
456
+ let diffHunks = [];
457
+ let currentHunk = null;
458
+ const diffLines = currentFileDiff.replace(/\r\n/g, '\n').split('\n');
459
+ for (const dl of diffLines) {
460
+ if (dl.startsWith('---') || dl.startsWith('+++')) continue;
461
+ let match = dl.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
462
+ if (match) {
463
+ currentHunk = {
464
+ newStart: parseInt(match[2], 10),
465
+ lines: []
466
+ };
467
+ diffHunks.push(currentHunk);
468
+ } else if (currentHunk) {
469
+ currentHunk.lines.push(dl);
470
+ }
471
+ }
472
+
473
+ if (currentFileRenderRawDiff || diffHunks.length === 0) {
474
+ currentFileDiff.replace(/\r\n/g, '\n').split('\n').forEach(line => {
475
+ mergedLines.push({ type: 'raw', text: line });
476
+ });
477
+ } else {
478
+ let contentLineIdx = 1;
479
+ for (let hunk of diffHunks) {
480
+ while (contentLineIdx < hunk.newStart && contentLineIdx <= lines.length) {
481
+ mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
482
+ contentLineIdx++;
483
+ }
484
+ for (let dl of hunk.lines) {
485
+ if (dl.startsWith('-')) {
486
+ mergedLines.push({ type: 'deleted', text: dl.substring(1) });
487
+ } else if (dl.startsWith('+')) {
488
+ mergedLines.push({ type: 'added', text: dl.substring(1), lineNum: contentLineIdx });
489
+ contentLineIdx++;
490
+ } else if (dl.startsWith(' ')) {
491
+ mergedLines.push({ type: 'normal', text: dl.substring(1), lineNum: contentLineIdx });
492
+ contentLineIdx++;
493
+ }
494
+ }
495
+ }
496
+ while (contentLineIdx <= lines.length) {
497
+ mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
498
+ contentLineIdx++;
499
+ }
500
+ }
501
+ } else {
502
+ lines.forEach((line, idx) => {
503
+ mergedLines.push({ type: 'normal', text: line, lineNum: idx + 1 });
504
+ });
505
+ }
506
+
507
+ mergedLines.forEach(item => {
508
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
509
+ let prefix = ' ';
510
+ if (item.type === 'added') { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; prefix = '+'; }
511
+ else if (item.type === 'deleted') { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; prefix = '-'; }
512
+ else if (item.type === 'raw') {
513
+ if (item.text.startsWith('+') && !item.text.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
514
+ else if (item.text.startsWith('-') && !item.text.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
515
+ else if (item.text.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
516
+ prefix = '';
517
+ }
518
+
519
+ const renderedText = item.type === 'raw' ? item.text : `${prefix} ${item.text}`;
520
+ html += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding: 0 12px 0 8px; display:flex;">`;
521
+ html += `<div style="color:#555; margin-right:16px; user-select:none; flex-shrink:0; width: 36px; text-align:right;">${item.lineNum || ''}</div>`;
522
+ html += `<div style="flex:1; min-width:0; ${wrapStyle}">${escapeHtml(renderedText) || ' '}</div>`;
523
+ html += `</div>`;
524
+ });
525
+
526
+ html += `</div></div></div>`;
527
+ content.innerHTML = html;
528
+ }
529
+
530
+ document.addEventListener('DOMContentLoaded', () => {
531
+ refreshSessionsNow();
532
+ document.addEventListener('visibilitychange', refreshSessionsNow);
533
+ });