claude-opencode-viewer 2.5.0 → 2.6.0

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 (3) hide show
  1. package/index.html +442 -2
  2. package/package.json +1 -1
  3. package/server.js +95 -3
package/index.html CHANGED
@@ -574,6 +574,219 @@
574
574
  #copy-toast.show {
575
575
  display: block;
576
576
  }
577
+
578
+ /* Git Diff 面板 */
579
+ #git-diff-bar {
580
+ display: none;
581
+ position: absolute;
582
+ top: 0;
583
+ left: 0;
584
+ right: 0;
585
+ bottom: 0;
586
+ background: #0a0a0a;
587
+ z-index: 1000;
588
+ flex-direction: column;
589
+ }
590
+
591
+ #git-diff-bar.visible {
592
+ display: flex;
593
+ }
594
+
595
+ #git-diff-header {
596
+ display: flex;
597
+ align-items: center;
598
+ justify-content: space-between;
599
+ padding: 12px 16px;
600
+ background: #111;
601
+ border-bottom: 1px solid #222;
602
+ flex-shrink: 0;
603
+ }
604
+
605
+ #git-diff-title {
606
+ font-size: 14px;
607
+ color: #ddd;
608
+ font-weight: 600;
609
+ }
610
+
611
+ .git-diff-file-list {
612
+ height: 250px;
613
+ flex-shrink: 0;
614
+ overflow-y: auto;
615
+ border-bottom: 1px solid #2a2a2a;
616
+ -webkit-overflow-scrolling: touch;
617
+ }
618
+
619
+ .git-diff-file-item {
620
+ display: flex;
621
+ align-items: center;
622
+ padding: 6px 12px;
623
+ cursor: pointer;
624
+ color: #ccc;
625
+ font-size: 13px;
626
+ gap: 8px;
627
+ white-space: nowrap;
628
+ }
629
+
630
+ .git-diff-file-item:hover {
631
+ background: #1a1a1a;
632
+ }
633
+
634
+ .git-diff-file-item.active {
635
+ background: rgba(74, 158, 255, 0.12);
636
+ color: #fff;
637
+ }
638
+
639
+ .git-diff-file-status {
640
+ width: 18px;
641
+ flex-shrink: 0;
642
+ font-size: 11px;
643
+ font-weight: 700;
644
+ text-align: center;
645
+ }
646
+
647
+ .git-diff-file-name {
648
+ overflow: hidden;
649
+ text-overflow: ellipsis;
650
+ flex: 1;
651
+ font-family: Menlo, Monaco, monospace;
652
+ font-size: 12px;
653
+ }
654
+
655
+ .git-diff-content-area {
656
+ flex: 1;
657
+ display: flex;
658
+ flex-direction: column;
659
+ min-height: 0;
660
+ overflow: hidden;
661
+ }
662
+
663
+ .git-diff-content-header {
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 10px;
667
+ padding: 8px 12px;
668
+ border-bottom: 1px solid #2a2a2a;
669
+ background: #111;
670
+ flex-shrink: 0;
671
+ }
672
+
673
+ .git-diff-content-path {
674
+ font-size: 12px;
675
+ color: #ccc;
676
+ font-family: Menlo, Monaco, monospace;
677
+ overflow: hidden;
678
+ text-overflow: ellipsis;
679
+ white-space: nowrap;
680
+ flex: 1;
681
+ }
682
+
683
+ .git-diff-badge {
684
+ padding: 2px 8px;
685
+ background: #2a2a2a;
686
+ border: 1px solid #444;
687
+ border-radius: 4px;
688
+ font-size: 10px;
689
+ font-weight: 600;
690
+ color: #e2c08d;
691
+ letter-spacing: 0.5px;
692
+ flex-shrink: 0;
693
+ }
694
+
695
+ .git-diff-content-scroll {
696
+ flex: 1;
697
+ overflow: auto;
698
+ -webkit-overflow-scrolling: touch;
699
+ }
700
+
701
+ .git-diff-placeholder {
702
+ flex: 1;
703
+ display: flex;
704
+ flex-direction: column;
705
+ align-items: center;
706
+ justify-content: center;
707
+ gap: 12px;
708
+ color: #333;
709
+ font-size: 13px;
710
+ }
711
+
712
+ .git-diff-loading {
713
+ text-align: center;
714
+ padding: 16px;
715
+ color: #888;
716
+ font-size: 12px;
717
+ }
718
+
719
+ .git-diff-error {
720
+ color: #ff6b6b;
721
+ font-size: 12px;
722
+ padding: 16px 12px;
723
+ text-align: center;
724
+ }
725
+
726
+ /* Unified diff 行 */
727
+ .diff-table {
728
+ width: 100%;
729
+ border-collapse: collapse;
730
+ font-family: Menlo, Monaco, 'Courier New', monospace;
731
+ font-size: 12px;
732
+ line-height: 1.5;
733
+ }
734
+
735
+ .diff-line {
736
+ border: none;
737
+ }
738
+
739
+ .diff-line-num {
740
+ width: 28px;
741
+ min-width: 28px;
742
+ padding: 0 3px;
743
+ text-align: right;
744
+ color: #555;
745
+ font-size: 11px;
746
+ user-select: none;
747
+ -webkit-user-select: none;
748
+ vertical-align: top;
749
+ }
750
+
751
+ .diff-line-content {
752
+ padding: 0 8px;
753
+ white-space: pre-wrap;
754
+ word-break: break-all;
755
+ color: #d4d4d4;
756
+ }
757
+
758
+ .diff-line-add {
759
+ background: rgba(35, 134, 54, 0.2);
760
+ }
761
+
762
+ .diff-line-add .diff-line-content {
763
+ color: #7ee787;
764
+ }
765
+
766
+ .diff-line-del {
767
+ background: rgba(248, 81, 73, 0.2);
768
+ }
769
+
770
+ .diff-line-del .diff-line-content {
771
+ color: #ffa198;
772
+ }
773
+
774
+ .diff-line-hunk {
775
+ background: rgba(56, 139, 253, 0.1);
776
+ }
777
+
778
+ .diff-line-hunk .diff-line-content {
779
+ color: #79c0ff;
780
+ font-style: italic;
781
+ }
782
+
783
+ .diff-file-count {
784
+ font-size: 10px;
785
+ color: #555;
786
+ background: #1a1a1a;
787
+ padding: 1px 6px;
788
+ border-radius: 8px;
789
+ }
577
790
  </style>
