claude-opencode-viewer 2.6.48 → 2.6.49

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/index-pc.html CHANGED
@@ -740,6 +740,39 @@
740
740
  line-height: 1.6;
741
741
  color: #d4d4d4;
742
742
  }
743
+ .docs-md-content {
744
+ font-size: 13px; line-height: 1.7; color: #d4d4d4; word-break: break-word;
745
+ }
746
+ .docs-md-content h1, .docs-md-content h2, .docs-md-content h3 {
747
+ margin: 16px 0 8px; color: #e0e0e0; border-bottom: 1px solid #333; padding-bottom: 4px;
748
+ }
749
+ .docs-md-content h1 { font-size: 1.4em; }
750
+ .docs-md-content h2 { font-size: 1.2em; }
751
+ .docs-md-content h3 { font-size: 1.05em; }
752
+ .docs-md-content p { margin: 8px 0; }
753
+ .docs-md-content pre {
754
+ background: #1a1a1a; border: 1px solid #333; border-radius: 4px;
755
+ padding: 10px; overflow-x: auto; margin: 8px 0; white-space: pre-wrap; word-break: break-word;
756
+ }
757
+ .docs-md-content code {
758
+ background: #1a1a1a; padding: 1px 4px; border-radius: 3px; font-size: 12px;
759
+ font-family: Menlo, Monaco, monospace;
760
+ }
761
+ .docs-md-content pre code { background: none; padding: 0; }
762
+ .docs-md-content ul, .docs-md-content ol { margin: 8px 0; padding-left: 20px; }
763
+ .docs-md-content li { margin: 4px 0; }
764
+ .docs-md-content blockquote {
765
+ border-left: 3px solid #444; margin: 8px 0; padding: 4px 12px; color: #999;
766
+ }
767
+ .docs-md-content table { border-collapse: collapse; margin: 8px 0; width: 100%; }
768
+ .docs-md-content th, .docs-md-content td {
769
+ border: 1px solid #333; padding: 6px 10px; text-align: left;
770
+ }
771
+ .docs-md-content th { background: #1a1a1a; }
772
+ .docs-md-content a { color: #58a6ff; text-decoration: none; }
773
+ .docs-md-content a:hover { text-decoration: underline; }
774
+ .docs-md-content img { max-width: 100%; }
775
+ .docs-md-content hr { border: none; border-top: 1px solid #333; margin: 12px 0; }
743
776
  .docs-placeholder {
744
777
  flex: 1;
745
778
  display: flex;
@@ -805,6 +838,13 @@
805
838
  color: #ffa198;
806
839
  }
807
840
 
841
+ .diff-sign {
842
+ display: inline-block;
843
+ width: 12px;
844
+ font-weight: bold;
845
+ user-select: none;
846
+ }
847
+
808
848
  .diff-line-hunk {
809
849
  background: rgba(56, 139, 253, 0.1);
810
850
  }
@@ -1071,6 +1111,8 @@
1071
1111
  <div id="copy-toast">已复制</div>
1072
1112
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
1073
1113
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
1114
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
1115
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
1074
1116
  <script>
1075
1117
  (function() {
1076
1118
  var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
@@ -1724,6 +1766,11 @@
1724
1766
  term.clear();
1725
1767
  }
1726
1768
  else if (msg.type === 'state') {
1769
+ // 重连时清掉旧终端内容,防止重复叠加
1770
+ if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1771
+ writeBuffer = '';
1772
+ term.reset();
1773
+ term.clear();
1727
1774
  if (msg.mode) {
1728
1775
  currentMode = msg.mode;
1729
1776
  modeSelect.value = msg.mode;
@@ -2432,38 +2479,29 @@
2432
2479
 
2433
2480
  function loadDiffContent(file) {
2434
2481
  var area = document.getElementById('git-diff-content-area');
2435
- area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
2436
-
2437
- fetch(basePath + '/api/git-diff?files=' + encodeURIComponent(file))
2438
- .then(function(r) { return r.json(); })
2439
- .then(function(data) {
2440
- if (!data.diffs || !data.diffs[0]) {
2441
- area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
2442
- return;
2443
- }
2444
- var d = data.diffs[0];
2445
- var html = '<div class="git-diff-content-header">';
2446
- html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
2447
- html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
2448
- html += '</div>';
2449
- html += '<div class="git-diff-content-scroll">';
2450
-
2451
- if (d.is_binary) {
2452
- html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
2453
- } else if (d.is_large) {
2454
- html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
2455
- } else if (d.unified_diff) {
2456
- html += renderUnifiedDiff(d.unified_diff);
2457
- } else {
2458
- html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
2459
- }
2482
+ var d = diffChanges.find(function(c) { return c.file === file; });
2483
+ if (!d) {
2484
+ area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
2485
+ return;
2486
+ }
2487
+ var html = '<div class="git-diff-content-header">';
2488
+ html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
2489
+ html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
2490
+ html += '</div>';
2491
+ html += '<div class="git-diff-content-scroll">';
2492
+
2493
+ if (d.is_binary) {
2494
+ html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
2495
+ } else if (d.is_large) {
2496
+ html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
2497
+ } else if (d.unified_diff) {
2498
+ html += renderUnifiedDiff(d.unified_diff);
2499
+ } else {
2500
+ html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
2501
+ }
2460
2502
 
2461
- html += '</div>';
2462
- area.innerHTML = html;
2463
- })
2464
- .catch(function(err) {
2465
- area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
2466
- });
2503
+ html += '</div>';
2504
+ area.innerHTML = html;
2467
2505
  }
2468
2506
 
2469
2507
  function renderUnifiedDiff(diffText) {
@@ -2496,13 +2534,13 @@
2496
2534
  html += '<tr class="diff-line diff-line-add">';
2497
2535
  html += '<td class="diff-line-num"></td>';
2498
2536
  html += '<td class="diff-line-num">' + newLine + '</td>';
2499
- html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2537
+ html += '<td class="diff-line-content"><span class="diff-sign">+</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
2500
2538
  newLine++;
2501
2539
  } else if (line.startsWith('-')) {
2502
2540
  html += '<tr class="diff-line diff-line-del">';
2503
2541
  html += '<td class="diff-line-num">' + oldLine + '</td>';
2504
2542
  html += '<td class="diff-line-num"></td>';
2505
- html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2543
+ html += '<td class="diff-line-content"><span class="diff-sign">-</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
2506
2544
  oldLine++;
2507
2545
  } else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
2508
2546
  html += '<tr class="diff-line">';
@@ -2608,6 +2646,15 @@
2608
2646
  });
2609
2647
  }
2610
2648
 
2649
+ function formatDocContent(text) {
2650
+ if (!text) return '';
2651
+ try {
2652
+ return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
2653
+ } catch (e) {
2654
+ return '<pre>' + escapeHtml(text) + '</pre>';
2655
+ }
2656
+ }
2657
+
2611
2658
  function loadDocContent(file) {
2612
2659
  var area = document.getElementById('docs-content-area');
2613
2660
  area.innerHTML = '<div class="docs-loading">加载中...</div>';
@@ -2618,7 +2665,7 @@
2618
2665
  area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">' + escapeHtml(data.error) + '</div>';
2619
2666
  return;
2620
2667
  }
2621
- area.innerHTML = '<pre>' + escapeHtml(data.content) + '</pre>';
2668
+ area.innerHTML = '<div class="docs-md-content">' + formatDocContent(data.content) + '</div>';
2622
2669
  })
2623
2670
  .catch(function(err) {
2624
2671
  area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
package/index.html CHANGED
@@ -809,6 +809,39 @@
809
809
  line-height: 1.6;
810
810
  color: #d4d4d4;
811
811
  }
812
+ .docs-md-content {
813
+ font-size: 13px; line-height: 1.7; color: #d4d4d4; word-break: break-word;
814
+ }
815
+ .docs-md-content h1, .docs-md-content h2, .docs-md-content h3 {
816
+ margin: 16px 0 8px; color: #e0e0e0; border-bottom: 1px solid #333; padding-bottom: 4px;
817
+ }
818
+ .docs-md-content h1 { font-size: 1.4em; }
819
+ .docs-md-content h2 { font-size: 1.2em; }
820
+ .docs-md-content h3 { font-size: 1.05em; }
821
+ .docs-md-content p { margin: 8px 0; }
822
+ .docs-md-content pre {
823
+ background: #1a1a1a; border: 1px solid #333; border-radius: 4px;
824
+ padding: 10px; overflow-x: auto; margin: 8px 0; white-space: pre-wrap; word-break: break-word;
825
+ }
826
+ .docs-md-content code {
827
+ background: #1a1a1a; padding: 1px 4px; border-radius: 3px; font-size: 12px;
828
+ font-family: Menlo, Monaco, monospace;
829
+ }
830
+ .docs-md-content pre code { background: none; padding: 0; }
831
+ .docs-md-content ul, .docs-md-content ol { margin: 8px 0; padding-left: 20px; }
832
+ .docs-md-content li { margin: 4px 0; }
833
+ .docs-md-content blockquote {
834
+ border-left: 3px solid #444; margin: 8px 0; padding: 4px 12px; color: #999;
835
+ }
836
+ .docs-md-content table { border-collapse: collapse; margin: 8px 0; width: 100%; }
837
+ .docs-md-content th, .docs-md-content td {
838
+ border: 1px solid #333; padding: 6px 10px; text-align: left;
839
+ }
840
+ .docs-md-content th { background: #1a1a1a; }
841
+ .docs-md-content a { color: #58a6ff; text-decoration: none; }
842
+ .docs-md-content a:hover { text-decoration: underline; }
843
+ .docs-md-content img { max-width: 100%; }
844
+ .docs-md-content hr { border: none; border-top: 1px solid #333; margin: 12px 0; }
812
845
  .docs-placeholder {
813
846
  flex: 1;
814
847
  display: flex;
@@ -874,6 +907,13 @@
874
907
  color: #ffa198;
875
908
  }
876
909
 
910
+ .diff-sign {
911
+ display: inline-block;
912
+ width: 12px;
913
+ font-weight: bold;
914
+ user-select: none;
915
+ }
916
+
877
917
  .diff-line-hunk {
878
918
  background: rgba(56, 139, 253, 0.1);
879
919
  }
@@ -2633,38 +2673,29 @@
2633
2673
 
2634
2674
  function loadDiffContent(file) {
2635
2675
  var area = document.getElementById('git-diff-content-area');
2636
- area.innerHTML = '<div class="git-diff-loading">加载 diff...</div>';
2637
-
2638
- fetch(basePath + '/api/git-diff?files=' + encodeURIComponent(file))
2639
- .then(function(r) { return r.json(); })
2640
- .then(function(data) {
2641
- if (!data.diffs || !data.diffs[0]) {
2642
- area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
2643
- return;
2644
- }
2645
- var d = data.diffs[0];
2646
- var html = '<div class="git-diff-content-header">';
2647
- html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
2648
- html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
2649
- html += '</div>';
2650
- html += '<div class="git-diff-content-scroll">';
2651
-
2652
- if (d.is_binary) {
2653
- html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
2654
- } else if (d.is_large) {
2655
- html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
2656
- } else if (d.unified_diff) {
2657
- html += renderUnifiedDiff(d.unified_diff);
2658
- } else {
2659
- html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
2660
- }
2676
+ var d = diffChanges.find(function(c) { return c.file === file; });
2677
+ if (!d) {
2678
+ area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
2679
+ return;
2680
+ }
2681
+ var html = '<div class="git-diff-content-header">';
2682
+ html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
2683
+ html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
2684
+ html += '</div>';
2685
+ html += '<div class="git-diff-content-scroll">';
2686
+
2687
+ if (d.is_binary) {
2688
+ html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
2689
+ } else if (d.is_large) {
2690
+ html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
2691
+ } else if (d.unified_diff) {
2692
+ html += renderUnifiedDiff(d.unified_diff);
2693
+ } else {
2694
+ html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
2695
+ }
2661
2696
 
2662
- html += '</div>';
2663
- area.innerHTML = html;
2664
- })
2665
- .catch(function(err) {
2666
- area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
2667
- });
2697
+ html += '</div>';
2698
+ area.innerHTML = html;
2668
2699
  }
2669
2700
 
2670
2701
  function renderUnifiedDiff(diffText) {
@@ -2697,13 +2728,13 @@
2697
2728
  html += '<tr class="diff-line diff-line-add">';
2698
2729
  html += '<td class="diff-line-num"></td>';
2699
2730
  html += '<td class="diff-line-num">' + newLine + '</td>';
2700
- html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2731
+ html += '<td class="diff-line-content"><span class="diff-sign">+</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
2701
2732
  newLine++;
2702
2733
  } else if (line.startsWith('-')) {
2703
2734
  html += '<tr class="diff-line diff-line-del">';
2704
2735
  html += '<td class="diff-line-num">' + oldLine + '</td>';
2705
2736
  html += '<td class="diff-line-num"></td>';
2706
- html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
2737
+ html += '<td class="diff-line-content"><span class="diff-sign">-</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
2707
2738
  oldLine++;
2708
2739
  } else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
2709
2740
  html += '<tr class="diff-line">';
@@ -2811,6 +2842,15 @@
2811
2842
  });
2812
2843
  }
2813
2844
 
2845
+ function formatDocContent(text) {
2846
+ if (!text) return '';
2847
+ try {
2848
+ return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
2849
+ } catch (e) {
2850
+ return '<pre>' + escapeHtml(text) + '</pre>';
2851
+ }
2852
+ }
2853
+
2814
2854
  function loadDocContent(file) {
2815
2855
  var area = document.getElementById('docs-content-area');
2816
2856
  area.innerHTML = '<div class="docs-loading">加载中...</div>';
@@ -2821,7 +2861,7 @@
2821
2861
  area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">' + escapeHtml(data.error) + '</div>';
2822
2862
  return;
2823
2863
  }
2824
- area.innerHTML = '<pre>' + escapeHtml(data.content) + '</pre>';
2864
+ area.innerHTML = '<div class="docs-md-content">' + formatDocContent(data.content) + '</div>';
2825
2865
  })
