glad-web 1.0.45 → 2.0.1

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 (68) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -58
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1578
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -586
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -89
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/usage.js +0 -23
  37. package/lib/server/routes/workspace.js +0 -77
  38. package/lib/session/buffer.js +0 -102
  39. package/lib/session/file-attachment-store.js +0 -168
  40. package/lib/session/pty-manager.js +0 -255
  41. package/lib/session/rendered-history.js +0 -225
  42. package/lib/session/session-manager.js +0 -1001
  43. package/lib/session/text-history.js +0 -274
  44. package/lib/usage/ccusage-runner.js +0 -128
  45. package/lib/usage/source-catalog.js +0 -26
  46. package/lib/usage/usage-service.js +0 -226
  47. package/lib/utils/logger.js +0 -74
  48. package/lib/utils/pid.js +0 -67
  49. package/lib/utils/validation.js +0 -53
  50. package/lib/web/claude.js +0 -1129
  51. package/lib/web/codex.js +0 -1042
  52. package/lib/web/composer.js +0 -463
  53. package/lib/web/core.js +0 -373
  54. package/lib/web/git.js +0 -535
  55. package/lib/web/gitgraph.js +0 -293
  56. package/lib/web/index.html +0 -516
  57. package/lib/web/layout.js +0 -72
  58. package/lib/web/notifications.js +0 -163
  59. package/lib/web/schedules.js +0 -245
  60. package/lib/web/session.js +0 -360
  61. package/lib/web/shell.js +0 -59
  62. package/lib/web/styles.css +0 -905
  63. package/lib/web/terminal-scroll.js +0 -81
  64. package/lib/web/theme.js +0 -60
  65. package/lib/web/timed-inputs.js +0 -216
  66. package/lib/web/usage.js +0 -323
  67. package/lib/workspace/service.js +0 -77
  68. package/scripts/check-syntax.js +0 -26