578
791
  </head>
579
792
  <body>
@@ -581,20 +794,29 @@
581
794
  <div id="layout">
582
795
  <div id="header">
583
796
  <div style="display: flex; gap: 8px; align-items: center;">
584
- <button class="history-toggle-btn" id="new-session-btn">
797
+ <button class="history-toggle-btn" id="new-session-btn" style="color:#73c991; border-color:#2a5a3a;">
585
798
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
586
799
  <line x1="12" y1="5" x2="12" y2="19"></line>
587
800
  <line x1="5" y1="12" x2="19" y2="12"></line>
588
801
  </svg>
589
802
  <span>新会话</span>
590
803
  </button>
591
- <button class="history-toggle-btn" id="history-toggle">
804
+ <button class="history-toggle-btn" id="history-toggle" style="color:#79c0ff; border-color:#2a4a7c;">
592
805
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
593
806
  <circle cx="12" cy="12" r="10"></circle>
594
807
  <polyline points="12 6 12 12 16 14"></polyline>
595
808
  </svg>
596
809
  <span>历史</span>
597
810
  </button>
811
+ <button class="history-toggle-btn" id="diff-toggle" style="color:#e2c08d; border-color:#5a4a2a;">
812
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
813
+ <line x1="6" y1="3" x2="6" y2="15"></line>
814
+ <circle cx="18" cy="6" r="3"></circle>
815
+ <circle cx="6" cy="18" r="3"></circle>
816
+ <path d="M18 9a9 9 0 0 1-9 9"></path>
817
+ </svg>
818
+ <span>Diff</span>
819
+ </button>
598
820
  </div>
