glad-web 1.0.17 → 1.0.18

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.
@@ -244,6 +244,15 @@ async function webCommand(options) {
244
244
  res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
245
245
  });
246
246
 
247
+ // API: Git Branch Name for Commit
248
+ app.get('/api/sessions/:id/git-branch/:hash', async (req, res) => {
249
+ const session = sessionManager.get(req.params.id);
250
+ if (!session) return res.status(404).json({ error: 'Session not found' });
251
+ const hash = req.params.hash;
252
+ const result = await gitService.nameRev(session.ptyManager.workingDir, hash);
253
+ res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
254
+ });
255
+
247
256
  // API: Git Log
248
257
  app.get('/api/sessions/:id/git-log', async (req, res) => {
249
258
  const session = sessionManager.get(req.params.id);
@@ -70,6 +70,10 @@ class GitService {
70
70
  : ['diff', '--no-ext-diff', '--', filePath];
71
71
  return execFilePromise('git', args, cwd);
72
72
  }
73
+
74
+ async nameRev(cwd, hash) {
75
+ return execFilePromise('git', ['name-rev', '--name-only', '--exclude=tags/*', hash], cwd);
76
+ }
73
77
  }
74
78
 
75
79
  module.exports = {
@@ -175,13 +175,14 @@ class GitGraphRenderer {
175
175
 
176
176
  let detailsHTML = `
177
177
  <div style="display:flex; justify-content:space-between; margin-bottom:12px;">
178
- <div>
179
- <div style="font-weight:600; font-size:14px; margin-bottom:4px;">${commit.subject.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>
178
+ <div style="flex: 1; padding-right: 16px; min-width: 0;">
179
+ <div style="font-weight:600; font-size:14px; margin-bottom:4px; word-break: break-word;">${commit.subject.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>
180
180
  <div style="color:var(--text-dim);">${commit.author} commited ${commit.time}</div>
181
+ <div id="branch-info-${commit.hash}" style="font-family:monospace; color:var(--primary); margin-top:6px; font-size:12px; font-weight:500;"></div>
181
182
  </div>
182
- <div style="text-align:right;">
183
+ <div style="text-align:right; flex-shrink: 0;">
183
184
  <div style="font-family:monospace; color:var(--text-dim);">Commit: ${commit.hash}</div>
184
- ${commit.parents.length > 0 ? `<div style="font-family:monospace; color:var(--text-dim);">Parents: ${commit.parents.join(', ')}</div>` : ''}
185
+ ${commit.parents.length > 0 ? `<div style="font-family:monospace; color:var(--text-dim); margin-top:2px;">Parents: ${commit.parents.join(', ')}</div>` : ''}
185
186
  </div>
186
187
  </div>
187
188
  `;
@@ -189,13 +190,32 @@ class GitGraphRenderer {
189
190
  // Add diff placeholder
190
191
  detailsHTML += `
191
192
  <div style="border-top:1px solid #333; padding-top:12px; margin-top:12px;">
192
- <button onclick="window.loadCommitDiff('${commit.hash}', this)" style="background:var(--primary); border:none; color:#fff; padding:6px 12px; border-radius:4px; font-size:12px; cursor:pointer;">Load Diff</button>
193
- <div class="diff-container" style="margin-top:12px; font-family:monospace; font-size:12px; white-space:pre-wrap; overflow-x:auto;"></div>
193
+ <button onclick="window.loadCommitDiff('${commit.hash}')" style="background:var(--primary); border:none; color:#fff; padding:6px 12px; border-radius:4px; font-size:12px; cursor:pointer;">View Full Diff</button>
194
194
  </div>
195
195
  `;
196
196
 
197
197
  detailsDiv.innerHTML = detailsHTML;
198
198
  rowDiv.parentNode.insertBefore(detailsDiv, rowDiv.nextSibling);
199
+
200
+ // Fetch branch info
201
+ if (window.activeSessionId) {
202
+ const branchContainer = document.getElementById(`branch-info-${commit.hash}`);
203
+ if (branchContainer) {
204
+ branchContainer.textContent = 'Loading branch...';
205
+ fetch(`/api/sessions/${window.activeSessionId}/git-branch/${commit.hash}`)
206
+ .then(res => res.json())
207
+ .then(data => {
208
+ if (data.success && data.stdout && data.stdout.trim()) {
209
+ branchContainer.textContent = `Branch: ${data.stdout.trim()}`;
210
+ } else {
211
+ branchContainer.textContent = '';
212
+ }
213
+ })
214
+ .catch(() => {
215
+ branchContainer.textContent = '';
216
+ });
217
+ }
218
+ }
199
219
  };
200
220
 
201
221
  rowDiv.appendChild(contentDiv);
@@ -883,6 +883,7 @@
883
883
 
884
884
  function joinSession(id, sessionName, toolKey = null) {
885
885
  activeSessionId = id;
886
+ window.activeSessionId = id;
886
887
  activeToolKey = toolKey;
887
888
  clearTimeout(sessionPollTimer);
888
889
  sessionPollTimer = null;
@@ -1283,34 +1284,90 @@
1283
1284
  }
1284
1285
 
1285
1286
 
1286
- window.loadCommitDiff = async function(hash, btn) {
1287
+ let currentCommitDiffHTML = '';
1288
+ let currentCommitHash = '';
1289
+
1290
+ window.loadCommitDiff = async function(hash) {
1287
1291
  if (!activeSessionId) return;
1288
- const container = btn.nextElementSibling;
1289
- btn.style.display = 'none';
1290
- container.innerHTML = '<span style="color:var(--text-dim);">Loading diff...</span>';
1292
+ const content = document.getElementById('git-content');
1293
+ content.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-dim);">Loading diff...</div>';
1294
+ currentCommitHash = hash;
1295
+
1291
1296
  try {
1292
1297
  const res = await fetch(`/api/sessions/${activeSessionId}/git-show/${hash}`);
1293
1298
  const data = await res.json();
1294
1299
  if (data.success) {
1295
- let diffHTML = '';
1300
+ let fileBlocks = [];
1301
+ let currentBlock = { name: 'Commit Details', lines: [] };
1302
+ fileBlocks.push(currentBlock);
1303
+
1296
1304
  const lines = data.stdout.split('\n');
1297
1305
  for (const line of lines) {
1298
- let color = '#ccc';
1299
- if (line.startsWith('+') && !line.startsWith('+++')) color = '#34c759';
1300
- else if (line.startsWith('-') && !line.startsWith('---')) color = '#ff3b30';
1301
- else if (line.startsWith('@@')) color = '#5ac8fa';
1302
- else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('commit') || line.startsWith('Author') || line.startsWith('Date')) color = '#fff';
1303
- diffHTML += `<div style="color:${color}; padding:0 4px; border-radius:2px;">${line.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`;
1306
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\//);
1307
+ if (diffMatch) {
1308
+ currentBlock = { name: diffMatch[1], lines: [] };
1309
+ fileBlocks.push(currentBlock);
1310
+ }
1311
+ currentBlock.lines.push(line);
1304
1312
  }
1305
- container.innerHTML = diffHTML;
1313
+
1314
+ let diffHTML = '';
1315
+ for (const block of fileBlocks) {
1316
+ if (block.lines.length === 0 || (block.lines.length === 1 && !block.lines[0])) continue;
1317
+
1318
+ let blockContent = '';
1319
+ let addCount = 0;
1320
+ let subCount = 0;
1321
+ for (const line of block.lines) {
1322
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
1323
+ if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; addCount++; }
1324
+ else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; subCount++; }
1325
+ else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
1326
+ else if (line.startsWith('diff') || line.startsWith('index') || line.startsWith('commit') || line.startsWith('Author') || line.startsWith('Date')) color = '#fff';
1327
+
1328
+ 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>`;
1329
+ }
1330
+
1331
+ const isOpen = block.name === 'Commit Details';
1332
+ const statHTML = block.name !== 'Commit Details' ? `<span style="margin-left: 12px; font-family: monospace; font-size: 12px;"><span style="color:#4ade80;">+${addCount}</span> <span style="color:#f87171; margin-left:6px;">-${subCount}</span></span>` : '';
1333
+ diffHTML += `
1334
+ <details ${isOpen ? 'open' : ''} style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; overflow: hidden;">
1335
+ <summary style="background: #1e1e1e; padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none;">
1336
+ ${block.name === 'Commit Details' ? '📝 ' : '📄 '}${block.name.replace(/</g, '&lt;').replace(/>/g, '&gt;')}${statHTML}
1337
+ </summary>
1338
+ <div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0;">
1339
+ ${blockContent}
1340
+ </div>
1341
+ </details>
1342
+ `;
1343
+ }
1344
+ currentCommitDiffHTML = diffHTML;
1345
+ renderCommitDiffFullView();
1306
1346
  } else {
1307
- container.innerHTML = `<span style="color:#ff3b30;">Error loading diff</span>`;
1347
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Error loading diff</p>`;
1308
1348
  }
1309
1349
  } catch (e) {
1310
- container.innerHTML = `<span style="color:#ff3b30;">Network error</span>`;
1350
+ content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Network error</p>`;
1311
1351
  }
1312
1352
  };
1313
1353
 
1354
+ function renderCommitDiffFullView() {
1355
+ const content = document.getElementById('git-content');
1356
+ let html = `
1357
+ <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
1358
+ <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;">
1359
+ <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
1360
+ </button>
1361
+ <div style="flex:1; min-width:0;">
1362
+ <div style="font-weight:600; font-size:15px; font-family: monospace;">Commit: ${currentCommitHash}</div>
1363
+ </div>
1364
+ </div>
1365
+ <div style="padding: 10px;">
1366
+ ${currentCommitDiffHTML}
1367
+ </div>`;
1368
+ content.innerHTML = html;
1369
+ }
1370
+
1314
1371
  async function loadGitGraph() {
1315
1372
 
1316
1373
  if (!activeSessionId) return;
@@ -1396,8 +1453,13 @@
1396
1453
  content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading...</p>';
1397
1454
  if (!activeSessionId) return;
1398
1455
  try {
1399
- const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/git-status`);
1400
- const data = await res.json();
1456
+ const [resStatus, resUnstaged, resStaged] = await Promise.all([
1457
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-status`),
1458
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=false`).catch(() => ({ok:false})),
1459
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-numstat?staged=true`).catch(() => ({ok:false}))
1460
+ ]);
1461
+
1462
+ const data = await resStatus.json();
1401
1463
  if (!data.success) {
1402
1464
  content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Git error: ${data.error}</p>`;
1403
1465
  return;
@@ -1409,92 +1471,327 @@
1409
1471
  return;
1410
1472
  }
1411
1473
 
1412
- let html = '<div style="padding:10px;"><div style="background:var(--card-bg); border-radius:12px; overflow:hidden;">';
1474
+ let statsMap = {};
1475
+ const parseNumstat = (output) => {
1476
+ if (!output) return;
1477
+ output.split('\n').forEach(line => {
1478
+ const parts = line.split('\t');
1479
+ if (parts.length >= 3) {
1480
+ const added = parseInt(parts[0]) || 0;
1481
+ const removed = parseInt(parts[1]) || 0;
1482
+ const file = parts.slice(2).join('\t');
1483
+ if (!statsMap[file]) statsMap[file] = { added: 0, removed: 0 };
1484
+ statsMap[file].added += added;
1485
+ statsMap[file].removed += removed;
1486
+ }
1487
+ });
1488
+ };
1489
+
1490
+ if (resUnstaged.ok) {
1491
+ const unstagedData = await resUnstaged.json();
1492
+ if (unstagedData.success) parseNumstat(unstagedData.stdout);
1493
+ }
1494
+ if (resStaged.ok) {
1495
+ const stagedData = await resStaged.json();
1496
+ if (stagedData.success) parseNumstat(stagedData.stdout);
1497
+ }
1498
+
1499
+ let html = '<div style="padding:10px;">';
1413
1500
  files.forEach((f, idx) => {
1414
1501
  const encodedPath = encodePathValue(f.path);
1415
1502
  const escapedPath = escapeHtml(f.path);
1416
- const escapedBaseName = escapeHtml(f.path.split('/').pop() || f.path);
1417
1503
  let color = '#fff';
1418
1504
  let label = f.status;
1419
- if (label.includes('M')) { color = '#f59e0b'; }
1505
+ if (label.includes('M')) { color = '#f59e0b'; }
1420
1506
  else if (label.includes('A') || label === '??') { color = '#4ade80'; if(label === '??') label = 'U'; }
1421
1507
  else if (label.includes('D')) { color = '#f87171'; }
1422
- const borderBottom = idx < files.length - 1 ? 'border-bottom: 1px solid rgba(255,255,255,0.05);' : '';
1423
- html += `<div style="padding:14px; ${borderBottom} display:flex; justify-content:space-between; align-items:center; cursor:pointer;" onclick="showFileDetails(decodePathValue('${encodedPath}'), true)">
1424
- <div style="flex:1; min-width:0; margin-right:10px;">
1425
- <div style="font-size:14px; font-weight:500;">${escapedBaseName}</div>
1426
- <div style="font-size:12px; color:var(--text-dim);">${escapedPath}</div>
1508
+ const isUntracked = f.status === '??';
1509
+ const hasStaged = !isUntracked && f.status[0] && f.status[0] !== ' ';
1510
+ const hasUnstaged = !isUntracked && f.status[1] && f.status[1] !== ' ';
1511
+
1512
+ let statHTML = '';
1513
+ if (statsMap[f.path]) {
1514
+ const { added, removed } = statsMap[f.path];
1515
+ if (added > 0 || removed > 0) {
1516
+ statHTML = `<span style="margin-left: 12px; font-family: monospace; font-size: 12px; white-space: nowrap;"><span style="color:#4ade80;">+${added}</span> <span style="color:#f87171; margin-left:6px;">-${removed}</span></span>`;
1517
+ }
1518
+ }
1519
+
1520
+ html += `<div style="margin-bottom: 8px; border: 1px solid #333; border-radius: 4px; background: #1e1e1e; overflow: hidden;">
1521
+ <div onclick="toggleInlineDiff('${encodedPath}', 'inline-diff-${idx}', ${!!hasStaged}, ${!!hasUnstaged}, ${isUntracked})" style="padding: 6px 10px; cursor: pointer; color: #fff; font-weight: 500; font-size: 13px; outline: none; user-select: none; display: flex; align-items: center;">
1522
+ <div style="flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">📄 ${escapedPath}${statHTML}</div>
1523
+ <span style="color:${color}; font-weight:700; font-size:10px; border:1px solid ${color}; padding:1px 4px; border-radius:3px; opacity:0.8; flex-shrink: 0; margin-left: 8px;">${label}</span>
1427
1524
  </div>
1428
- <span style="color:${color}; font-weight:700; font-size:12px; border:1px solid ${color}; padding:2px 6px; border-radius:4px; opacity:0.8;">${label}</span>
1525
+ <div id="inline-diff-${idx}" style="display: none;" data-loaded="false"></div>
1429
1526
  </div>`;
1430
1527
  });
1431
- html += '</div></div>';
1528
+ html += '</div>';
1432
1529
  content.innerHTML = html;
1433
1530
  } catch (e) {
1434
1531
  content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`;
1435
1532
  }
1436
1533
  }
1437
1534
 
1438
- let currentFilePath = '', currentFileDiff = '', currentFileContent = '', currentFileMode = 'diff';
1535
+ function buildFileDiffUrl(path, staged) {
1536
+ return `/api/sessions/${activeSessionId}/git-diff-file?path=${encodeURIComponent(path)}&staged=${staged ? 'true' : 'false'}`;
1537
+ }
1538
+
1539
+ async function loadFileChangeData(path, options = {}) {
1540
+ const hasStaged = !!options.hasStaged;
1541
+ const hasUnstaged = !!options.hasUnstaged;
1542
+ const isUntracked = !!options.isUntracked;
1543
+ const diffRequests = [];
1544
+
1545
+ if (hasStaged) {
1546
+ diffRequests.push({
1547
+ label: 'Staged changes',
1548
+ promise: fetchWithTimeout(buildFileDiffUrl(path, true)).catch(() => ({ok:false}))
1549
+ });
1550
+ }
1551
+ if (hasUnstaged || (!hasStaged && !isUntracked)) {
1552
+ diffRequests.push({
1553
+ label: 'Unstaged changes',
1554
+ promise: fetchWithTimeout(buildFileDiffUrl(path, false)).catch(() => ({ok:false}))
1555
+ });
1556
+ }
1557
+
1558
+ const [diffResponses, fileRes] = await Promise.all([
1559
+ Promise.all(diffRequests.map(item => item.promise)),
1560
+ fetchWithTimeout(`/api/sessions/${activeSessionId}/file?path=${encodeURIComponent(path)}`).catch(() => ({ok:false}))
1561
+ ]);
1562
+
1563
+ const diffParts = [];
1564
+ for (let i = 0; i < diffResponses.length; i++) {
1565
+ const res = diffResponses[i];
1566
+ const data = res.ok ? await res.json() : { success: false };
1567
+ if (data.success && data.stdout) {
1568
+ diffParts.push({ label: diffRequests[i].label, stdout: data.stdout });
1569
+ }
1570
+ }
1571
+
1572
+ const fileData = fileRes.ok ? await fileRes.json() : { success: false };
1573
+ const content = fileData.success ? fileData.content : '';
1574
+ let diff = diffParts.map(part => (
1575
+ diffParts.length > 1 ? `# ${part.label}\n${part.stdout}` : part.stdout
1576
+ )).join('\n');
1577
+ if (!diff && isUntracked && content) diff = 'Untracked file:\n\n' + content;
1578
+
1579
+ return {
1580
+ diff,
1581
+ content,
1582
+ renderRawDiff: diffParts.length > 1
1583
+ };
1584
+ }
1585
+
1586
+ window.toggleInlineDiff = async function(encodedPath, containerId, hasStaged = false, hasUnstaged = true, isUntracked = false) {
1587
+ const container = document.getElementById(containerId);
1588
+ if (container.style.display === 'block') {
1589
+ container.style.display = 'none';
1590
+ return;
1591
+ }
1592
+ container.style.display = 'block';
1593
+ if (container.dataset.loaded === 'true') return;
1594
+
1595
+ const path = decodePathValue(encodedPath);
1596
+ container.innerHTML = '<div style="padding: 10px; color: var(--text-dim); text-align: center; font-size: 12px;">Loading...</div>';
1597
+
1598
+ try {
1599
+ const { diff: currentFileDiff, content: currentFileContent } = await loadFileChangeData(path, {
1600
+ hasStaged,
1601
+ hasUnstaged,
1602
+ isUntracked
1603
+ });
1604
+
1605
+ if (!currentFileDiff && !currentFileContent) {
1606
+ container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">No diff available</div>`;
1607
+ return;
1608
+ }
1439
1609
 
1440
- async function showFileDetails(path, isFromChanges = true) {
1610
+ let blockContent = '';
1611
+ currentFileDiff.split('\n').forEach(line => {
1612
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
1613
+ if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
1614
+ else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
1615
+ else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
1616
+
1617
+ blockContent += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding:2px 8px; white-space:pre-wrap; word-break:break-all;">${escapeHtml(line) || ' '}</div>`;
1618
+ });
1619
+
1620
+ container.dataset.loaded = 'true';
1621
+ container.innerHTML = `
1622
+ <div style="background: #0d0d0d; overflow-x: auto; font-family: monospace; font-size: 12px; line-height: 1.5; padding: 4px 0; border-top: 1px solid #333; max-height: 400px;">
1623
+ ${blockContent}
1624
+ </div>
1625
+ <div style="padding: 8px; background: #1a1a1a; border-top: 1px solid #333; text-align: center;">
1626
+ <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;">
1627
+ <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>
1628
+ View Full File
1629
+ </button>
1630
+ </div>
1631
+ `;
1632
+ } catch (e) {
1633
+ container.innerHTML = `<div style="padding: 10px; color: #f87171; font-size: 12px; text-align:center;">Error: ${e.message}</div>`;
1634
+ }
1635
+ };
1636
+
1637
+ let currentFilePath = '', currentFileDiff = '', currentFileContent = '', currentFileMode = 'diff', currentFileWrap = false, currentFontSize = 12, currentFileRenderRawDiff = false;
1638
+
1639
+ function changeFontSize(delta) {
1640
+ if (delta === 0) currentFontSize = 12;
1641
+ else currentFontSize = Math.max(8, Math.min(32, currentFontSize + delta));
1642
+ renderFileDetails();
1643
+ }
1644
+
1645
+ async function showFileDetails(path, isFromChanges = true, hasStaged = false, hasUnstaged = true, isUntracked = false) {
1441
1646
  currentFilePath = path;
1442
1647
  currentFileMode = isFromChanges ? 'diff' : 'file';
1648
+ currentFileRenderRawDiff = false;
1443
1649
  const content = document.getElementById('git-content');
1444
1650
  content.innerHTML = '<p style="text-align:center;color:#888;padding:20px;">Loading details...</p>';
1445
1651
  try {
1446
- const [diffRes, fileRes] = await Promise.all([
1447
- fetchWithTimeout(`/api/sessions/${activeSessionId}/git-diff-file?path=${encodeURIComponent(path)}`).catch(()=>({ok:false})),
1448
- fetchWithTimeout(`/api/sessions/${activeSessionId}/file?path=${encodeURIComponent(path)}`).catch(()=>({ok:false}))
1449
- ]);
1450
- let diffData = diffRes.ok ? await diffRes.json() : { success: false };
1451
- let fileData = fileRes.ok ? await fileRes.json() : { success: false };
1452
- currentFileDiff = (diffData.success && diffData.stdout) ? diffData.stdout : '';
1453
- currentFileContent = fileData.success ? fileData.content : '';
1454
- if (!currentFileDiff && isFromChanges && currentFileContent) currentFileDiff = 'Untracked file:\n\n' + currentFileContent;
1652
+ const detailData = await loadFileChangeData(path, {
1653
+ hasStaged: isFromChanges ? hasStaged : false,
1654
+ hasUnstaged: isFromChanges ? hasUnstaged : true,
1655
+ isUntracked: isFromChanges ? isUntracked : false
1656
+ });
1657
+ currentFileDiff = detailData.diff;
1658
+ currentFileContent = detailData.content;
1659
+ currentFileRenderRawDiff = detailData.renderRawDiff;
1455
1660
  if (!currentFileDiff && !currentFileContent) {
1456
1661
  content.innerHTML = `<p style="color:#ff3b30; padding:10px;">Failed to load details.</p>`;
1457
1662
  return;
1458
1663
  }
1459
- if (!diffData.stdout && !isFromChanges) currentFileMode = 'file';
1664
+ if (!currentFileDiff && !isFromChanges) currentFileMode = 'file';
1460
1665
  renderFileDetails();
1461
1666
  } catch (e) { content.innerHTML = `<p style="color:#ff3b30; padding:10px;">${e.message}</p>`; }
1462
1667
  }
1463
1668
 
1464
- function setFileMode(mode) { currentFileMode = mode; renderFileDetails(); }
1669
+ function toggleFileMode() { currentFileMode = currentFileMode === 'diff' ? 'file' : 'diff'; renderFileDetails(); }
1670
+ function toggleFileWrap() { currentFileWrap = !currentFileWrap; renderFileDetails(); }
1465
1671
 
1466
1672
  function renderFileDetails() {
1467
1673
  const content = document.getElementById('git-content');
1468
1674
  let html = `
1469
- <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05);">
1470
- <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;">
1675
+ <div style="display:flex; align-items:center; background: var(--card-bg); padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.05); position: sticky; top: 0; z-index: 10;">
1676
+ <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;">
1471
1677
  <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
1472
1678
  </button>
1473
- <div style="flex:1; min-width:0;">
1474
- <div style="font-weight:600; font-size:15px;">${escapeHtml(currentFilePath.split('/').pop() || currentFilePath)}</div>
1475
- </div>
1476
- </div>`;
1679
+ <div style="flex:1; min-width:0; margin-right: 12px;">
1680
+ <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>
1681
+ </div>`;
1682
+
1477
1683
  if (currentFileDiff && currentFileContent) {
1478
- html += `<div class="btn-toggle">
1479
- <button class="${currentFileMode === 'diff' ? 'active' : ''}" onclick="setFileMode('diff')">Diff</button>
1480
- <button class="${currentFileMode === 'file' ? 'active' : ''}" onclick="setFileMode('file')">File</button>
1684
+ html += `
1685
+ <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
1686
+ <div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
1687
+ <button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
1688
+ <button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
1689
+ <button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
1690
+ </div>
1691
+ <button onclick="toggleFileMode()" style="background: ${currentFileMode === 'diff' ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
1692
+ Diff
1693
+ </button>
1694
+ <button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
1695
+ Wrap
1696
+ </button>
1697
+ </div>`;
1698
+ } else {
1699
+ html += `
1700
+ <div style="display: flex; gap: 6px; flex-shrink: 0; align-items: center;">
1701
+ <div style="display: flex; gap: 2px; margin-right: 8px; background: rgba(0,0,0,0.3); padding: 2px; border-radius: 4px;">
1702
+ <button onclick="changeFontSize(-1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom Out" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A-</button>
1703
+ <button onclick="changeFontSize(0)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Reset Size" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">${currentFontSize}</button>
1704
+ <button onclick="changeFontSize(1)" style="background: transparent; border: none; color: #aaa; padding: 4px 8px; font-size: 12px; cursor: pointer; outline: none; border-radius: 3px;" title="Zoom In" onmouseover="this.style.background='rgba(255,255,255,0.1)';this.style.color='#fff'" onmouseout="this.style.background='transparent';this.style.color='#aaa'">A+</button>
1705
+ </div>
1706
+ <button onclick="toggleFileWrap()" style="background: ${currentFileWrap ? 'var(--primary)' : 'rgba(255,255,255,0.1)'}; border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; outline: none;">
1707
+ Wrap
1708
+ </button>
1481
1709
  </div>`;
1482
1710
  }
1711
+
1712
+ html += `</div>`;
1713
+
1714
+ const wrapStyle = currentFileWrap ? 'white-space: pre-wrap; word-break: break-all;' : 'white-space: pre;';
1715
+ const innerWrapStyle = currentFileWrap ? '' : 'min-width: max-content;';
1483
1716
  html += `<div style="padding: 10px;">`;
1484
- if (currentFileMode === 'diff' && currentFileDiff) {
1485
- html += `<div style="background:#0d0d0d; border-radius:8px; overflow-x:auto; font-family:monospace; font-size:12px; line-height:1.5;">`;
1486
- currentFileDiff.split('\n').forEach(line => {
1487
- let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
1488
- if (line.startsWith('+') && !line.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
1489
- else if (line.startsWith('-') && !line.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
1490
- else if (line.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
1491
- html += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; white-space:pre-wrap; padding: 2px 8px;">${escapeHtml(line) || ' '}</div>`;
1492
- });
1493
- html += `</div>`;
1717
+ html += `<div style="background:#0d0d0d; border-radius:8px; overflow-x:auto; font-family:monospace; font-size:${currentFontSize}px; line-height:1.5; padding: 12px 0; margin: 0; -webkit-text-size-adjust: 100%; text-size-adjust: 100%;">`;
1718
+ html += `<div style="${innerWrapStyle}">`;
1719
+
1720
+ let mergedLines = [];
1721
+ const lines = currentFileContent ? currentFileContent.replace(/\r\n/g, '\n').split('\n') : [];
1722
+
1723
+ if (currentFileMode === 'diff' && currentFileDiff && !currentFileDiff.startsWith('Untracked file:')) {
1724
+ let diffHunks = [];
1725
+ let currentHunk = null;
1726
+ const diffLines = currentFileDiff.replace(/\r\n/g, '\n').split('\n');
1727
+ for (const dl of diffLines) {
1728
+ if (dl.startsWith('---') || dl.startsWith('+++')) continue;
1729
+ let match = dl.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1730
+ if (match) {
1731
+ currentHunk = {
1732
+ newStart: parseInt(match[2], 10),
1733
+ lines: []
1734
+ };
1735
+ diffHunks.push(currentHunk);
1736
+ } else if (currentHunk) {
1737
+ currentHunk.lines.push(dl);
1738
+ }
1739
+ }
1740
+
1741
+ if (currentFileRenderRawDiff || diffHunks.length === 0) {
1742
+ currentFileDiff.replace(/\r\n/g, '\n').split('\n').forEach(line => {
1743
+ mergedLines.push({ type: 'raw', text: line });
1744
+ });
1745
+ } else {
1746
+ let contentLineIdx = 1;
1747
+ for (let hunk of diffHunks) {
1748
+ while (contentLineIdx < hunk.newStart && contentLineIdx <= lines.length) {
1749
+ mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
1750
+ contentLineIdx++;
1751
+ }
1752
+ for (let dl of hunk.lines) {
1753
+ if (dl.startsWith('-')) {
1754
+ mergedLines.push({ type: 'deleted', text: dl.substring(1) });
1755
+ } else if (dl.startsWith('+')) {
1756
+ mergedLines.push({ type: 'added', text: dl.substring(1), lineNum: contentLineIdx });
1757
+ contentLineIdx++;
1758
+ } else if (dl.startsWith(' ')) {
1759
+ mergedLines.push({ type: 'normal', text: dl.substring(1), lineNum: contentLineIdx });
1760
+ contentLineIdx++;
1761
+ }
1762
+ }
1763
+ }
1764
+ while (contentLineIdx <= lines.length) {
1765
+ mergedLines.push({ type: 'normal', text: lines[contentLineIdx - 1], lineNum: contentLineIdx });
1766
+ contentLineIdx++;
1767
+ }
1768
+ }
1494
1769
  } else {
1495
- html += `<pre style="background:#0d0d0d; border-radius:8px; overflow-x:auto; font-family:monospace; font-size:12px; line-height:1.5; padding: 12px; color: #ccc; margin: 0; white-space: pre-wrap; word-break: break-all;"><code>${escapeHtml(currentFileContent)}</code></pre>`;
1770
+ lines.forEach((line, idx) => {
1771
+ mergedLines.push({ type: 'normal', text: line, lineNum: idx + 1 });
1772
+ });
1496
1773
  }
1497
- html += `</div>`;
1774
+
1775
+ mergedLines.forEach(item => {
1776
+ let color = '#ccc', bg = 'transparent', borderLeft = '2px solid transparent';
1777
+ let prefix = ' ';
1778
+ if (item.type === 'added') { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; prefix = '+'; }
1779
+ else if (item.type === 'deleted') { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; prefix = '-'; }
1780
+ else if (item.type === 'raw') {
1781
+ if (item.text.startsWith('+') && !item.text.startsWith('+++')) { color = '#4ade80'; bg = 'rgba(74, 222, 128, 0.1)'; borderLeft = '2px solid #4ade80'; }
1782
+ else if (item.text.startsWith('-') && !item.text.startsWith('---')) { color = '#f87171'; bg = 'rgba(248, 113, 113, 0.1)'; borderLeft = '2px solid #f87171'; }
1783
+ else if (item.text.startsWith('@@')) { color = '#60a5fa'; bg = 'rgba(96, 165, 250, 0.1)'; }
1784
+ prefix = '';
1785
+ }
1786
+
1787
+ const renderedText = item.type === 'raw' ? item.text : `${prefix} ${item.text}`;
1788
+ html += `<div style="color:${color}; background:${bg}; border-left:${borderLeft}; padding: 0 12px 0 8px; display:flex;">`;
1789
+ html += `<div style="color:#555; margin-right:16px; user-select:none; flex-shrink:0; width: 36px; text-align:right;">${item.lineNum || ''}</div>`;
1790
+ html += `<div style="flex:1; min-width:0; ${wrapStyle}">${escapeHtml(renderedText) || ' '}</div>`;
1791
+ html += `</div>`;
1792
+ });
1793
+
1794
+ html += `</div></div></div>`;
1498
1795
  content.innerHTML = html;
1499
1796
  }
1500
1797
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "main": "index.js",
6
6
  "bin": {