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.
- package/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- 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, '<').replace(/>/g, '>') || ' '}</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, '<').replace(/>/g, '>')}${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
|
-
});
|