599
821
  <div id="mode-switcher">
600
822
  <span id="mode-label"></span>
@@ -669,6 +891,46 @@
669
891
  </div>
670
892
  </div>
671
893
 
894
+ <!-- Git Diff 面板 -->
895
+ <div id="git-diff-bar">
896
+ <div id="git-diff-header">
897
+ <div style="display: flex; align-items: center; gap: 8px;">
898
+ <span id="git-diff-title">Git Changes</span>
899
+ <span class="diff-file-count" id="git-diff-count">0</span>
900
+ </div>
901
+ <div style="display: flex; gap: 8px;">
902
+ <button class="history-toggle-btn" id="refresh-diff">
903
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
904
+ <polyline points="23 4 23 10 17 10"></polyline>
905
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
906
+ </svg>
907
+ <span>刷新</span>
908
+ </button>
909
+ <button class="history-toggle-btn" id="close-diff">
910
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
911
+ <line x1="18" y1="6" x2="6" y2="18"></line>
912
+ <line x1="6" y1="6" x2="18" y2="18"></line>
913
+ </svg>
914
+ <span>返回</span>
915
+ </button>
916
+ </div>
917
+ </div>
918
+ <div class="git-diff-file-list" id="git-diff-file-list">
919
+ <div class="git-diff-loading">加载中...</div>
920
+ </div>
921
+ <div class="git-diff-content-area" id="git-diff-content-area">
922
+ <div class="git-diff-placeholder">
923
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">
924
+ <line x1="6" y1="3" x2="6" y2="15"></line>
925
+ <circle cx="18" cy="6" r="3"></circle>
926
+ <circle cx="6" cy="18" r="3"></circle>
927
+ <path d="M18 9a9 9 0 0 1-9 9"></path>
928
+ </svg>
929
+ <span>点击文件查看 diff</span>
930
+ </div>
931
+ </div>
932
+ </div>
933
+
672
934
  <div id="content">
673
935
  <div id="terminal-container">
674
936
  <div id="terminal">
@@ -1789,6 +2051,184 @@
1789
2051
  closeSelectMode();
1790
2052
  });
1791
2053
 