2826
2866
  .catch(function() {
2827
2867
  area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.48",
3
+ "version": "2.6.49",
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
@@ -655,7 +655,7 @@ const requestHandler = async (req, res) => {
655
655
  return;
656
656
  }
657
657
 
658
- // API: 获取 git status
658
+ // API: 获取 git status(含每个文件的 unified_diff,批量获取优化)
659
659
  if (req.url === '/api/git-status') {
660
660
  res.writeHead(200, {
661
661
  'Content-Type': 'application/json',
@@ -663,13 +663,76 @@ const requestHandler = async (req, res) => {
663
663
  });
664
664
  try {
665
665
  const gitCwd = process.env.PROJECT_DIR || process.cwd();
666
- const { stdout } = await execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain'], {
667
- cwd: gitCwd, encoding: 'utf-8', timeout: 60000,
668
- });
669
- const changes = stdout.split('\n').filter(Boolean).map(line => ({
666
+
667
+ // 并行执行: git status + git diff --numstat + git diff (批量获取)
668
+ const [statusResult, numstatResult, diffResult] = await Promise.all([
669
+ execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }),
670
+ execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--numstat', 'HEAD'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }).catch(() => ({ stdout: '' })),
671
+ execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', 'HEAD'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000, maxBuffer: 10 * 1024 * 1024 }).catch(e => ({ stdout: e.stdout || '' })),
672
+ ]);
673
+
674
+ const changes = statusResult.stdout.split('\n').filter(Boolean).map(line => ({
670
675
  status: line.substring(0, 2).trim(),
671
676
  file: line.substring(3),
672
677
  })).filter(c => !/^core-/.test(c.file));
678
+
679
+ // 解析 numstat 识别二进制文件
680
+ const binaryFiles = new Set();
681
+ numstatResult.stdout.split('\n').filter(Boolean).forEach(line => {
682
+ if (line.startsWith('-\t-\t')) binaryFiles.add(line.split('\t')[2]);
683
+ });
684
+
685
+ // 将批量 diff 输出按文件拆分
686
+ const diffMap = {};
687
+ const diffParts = diffResult.stdout.split(/^diff --git /m);
688
+ for (let i = 1; i < diffParts.length; i++) {
689
+ const part = diffParts[i];
690
+ // 提取文件名: "a/path b/path\n..."
691
+ const firstLine = part.substring(0, part.indexOf('\n'));
692
+ const bMatch = firstLine.match(/ b\/(.+)$/);
693
+ if (bMatch) diffMap[bMatch[1]] = 'diff --git ' + part;
694
+ }
695
+
696
+ // 填充每个文件的 diff 信息
697
+ const untrackedFiles = [];
698
+ for (const c of changes) {
699
+ if (c.file.includes('..') || c.file.startsWith('/')) continue;
700
+ c.is_new = c.status === 'A' || c.status === '??';
701
+ c.is_deleted = c.status === 'D';
702
+ c.is_binary = binaryFiles.has(c.file);
703
+ if (c.is_binary) continue;
704
+
705
+ // 检查大文件
706
+ if (!c.is_deleted) {
707
+ try {
708
+ const filePath = join(gitCwd, c.file);
709
+ if (existsSync(filePath)) {
710
+ const stat = statSync(filePath);
711
+ if (stat.size > 5 * 1024 * 1024) { c.is_large = true; c.size = stat.size; continue; }
712
+ }
713
+ } catch {}
714
+ }
715
+
716
+ if (c.status === '??') {
717
+ // untracked 文件需要单独处理
718
+ untrackedFiles.push(c);
719
+ } else {
720
+ c.unified_diff = diffMap[c.file] || '';
721
+ }
722
+ }
723
+
724
+ // 对 untracked 文件并行获取 diff
725
+ if (untrackedFiles.length > 0) {
726
+ await Promise.all(untrackedFiles.map(async (c) => {
727
+ try {
728
+ const { stdout: diffOut } = await execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', '--no-index', '/dev/null', c.file], { cwd: gitCwd, encoding: 'utf-8', timeout: 30000, maxBuffer: 5 * 1024 * 1024 });
729
+ c.unified_diff = diffOut;
730
+ } catch (e) {
731
+ c.unified_diff = e.stdout || '';
732
+ }
733
+ }));
734
+ }
735
+
673
736
  res.end(JSON.stringify({ changes, cwd: gitCwd }));
674
737
  } catch (err) {
675
738
  res.end(JSON.stringify({ changes: [], cwd: process.env.PROJECT_DIR || process.cwd(), error: err.message }));
@@ -1119,8 +1182,16 @@ wssInst.on('connection', (ws, req) => {
1119
1182
  }
1120
1183
  isSwitching = false;
1121
1184
  }, 200);
1122
- } else if (outputBuffer) {
1123
- ws.send(JSON.stringify({ type: 'data', data: outputBuffer }));
1185
+ } else if (currentProcess) {
1186
+ // TUI 程序使用 alternate screen buffer,直接回放 raw buffer 容易转义序列错乱。
1187
+ // 改为发送 resize 信号让 TUI 重绘当前画面。
1188
+ // 先改为不同尺寸再改回,强制触发 SIGWINCH(相同尺寸不触发)。
1189
+ try {
1190
+ currentProcess.resize(Math.max(2, lastPtyCols - 1), lastPtyRows);
1191
+ setTimeout(() => {
1192
+ try { currentProcess.resize(lastPtyCols, lastPtyRows); } catch {}
1193
+ }, 50);
1194
+ } catch {}
1124
1195
  }
1125
1196
 
1126
1197
 
package/test-doc.md ADDED
@@ -0,0 +1,86 @@
1
+ # Claude OpenCode Viewer 使用指南
2
+
3
+ ## 简介
4
+
5
+ Claude OpenCode Viewer(COV)是一个统一的终端查看器,支持在浏览器中远程查看和操作 Claude Code 与 OpenCode 的终端会话。适用于需要在手机或其他设备上查看 AI 编程助手工作进度的场景。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install -g claude-opencode-viewer
11
+ ```
12
+
13
+ 安装完成后,可以通过以下命令启动:
14
+
15
+ ```bash
16
+ cov
17
+ ```
18
+
19
+ 默认监听端口 7008,PC 模式使用 `--pc` 参数启动。
20
+
21
+ ## 功能特性
22
+
23
+ ### 终端查看
24
+
25
+ 支持实时查看终端输出,基于 xterm.js 实现完整的终端模拟,包括:
26
+
27
+ - 颜色渲染
28
+ - Unicode 字符支持
29
+ - WebGL 加速渲染
30
+ - 移动端触摸滚动
31
+
32
+ ### 会话管理
33
+
34
+ 可以管理多个会话,支持以下操作:
35
+
36
+ 1. 查看历史会话列表
37
+ 2. 恢复已有会话
38
+ 3. 创建新会话
39
+ 4. 在 Claude 和 OpenCode 之间切换
40
+
41
+ ### Git 变更查看
42
+
43
+ 集成了 Git 状态查看功能,可以直接在页面上查看:
44
+
45
+ | 状态 | 含义 | 颜色 |
46
+ |------|------|------|
47
+ | M | 已修改 | 橙色 |
48
+ | A | 新增 | 绿色 |
49
+ | D | 已删除 | 红色 |
50
+ | ?? | 未跟踪 | 灰色 |
51
+
52
+ ### 文档浏览
53
+
54
+ 支持浏览项目中的 Markdown 文档,自动扫描项目目录下的 `.md` 文件并以富文本格式展示。
55
+
56
+ ## 配置说明
57
+
58
+ > 注意:以下配置需要在项目根目录下操作,确保 `PROJECT_DIR` 环境变量指向正确的项目路径。
59
+
60
+ 常用环境变量:
61
+
62
+ - `PROJECT_DIR` — 指定项目工作目录
63
+ - `PORT` — 自定义端口号
64
+ - `COV_MODE` — 默认启动模式(claude / opencode)
65
+
66
+ ## 常见问题
67
+
68
+ ### 连接断开怎么办?
69
+
70
+ 页面会自动显示重连提示并尝试重新连接。如果持续无法连接,请检查:
71
+
72
+ 1. 服务进程是否仍在运行
73
+ 2. 网络是否可达
74
+ 3. 端口是否被占用
75
+
76
+ ### 移动端键盘遮挡问题
77
+
78
+ 在 iOS 设备上,系统会自动调整终端高度以适应键盘弹出。如果遇到显示异常,可以尝试旋转屏幕后再旋转回来。
79
+
80
+ ## 更新日志
81
+
82
+ **v2.6.48** — 修复重连内容重复、模式切换黑屏问题
83
+
84
+ **v2.6.47** — 添加 PC 端重连覆盖层
85
+
86
+ **v2.6.46** — 移动端键盘交互优化