glad-web 1.0.46 → 2.0.2

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 (74) 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 -61
  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 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  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/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. 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
- });