2054
+ // ======= Git Diff 功能 =======
2055
+ var diffBarVisible = false;
2056
+ var diffChanges = [];
2057
+ var diffSelectedFile = null;
2058
+
2059
+ var STATUS_COLORS = {
2060
+ 'M': '#e2c08d', 'A': '#73c991', 'D': '#f14c4c',
2061
+ 'R': '#73c991', 'C': '#73c991', 'U': '#e2c08d',
2062
+ '?': '#73c991', '??': '#73c991',
2063
+ };
2064
+
2065
+ function toggleDiffBar() {
2066
+ diffBarVisible = !diffBarVisible;
2067
+ var bar = document.getElementById('git-diff-bar');
2068
+ if (diffBarVisible) {
2069
+ bar.classList.add('visible');
2070
+ loadGitStatus();
2071
+ } else {
2072
+ bar.classList.remove('visible');
2073
+ diffSelectedFile = null;
2074
+ }
2075
+ }
2076
+
2077
+ function loadGitStatus() {
2078
+ var fileList = document.getElementById('git-diff-file-list');
2079
+ fileList.innerHTML = '<div class="git-diff-loading">加载中...</div>';
2080
+ document.getElementById('git-diff-count').textContent = '0';
2081
+
2082
+ fetch('/api/git-status')
2083
+ .then(function(r) { return r.json(); })
2084
+ .then(function(data) {
2085
+ diffChanges = data.changes || [];
2086
+ document.getElementById('git-diff-count').textContent = diffChanges.length;
2087
+ renderDiffFileList();
2088
+ })
2089
+ .catch(function() {
2090
+ fileList.innerHTML = '<div class="git-diff-error">无法加载 git status</div>';
2091
+ });
2092
+ }
2093
+
2094
+ function renderDiffFileList() {
2095
+ var fileList = document.getElementById('git-diff-file-list');
2096
+ if (diffChanges.length === 0) {
2097
+ fileList.innerHTML = '<div class="git-diff-loading" style="color:#666;">没有变更文件</div>';
2098
+ return;
2099
+ }
2100
+ var html = '';
2101
+ diffChanges.forEach(function(c) {
2102
+ var color = STATUS_COLORS[c.status] || '#888';
2103
+ var label = c.status === '??' ? 'U' : c.status;
2104
+ var activeClass = diffSelectedFile === c.file ? ' active' : '';
2105
+ html += '<div class="git-diff-file-item' + activeClass + '" data-file="' + escapeHtml(c.file) + '">';
2106
+ html += '<span class="git-diff-file-status" style="color:' + color + '">' + label + '</span>';
2107
+ html += '<span class="git-diff-file-name">' + escapeHtml(c.file) + '</span>';
2108
+ html += '</div>';
2109
+ });
2110
+ fileList.innerHTML = html;
2111
+
2112
+ // 绑定点击事件
2113
+ fileList.querySelectorAll('.git-diff-file-item').forEach(function(item) {
2114
+ item.addEventListener('click', function() {
2115
+ var file = item.getAttribute('data-file');
2116
+ diffSelectedFile = file;
2117
+ // 更新选中状态
2118
+ fileList.querySelectorAll('.git-diff-file-item').forEach(function(el) {
2119
+ el.classList.toggle('active', el.getAttribute('data-file') === file);
2120
+ });
2121
+ loadDiffContent(file);
2122
+ });
2123
+ });
2124
+ }
2125
+
2126
+ function loadDiffContent(file) {
2127
+ var area = document.getElementById('git-diff-content-area');
2128
+ area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
2129
+
2130
+ fetch('/api/git-diff?files=' + encodeURIComponent(file))
2131
+ .then(function(r) { return r.json(); })
2132
+ .then(function(data) {
2133
+ if (!data.diffs || !data.diffs[0]) {
2134
+ area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
2135
+ return;
2136
+ }
2137
+ var d = data.diffs[0];
2138
+ var html = '<div class="git-diff-content-header">';
2139
+ html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
2140
+ html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
2141
+ html += '</div>';
2142
+ html += '<div class="git-diff-content-scroll">';
2143
+
2144
+ if (d.is_binary) {
2145
+ html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
2146
+ } else if (d.is_large) {
2147
+ html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
2148
+ } else if (d.unified_diff) {
2149
+ html += renderUnifiedDiff(d.unified_diff);
2150
+ } else {
2151
+ html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
2152
+ }
2153
+
2154
+ html += '</div>';
2155
+ area.innerHTML = html;
2156
+ })
2157
+ .catch(function(err) {
2158
+ area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
2159
+ });
2160
+ }
2161
+
2162
+ function renderUnifiedDiff(diffText) {
2163
+ var lines = diffText.split('\n');
2164
+ var html = '<table class="diff-table">';
2165
+ var oldLine = 0, newLine = 0;
2166
+
2167
+ for (var i = 0; i < lines.length; i++) {
2168
+ var line = lines[i];
2169
+
2170
+ // 跳过 diff 头部信息
2171
+ if (line.startsWith('diff --git') || line.startsWith('index ') ||
2172
+ line.startsWith('---') || line.startsWith('+++') ||
2173
+ line.startsWith('new file') || line.startsWith('deleted file') ||
2174
+ line.startsWith('old mode') || line.startsWith('new mode')) {
2175
+ continue;
2176
+ }
2177
+
2178
+ if (line.startsWith('@@')) {
2179
+ // 解析 hunk header: @@ -old,count +new,count @@
2180
+ var match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
2181
+ if (match) {
2182
+ oldLine = parseInt(match[1], 10);
2183
+ newLine = parseInt(match[2], 10);
2184
+ }
2185
+ html += '<tr class="diff-line diff-line-hunk">';
2186
+ html += '<td class="diff-line-num"></td><td class="diff-line-num"></td>';
2187
+ html += '<td class="diff-line-content">' + escapeHtml(line) + '</td></tr>';
2188
+ } else if (line.startsWith('+')) {
2189
+ html += '<tr class="diff-line diff-line-add">';
2190
+ html += '<td class="diff-line-num"></td>';
2191
+ html += '<td class="diff-line-num">' + newLine + '</td>';
2192
+ html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2193
+ newLine++;
2194
+ } else if (line.startsWith('-')) {
2195
+ html += '<tr class="diff-line diff-line-del">';
2196
+ html += '<td class="diff-line-num">' + oldLine + '</td>';
2197
+ html += '<td class="diff-line-num"></td>';
2198
+ html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2199
+ oldLine++;
2200
+ } else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
2201
+ html += '<tr class="diff-line">';
2202
+ html += '<td class="diff-line-num">' + oldLine + '</td>';
2203
+ html += '<td class="diff-line-num">' + newLine + '</td>';
2204
+ html += '<td class="diff-line-content">' + escapeHtml(line.substring(1) || '') + '</td></tr>';
2205
+ oldLine++;
2206
+ newLine++;
2207
+ }
2208
+ }
2209
+
2210
+ html += '</table>';
2211
+ return html;
2212
+ }
2213
+
2214
+ // Diff 按钮事件绑定
2215
+ document.getElementById('diff-toggle').addEventListener('click', toggleDiffBar);
2216
+ document.getElementById('close-diff').addEventListener('click', toggleDiffBar);
2217
+ document.getElementById('refresh-diff').addEventListener('click', function(e) {
2218
+ e.stopPropagation();
2219
+ loadGitStatus();
2220
+ // 重置 diff 内容区
2221
+ diffSelectedFile = null;
2222
+ document.getElementById('git-diff-content-area').innerHTML =
2223
+ '<div class="git-diff-placeholder">' +
2224
+ '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="1.5">' +
2225
+ '<line x1="6" y1="3" x2="6" y2="15"></line>' +
2226
+ '<circle cx="18" cy="6" r="3"></circle>' +
2227
+ '<circle cx="6" cy="18" r="3"></circle>' +
2228
+ '<path d="M18 9a9 9 0 0 1-9 9"></path>' +
2229
+ '</svg><span>点击文件查看 diff</span></div>';
2230
+ });
2231
+
1792
2232
  // 初始化虚拟按键事件