package/lib/web/git.js DELETED
@@ -1,535 +0,0 @@
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 = 'var(--git-diff-text)', bg = 'transparent', borderLeft = '2px solid transparent';
55
- if (line.startsWith('+') && !line.startsWith('+++')) { color = 'var(--git-add-text)'; bg = 'var(--git-add-bg)'; borderLeft = '2px solid var(--git-add-text)'; addCount++; }
56
- else if (line.startsWith('-') && !line.startsWith('---')) { color = 'var(--git-del-text)'; bg = 'var(--git-del-bg)'; borderLeft = '2px solid var(--git-del-text)'; subCount++; }
57
- else if (line.startsWith('@@')) { color = 'var(--git-hunk-text)'; bg = 'var(--git-hunk-bg)'; }
58
- else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('commit') || line.startsWith('Author') || line.startsWith('Date')) color = 'var(--text)';
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:var(--git-add-text);">+${addCount}</span> <span style="color:var(--git-del-text); margin-left:6px;">-${subCount}</span></span>` : '';
65
- diffHTML += `
66
- <details ${isOpen ? 'open' : ''} class="git-diff-panel">
67
- <summary class="git-diff-summary">
68
- ${block.name === 'Commit Details' ? '📝 ' : '📄 '}${block.name.replace(/</g, '&lt;').replace(/>/g, '&gt;')}${statHTML}
69
- </summary>
70
- <div class="git-diff-body">
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 var(--line); 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 = 'var(--text)';
154
- let label = f.gitStatus.trim();
155
- if (label.includes('U') || label.includes('?')) { colorStyle = 'color: var(--git-add-text);'; color = 'var(--git-add-text)'; if(label === '??') label = 'U'; }
156
- else if (label.includes('M')) { colorStyle = 'color: var(--git-modified-text);'; color = 'var(--git-modified-text)'; }
157
- else if (label.includes('D')) { colorStyle = 'color: var(--git-del-text);'; color = 'var(--git-del-text)'; }
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 statusClass = 'modified';
236
- let label = f.status;
237
- if (label.includes('A') || label === '??') { statusClass = 'added'; if(label === '??') label = 'U'; }
238
- else if (label.includes('D')) { statusClass = 'deleted'; }
239
- const isUntracked = f.status === '??';
240
- const hasStaged = !isUntracked && f.status[0] && f.status[0] !== ' ';
241
- const hasUnstaged = !isUntracked && f.status[1] && f.status[1] !== ' ';
242
-
243
- let statHTML = '';
244
- if (statsMap[f.path]) {
245
- const { added, removed } = statsMap[f.path];
246
- if (added > 0 || removed > 0) {
247
- statHTML = `<span style="margin-left: 12px; font-family: monospace; font-size: 12px; white-space: nowrap;"><span style="color:var(--git-add-text);">+${added}</span> <span style="color:var(--git-del-text); margin-left:6px;">-${removed}</span></span>`;
248
- }
249
- }
250
-
251
- html += `<div class="git-change-card">
252
- <div class="git-change-header" onclick="toggleInlineDiff('${encodedPath}', 'inline-diff-${idx}', ${!!hasStaged}, ${!!hasUnstaged}, ${isUntracked})">
253
- <div class="git-change-path">📄 ${escapedPath}${statHTML}</div>
254
- <span class="git-status-badge ${statusClass}">${label}</span>
255
- </div>
256
- <div id="inline-diff-${idx}" style="display: none;" data-loaded="false"></div>
257
- </div>`;
258
- });
259
- html += '</div>';
260
- content.innerHTML = html;
261
- } catch (e) {
262
- content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
263
- }
264
- }
265
-
266
- function buildFileDiffUrl(path, staged) {
267
- return `/api/sessions/${activeSessionId}/git-diff-file?path=${encodeURIComponent(path)}&staged=${staged ? 'true' : 'false'}`;
268
- }
269
-
270
- async function loadFileChangeData(path, options = {}) {
271
- const hasStaged = !!options.hasStaged;
272
- const hasUnstaged = !!options.hasUnstaged;
273
- const isUntracked = !!options.isUntracked;
274
- const diffRequests = [];
275
-
276
- if (hasStaged) {
277
- diffRequests.push({
278
- label: 'Staged changes',
279
- promise: fetchWithTimeout(buildFileDiffUrl(path, true)).catch(() => ({ok:false}))
280
- });
281
- }
282
- if (hasUnstaged || (!hasStaged && !isUntracked)) {
283
- diffRequests.push({
284
- label: 'Unstaged changes',
285
- promise: fetchWithTimeout(buildFileDiffUrl(path, false)).catch(() => ({ok:false}))
286
- });
287
- }
288
-
289
- const [diffResponses, fileRes] = await Promise.all([
290
- Promise.all(diffRequests.map(item => item.promise)),
291
- fetchWithTimeout(`/api/sessions/${activeSessionId}/file?path=${encodeURIComponent(path)}`).catch(() => ({ok:false}))
292
- ]);
293
-
294
- const diffParts = [];
295
- for (let i = 0; i < diffResponses.length; i++) {
296
- const res = diffResponses[i];
297
- const data = res.ok ? await res.json() : { success: false };
298
- if (data.success && data.stdout) {
299
- diffParts.push({ label: diffRequests[i].label, stdout: data.stdout });
300
- }
301
- }
302
-
303
- const fileData = fileRes.ok ? await fileRes.json() : { success: false };
304
- const content = fileData.success ? fileData.content : '';
305
- let diff = diffParts.map(part => (
306
- diffParts.length > 1 ? `# ${part.label}\n${part.stdout}` : part.stdout
307
- )).join('\n');
308
- if (!diff && isUntracked && content) diff = 'Untracked file:\n\n' + content;
309
-
310
- return {
311
- diff,
312
- content,
313
- renderRawDiff: diffParts.length > 1
314
- };
315
- }
316
-
317
- window.toggleInlineDiff = async function(encodedPath, containerId, hasStaged = false, hasUnstaged = true, isUntracked = false) {
318
- const container = document.getElementById(containerId);
319
- if (container.style.display === 'block') {
320
- container.style.display = 'none';
321
- return;
322
- }
323
- container.style.display = 'block';
324
- if (container.dataset.loaded === 'true') return;
325
-
326
- const path = decodePathValue(encodedPath);
327
- container.innerHTML = '<div style="padding: 10px; color: var(--text-dim); text-align: center; font-size: 12px;">Loading...</div>';
328
-
329
- try {
330
- const { diff: currentFileDiff, content: currentFileContent } = await loadFileChangeData(path, {
331
- hasStaged,
332
- hasUnstaged,
333
- isUntracked
334
- });
335
-
336
- if (!currentFileDiff && !currentFileContent) {
337
- container.innerHTML = `<div style="padding: 10px; color: var(--git-del-text); font-size: 12px; text-align:center;">No diff available</div>`;
338
- return;
339
- }
340
-
341
- let blockContent = '';
342
- currentFileDiff.split('\n').forEach(line => {
343
- let color = 'var(--git-diff-text)', bg = 'transparent', borderLeft = '2px solid transparent';
344
- if (line.startsWith('+') && !line.startsWith('+++')) { color = 'var(--git-add-text)'; bg = 'var(--git-add-bg)'; borderLeft = '2px solid var(--git-add-text)'; }
345
- else if (line.startsWith('-') && !line.startsWith('---')) { color = 'var(--git-del-text)'; bg = 'var(--git-del-bg)'; borderLeft = '2px solid var(--git-del-text)'; }
346
- else if (line.startsWith('@@')) { color = 'var(--git-hunk-text)'; bg = 'var(--git-hunk-bg)'; }
347
-
348
- blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${escapeHtml(line) || ' '}</div>`;
349
- });
350
-
351
- container.dataset.loaded = 'true';
352
- container.innerHTML = `
353
- <div class="git-inline-diff git-inline-diff-body">
354
- ${blockContent}
355
- </div>
356
- <div class="git-inline-diff-footer">
357
- <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;">
358
- <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>
359
- View Full File
360
- </button>
361
- </div>
362
- `;
363
- } catch (e) {
364
- container.innerHTML = `<div style="padding: 10px; color: var(--git-del-text); font-size: 12px; text-align:center;">Error: ${e.message}</div>`;
365
- }
366
- };
367
-
368
- let currentFilePath = '', currentFileDiff = '', currentFileContent = '', currentFileMode = 'diff', currentFileWrap = false, currentFontSize = 12, currentFileRenderRawDiff = false;
369
-
370
- function changeFontSize(delta) {
371
- if (delta === 0) currentFontSize = 12;
372
- else currentFontSize = Math.max(8, Math.min(32, currentFontSize + delta));
373
- renderFileDetails();
374
- }
375
-
376
- async function showFileDetails(path, isFromChanges = true, hasStaged = false, hasUnstaged = true, isUntracked = false) {
377
- currentFilePath = path;
378
- currentFileMode = isFromChanges ? 'diff' : 'file';
379
- currentFileRenderRawDiff = false;
380
- const content = document.getElementById('git-content');
381
- content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading details...</p>';
382
- try {
383
- const detailData = await loadFileChangeData(path, {
384
- hasStaged: isFromChanges ? hasStaged : false,
385
- hasUnstaged: isFromChanges ? hasUnstaged : true,
386
- isUntracked: isFromChanges ? isUntracked : false
387
- });
388
- currentFileDiff = detailData.diff;
389
- currentFileContent = detailData.content;
390
- currentFileRenderRawDiff = detailData.renderRawDiff;
391
- if (!currentFileDiff && !currentFileContent) {
392
- content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Failed to load details.</p>`;
393
- return;
394
- }
395
- if (!currentFileDiff && !isFromChanges) currentFileMode = 'file';
396
- renderFileDetails();
397
- } catch (e) { content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`; }
398
- }
399
-
400
- function toggleFileMode() { currentFileMode = currentFileMode === 'diff' ? 'file' : 'diff'; renderFileDetails(); }
401
- function toggleFileWrap() { currentFileWrap = !currentFileWrap; renderFileDetails(); }
402
-
403
- function renderFileDetails() {
404
- const content = document.getElementById('git-content');
405
- let html = `
406
- <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 10;">
407
- <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;">
408
- <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
409
- </button>
410
- <div style="flex:1; min-width:0; margin-right: 12px;">
411
- <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>
412
- </div>`;
413
-
414
- if (currentFileDiff && currentFileContent) {
415
- html += `
416
- <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
417
- <div class="git-file-toolbar-group">
418
- <button class="git-file-toolbar-button" onclick="changeFontSize(-1)" title="Zoom Out">A-</button>
419
- <button class="git-file-toolbar-button" onclick="changeFontSize(0)" title="Reset Size">${currentFontSize}</button>
420
- <button class="git-file-toolbar-button" onclick="changeFontSize(1)" title="Zoom In">A+</button>
421
- </div>
422
- <button class="git-file-mode-button${currentFileMode === 'diff' ? ' active' : ''}" onclick="toggleFileMode()">
423
- Diff
424
- </button>
425
- <button class="git-file-mode-button${currentFileWrap ? ' active' : ''}" onclick="toggleFileWrap()">
426
- Wrap
427
- </button>
428
- </div>`;
429
- } else {
430
- html += `
431
- <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
432
- <div class="git-file-toolbar-group">
433
- <button class="git-file-toolbar-button" onclick="changeFontSize(-1)" title="Zoom Out">A-</button>
434
- <button class="git-file-toolbar-button" onclick="changeFontSize(0)" title="Reset Size">${currentFontSize}</button>
435
- <button class="git-file-toolbar-button" onclick="changeFontSize(1)" title="Zoom In">A+</button>
436
- </div>
437
- <button class="git-file-mode-button${currentFileWrap ? ' active' : ''}" onclick="toggleFileWrap()">
438
- Wrap
439
- </button>
440
- </div>`;
441
- }
442
-
443
- html += `</div>`;
444
-
445
- const wrapStyle = currentFileWrap ? 'white-space: pre-wrap; word-break: break-all;' : 'white-space: pre;';
446
- const innerWrapStyle = currentFileWrap ? '' : 'min-width: max-content;';
447
- html += `<div style="padding: 10px;">`;
448
- html += `<div class="git-file-code" style="font-size:${currentFontSize}px; line-height:1.5; -webkit-text-size-adjust:100%; text-size-adjust:100%;">`;
449
- html += `<div style="${innerWrapStyle}">`;
450
-
451
- let mergedLines = [];
452
- const lines = currentFileContent ? currentFileContent.replace(/\r\n/g, '\n').split('\n') : [];
453
-
454
- if (currentFileMode === 'diff' && currentFileDiff && !currentFileDiff.startsWith('Untracked file:')) {
455
- let diffHunks = [];
456
- let currentHunk = null;
457
- const diffLines = currentFileDiff.replace(/\r\n/g, '\n').split('\n');
458
- for (const dl of diffLines) {
459
- if (dl.startsWith('---') || dl.startsWith('+++')) continue;
460
- let match = dl.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
461
- if (match) {
462
- currentHunk = {
463
- newStart: parseInt(match[2], 10),
464
- lines: []
465
- };
466
- diffHunks.push(currentHunk);
467
- } else if (currentHunk) {
468
- currentHunk.lines.push(dl);
469
- }
470
- }
471
-
472
- if (currentFileRenderRawDiff || diffHunks.length === 0) {
473
- currentFileDiff.replace(/\r\n/g, '\n').split('\n').forEach(line => {
474
- mergedLines.push({ type: 'raw', text: line });
475
- });
476
- } else {
477
- let contentLineIdx = 1;
478
- for (let hunk of diffHunks) {
479
- while (contentLineIdx < hunk.newStart && contentLineIdx <= lines.length) {
480
- mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
481
- contentLineIdx++;
482
- }
483
- for (let dl of hunk.lines) {
484
- if (dl.startsWith('-')) {
485
- mergedLines.push({ type: 'deleted', text: dl.substring(1) });
486
- } else if (dl.startsWith('+')) {
487
- mergedLines.push({ type: 'added', text: dl.substring(1), lineNum: contentLineIdx });
488
- contentLineIdx++;
489
- } else if (dl.startsWith(' ')) {
490
- mergedLines.push({ type: 'normal', text: dl.substring(1), lineNum: contentLineIdx });
491
- contentLineIdx++;
492
- }
493
- }
494
- }
495
- while (contentLineIdx <= lines.length) {
496
- mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
497
- contentLineIdx++;
498
- }
499
- }
500
- } else {
501
- lines.forEach((line, idx) => {
502
- mergedLines.push({ type: 'normal', text: line, lineNum: idx + 1 });
503
- });
504
- }
505
-
506
- mergedLines.forEach(item => {
507
- let color = 'var(--git-diff-text)', bg = 'transparent', borderLeft = '2px solid transparent';
508
- let prefix = ' ';
509
- if (item.type === 'added') { color = 'var(--git-add-text)'; bg = 'var(--git-add-bg)'; borderLeft = '2px solid var(--git-add-text)'; prefix = '+'; }
510
- else if (item.type === 'deleted') { color = 'var(--git-del-text)'; bg = 'var(--git-del-bg)'; borderLeft = '2px solid var(--git-del-text)'; prefix = '-'; }
511
- else if (item.type === 'raw') {
512
- if (item.text.startsWith('+') && !item.text.startsWith('+++')) { color = 'var(--git-add-text)'; bg = 'var(--git-add-bg)'; borderLeft = '2px solid var(--git-add-text)'; }
513
- else if (item.text.startsWith('-') && !item.text.startsWith('---')) { color = 'var(--git-del-text)'; bg = 'var(--git-del-bg)'; borderLeft = '2px solid var(--git-del-text)'; }
514
- else if (item.text.startsWith('@@')) { color = 'var(--git-hunk-text)'; bg = 'var(--git-hunk-bg)'; }
515
- prefix = '';
516
- }
517
-
518
- const renderedText = item.type === 'raw' ? item.text : `${prefix} ${item.text}`;
519
- html += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding: 0 12px 0 8px; display:flex;">`;
520
- html += `<div class="git-line-number">${item.lineNum || ''}</div>`;
521
- html += `<div style="flex:1; min-width:0; ${wrapStyle}">${escapeHtml(renderedText) || ' '}</div>`;
522
- html += `</div>`;
523
- });
524
-
525
- html += `</div></div></div>`;
526
- content.innerHTML = html;
527
- }
528
-
529
- document.addEventListener('DOMContentLoaded', () => {
530
- refreshSessionsNow();
531
- document.addEventListener('visibilitychange', refreshSessionsNow);
532
- });
533
- window.addEventListener('glad-theme-change', () => {
534
- if (document.getElementById('git-view').classList.contains('active')) switchGitTab(currentGitTab);
535
- });