1793
2233
  setupVirtualKeyEvents();
1794
2234
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from 'node:http';
3
- import { existsSync, createReadStream } from 'node:fs';
3
+ import { existsSync, createReadStream, readFileSync } from 'node:fs';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { networkInterfaces, platform, arch, homedir } from 'node:os';
7
7
  import { chmodSync, statSync } from 'node:fs';
8
- import { execSync } from 'child_process';
8
+ import { execSync, execFile } from 'child_process';
9
+ import { promisify } from 'node:util';
9
10
  import { WebSocketServer } from 'ws';
10
11
  import Database from 'better-sqlite3';
11
12
 
@@ -22,6 +23,7 @@ const OPENCODE_DB_PATH = process.env.OPENCODE_DB_PATH || join(
22
23
  );
23
24
 
24
25
  const MAX_BUFFER = 200000;
26
+ const execFileAsync = promisify(execFile);
25
27
 
26
28
  let ptyModule = null;
27
29
  let claudeProcess = null;
@@ -388,7 +390,7 @@ function getSessionMessages(sessionId) {
388
390
  }
389
391
  }
390
392
 
391
- const server = createServer((req, res) => {
393
+ const server = createServer(async (req, res) => {
392
394
  if (req.url === '/' || req.url === '/index.html') {
393
395
  res.writeHead(200, {
394
396
  'Content-Type': 'text/html; charset=utf-8',
@@ -409,6 +411,96 @@ const server = createServer((req, res) => {
409
411
  return;
410
412
  }
411
413
 
414
+ // API: 获取 git status
415
+ if (req.url === '/api/git-status') {
416
+ res.writeHead(200, {
417
+ 'Content-Type': 'application/json',
418
+ 'Access-Control-Allow-Origin': '*',
419
+ });
420
+ try {
421
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
422
+ cwd: process.cwd(), encoding: 'utf-8', timeout: 5000,
423
+ });
424
+ const changes = stdout.split('\n').filter(Boolean).map(line => ({
425
+ status: line.substring(0, 2).trim(),
426
+ file: line.substring(3),
427
+ }));
428
+ res.end(JSON.stringify({ changes }));
429
+ } catch (err) {
430
+ res.end(JSON.stringify({ changes: [], error: err.message }));
431
+ }
432
+ return;
433
+ }
434
+
435
+ // API: 获取 git diff
436
+ if (req.url?.startsWith('/api/git-diff')) {
437
+ const url = new URL(req.url, `http://${req.headers.host}`);
438
+ const files = url.searchParams.get('files');
439
+ res.writeHead(200, {
440
+ 'Content-Type': 'application/json',
441
+ 'Access-Control-Allow-Origin': '*',
442
+ });
443
+ if (!files) {
444
+ res.end(JSON.stringify({ diffs: [] }));
445
+ return;
446
+ }
447
+ const fileList = files.split(',').filter(Boolean);
448
+ const diffs = [];
449
+ const cwd = process.cwd();
450
+ for (const file of fileList) {
451
+ if (file.includes('..') || file.startsWith('/')) continue;
452
+ try {
453
+ const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain', '--', file], { cwd, encoding: 'utf-8', timeout: 3000 });
454
+ if (!statusOut.trim()) continue;
455
+ const status = statusOut.substring(0, 2).trim();
456
+ const is_new = status === 'A' || status === '??';
457
+ const is_deleted = status === 'D';
458
+ let is_binary = false;
459
+ if (!is_deleted) {
460
+ try {
461
+ const { stdout: dc } = await execFileAsync('git', ['diff', '--numstat', 'HEAD', '--', file], { cwd, encoding: 'utf-8', timeout: 3000 });
462
+ if (dc.includes('-\t-\t')) is_binary = true;
463
+ } catch {}
464
+ }
465
+ let old_content = '', new_content = '', unified_diff = '';
466
+ if (!is_binary) {
467
+ if (!is_new) {
468
+ try {
469
+ const { stdout } = await execFileAsync('git', ['show', `HEAD:${file}`], { cwd, encoding: 'utf-8', timeout: 5000, maxBuffer: 5 * 1024 * 1024 });
470
+ old_content = stdout;
471
+ } catch {}
472
+ }
473
+ if (!is_deleted) {
474
+ try {
475
+ const filePath = join(cwd, file);
476
+ if (existsSync(filePath)) {
477
+ const stat = statSync(filePath);
478
+ if (stat.size > 5 * 1024 * 1024) {
479
+ diffs.push({ file, status, is_large: true, size: stat.size });
480
+ continue;
481
+ }
482
+ new_content = readFileSync(filePath, 'utf-8');
483
+ }
484
+ } catch {}
485
+ }
486
+ // 获取 unified diff
487
+ try {
488
+ const diffArgs = is_new
489
+ ? ['diff', '--no-color', '-U3', '--no-index', '/dev/null', file]
490
+ : ['diff', '--no-color', '-U3', 'HEAD', '--', file];
491
+ const { stdout } = await execFileAsync('git', diffArgs, { cwd, encoding: 'utf-8', timeout: 5000, maxBuffer: 5 * 1024 * 1024 });
492
+ unified_diff = stdout;
493
+ } catch (e) {
494
+ if (e.stdout) unified_diff = e.stdout;
495
+ }
496
+ }
497
+ diffs.push({ file, status, old_content, new_content, unified_diff, is_binary, is_new, is_deleted });
498
+ } catch { continue; }
499
+ }
500
+ res.end(JSON.stringify({ diffs }));
501
+ return;
502
+ }
503
+
412
504
  // API: 获取会话消息
413
505
  if (req.url?.startsWith('/api/session/')) {
414
506
  const sessionId = req.url.split('/').pop();