claude-opencode-viewer 2.6.48 → 2.6.50
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 +81 -34
- package/index.html +231 -104
- package/package.json +1 -1
- package/server.js +101 -14
- package/test-doc.md +86 -0
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
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
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 = '<
|
|
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
|
@@ -11,24 +11,62 @@
|
|
|
11
11
|
50% { content: '..'; }
|
|
12
12
|
75% { content: '...'; }
|
|
13
13
|
}
|
|
14
|
-
#
|
|
15
|
-
|
|
14
|
+
#init-overlay {
|
|
15
|
+
display: none;
|
|
16
|
+
position: absolute;
|
|
16
17
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
17
18
|
background: #0a0a0a;
|
|
18
19
|
color: #ccc;
|
|
19
|
-
display: flex;
|
|
20
20
|
align-items: center;
|
|
21
21
|
justify-content: center;
|
|
22
22
|
font-size: 16px;
|
|
23
23
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
24
24
|
letter-spacing: 1px;
|
|
25
|
-
z-index:
|
|
25
|
+
z-index: 100;
|
|
26
|
+
}
|
|
27
|
+
#init-overlay::after {
|
|
28
|
+
content: '';
|
|
29
|
+
animation: loading-dots 1.2s steps(4, end) infinite;
|
|
30
|
+
}
|
|
31
|
+
#init-overlay.visible { display: flex; }
|
|
32
|
+
#reconnect-overlay {
|
|
33
|
+
display: none;
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
36
|
+
background: rgba(0,0,0,0.85);
|
|
37
|
+
color: #ccc;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
font-size: 15px;
|
|
41
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
42
|
+
letter-spacing: 1px;
|
|
43
|
+
z-index: 100;
|
|
44
|
+
}
|
|
45
|
+
#reconnect-overlay.visible { display: flex; }
|
|
46
|
+
#reconnect-overlay::after {
|
|
47
|
+
content: '';
|
|
48
|
+
animation: loading-dots 1.2s steps(4, end) infinite;
|
|
49
|
+
}
|
|
50
|
+
#restore-overlay {
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
53
|
+
background: rgba(0,0,0,0.85);
|
|
54
|
+
color: #ccc;
|
|
55
|
+
display: none;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
font-size: 15px;
|
|
59
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
60
|
+
letter-spacing: 1px;
|
|
61
|
+
z-index: 100;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 12px;
|
|
26
64
|
}
|
|
27
|
-
#
|
|
65
|
+
#restore-overlay.visible { display: flex; }
|
|
66
|
+
#restore-overlay::after {
|
|
28
67
|
content: '';
|
|
29
68
|
animation: loading-dots 1.2s steps(4, end) infinite;
|
|
30
69
|
}
|
|
31
|
-
#loading-overlay.hidden { display: none !important; }
|
|
32
70
|
</style>
|
|
33
71
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.min.css" media="print" onload="this.media='all'">
|
|
34
72
|
<style>
|
|
@@ -393,6 +431,8 @@
|
|
|
393
431
|
padding: 4px 8px;
|
|
394
432
|
touch-action: none;
|
|
395
433
|
overscroll-behavior: contain;
|
|
434
|
+
background: #000;
|
|
435
|
+
background: #000;
|
|
396
436
|
}
|
|
397
437
|
|
|
398
438
|
#terminal.transitioning .xterm { visibility: hidden; }
|
|
@@ -809,6 +849,39 @@
|
|
|
809
849
|
line-height: 1.6;
|
|
810
850
|
color: #d4d4d4;
|
|
811
851
|
}
|
|
852
|
+
.docs-md-content {
|
|
853
|
+
font-size: 13px; line-height: 1.7; color: #d4d4d4; word-break: break-word;
|
|
854
|
+
}
|
|
855
|
+
.docs-md-content h1, .docs-md-content h2, .docs-md-content h3 {
|
|
856
|
+
margin: 16px 0 8px; color: #e0e0e0; border-bottom: 1px solid #333; padding-bottom: 4px;
|
|
857
|
+
}
|
|
858
|
+
.docs-md-content h1 { font-size: 1.4em; }
|
|
859
|
+
.docs-md-content h2 { font-size: 1.2em; }
|
|
860
|
+
.docs-md-content h3 { font-size: 1.05em; }
|
|
861
|
+
.docs-md-content p { margin: 8px 0; }
|
|
862
|
+
.docs-md-content pre {
|
|
863
|
+
background: #1a1a1a; border: 1px solid #333; border-radius: 4px;
|
|
864
|
+
padding: 10px; overflow-x: auto; margin: 8px 0; white-space: pre-wrap; word-break: break-word;
|
|
865
|
+
}
|
|
866
|
+
.docs-md-content code {
|
|
867
|
+
background: #1a1a1a; padding: 1px 4px; border-radius: 3px; font-size: 12px;
|
|
868
|
+
font-family: Menlo, Monaco, monospace;
|
|
869
|
+
}
|
|
870
|
+
.docs-md-content pre code { background: none; padding: 0; }
|
|
871
|
+
.docs-md-content ul, .docs-md-content ol { margin: 8px 0; padding-left: 20px; }
|
|
872
|
+
.docs-md-content li { margin: 4px 0; }
|
|
873
|
+
.docs-md-content blockquote {
|
|
874
|
+
border-left: 3px solid #444; margin: 8px 0; padding: 4px 12px; color: #999;
|
|
875
|
+
}
|
|
876
|
+
.docs-md-content table { border-collapse: collapse; margin: 8px 0; width: 100%; }
|
|
877
|
+
.docs-md-content th, .docs-md-content td {
|
|
878
|
+
border: 1px solid #333; padding: 6px 10px; text-align: left;
|
|
879
|
+
}
|
|
880
|
+
.docs-md-content th { background: #1a1a1a; }
|
|
881
|
+
.docs-md-content a { color: #58a6ff; text-decoration: none; }
|
|
882
|
+
.docs-md-content a:hover { text-decoration: underline; }
|
|
883
|
+
.docs-md-content img { max-width: 100%; }
|
|
884
|
+
.docs-md-content hr { border: none; border-top: 1px solid #333; margin: 12px 0; }
|
|
812
885
|
.docs-placeholder {
|
|
813
886
|
flex: 1;
|
|
814
887
|
display: flex;
|
|
@@ -874,6 +947,13 @@
|
|
|
874
947
|
color: #ffa198;
|
|
875
948
|
}
|
|
876
949
|
|
|
950
|
+
.diff-sign {
|
|
951
|
+
display: inline-block;
|
|
952
|
+
width: 12px;
|
|
953
|
+
font-weight: bold;
|
|
954
|
+
user-select: none;
|
|
955
|
+
}
|
|
956
|
+
|
|
877
957
|
.diff-line-hunk {
|
|
878
958
|
background: rgba(56, 139, 253, 0.1);
|
|
879
959
|
}
|
|
@@ -894,7 +974,6 @@
|
|
|
894
974
|
</head>
|
|
895
975
|
<body>
|
|
896
976
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
897
|
-
<div id="loading-overlay">正在初始化</div>
|
|
898
977
|
<div id="layout">
|
|
899
978
|
<div id="header">
|
|
900
979
|
<div style="display: flex; gap: 4px; align-items: center; overflow-x: auto; flex: 1; min-width: 0;">
|
|
@@ -1080,6 +1159,9 @@
|
|
|
1080
1159
|
<div id="terminal-container">
|
|
1081
1160
|
<div id="terminal" style="position:relative;">
|
|
1082
1161
|
<div id="switch-overlay">正在切换</div>
|
|
1162
|
+
<div id="restore-overlay">正在恢复会话</div>
|
|
1163
|
+
<div id="init-overlay">正在初始化</div>
|
|
1164
|
+
<div id="reconnect-overlay">连接断开,正在重连</div>
|
|
1083
1165
|
</div>
|
|
1084
1166
|
<div id="virtual-keybar">
|
|
1085
1167
|
<div class="virtual-key" data-key="up">↑</div>
|
|
@@ -1269,24 +1351,31 @@
|
|
|
1269
1351
|
// iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
|
|
1270
1352
|
// 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
|
|
1271
1353
|
// 事件同步可见区域的高度和偏移,用 fixed 定位将布局锁定在可见区域内。
|
|
1272
|
-
|
|
1354
|
+
var mobileKbOpen = false;
|
|
1355
|
+
if (isMobile && window.visualViewport) {
|
|
1273
1356
|
var layoutEl = document.getElementById('layout');
|
|
1357
|
+
var initVVH = window.visualViewport.height;
|
|
1274
1358
|
var onVVChange = function() {
|
|
1275
1359
|
if (!layoutEl) return;
|
|
1276
1360
|
var vv = window.visualViewport;
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1361
|
+
if ((initVVH - vv.height) > 50) {
|
|
1362
|
+
mobileKbOpen = true;
|
|
1363
|
+
layoutEl.style.height = vv.height + 'px';
|
|
1364
|
+
layoutEl.style.top = vv.offsetTop + 'px';
|
|
1365
|
+
layoutEl.style.bottom = 'auto';
|
|
1366
|
+
} else {
|
|
1367
|
+
mobileKbOpen = false;
|
|
1368
|
+
layoutEl.style.height = '';
|
|
1369
|
+
layoutEl.style.top = '0';
|
|
1370
|
+
layoutEl.style.bottom = '0';
|
|
1371
|
+
}
|
|
1283
1372
|
};
|
|
1284
1373
|
window.visualViewport.addEventListener('resize', onVVChange);
|
|
1285
1374
|
window.visualViewport.addEventListener('scroll', onVVChange);
|
|
1286
|
-
onVVChange();
|
|
1287
1375
|
}
|
|
1288
1376
|
|
|
1289
|
-
//
|
|
1377
|
+
// 移动端固定尺寸计算:用屏幕全高减去固定区域,保证全屏时也填满
|
|
1378
|
+
var lastMobileCols = 0, lastMobileRows = 0;
|
|
1290
1379
|
function mobileFixedResize() {
|
|
1291
1380
|
if (!term) return;
|
|
1292
1381
|
var cellDims = getCellDims();
|
|
@@ -1310,9 +1399,12 @@
|
|
|
1310
1399
|
var newCellDims = getCellDims();
|
|
1311
1400
|
var lineHeight = (newCellDims && newCellDims.height) || cellDims.height;
|
|
1312
1401
|
var rows = Math.max(5, Math.min(Math.floor(availH / lineHeight), 100));
|
|
1402
|
+
// 尺寸未变时只 resize xterm 本地,不通知服务端(避免重复 SIGWINCH)
|
|
1403
|
+
var sizeChanged = !(MOBILE_COLS === lastMobileCols && rows === lastMobileRows);
|
|
1404
|
+
lastMobileCols = MOBILE_COLS;
|
|
1405
|
+
lastMobileRows = rows;
|
|
1313
1406
|
term.resize(MOBILE_COLS, rows);
|
|
1314
|
-
|
|
1315
|
-
if (ws && ws.readyState === 1 && !isTransitioning) {
|
|
1407
|
+
if (sizeChanged && ws && ws.readyState === 1) {
|
|
1316
1408
|
ws.send(JSON.stringify({ type: 'resize', cols: MOBILE_COLS, rows: rows, mobile: true }));
|
|
1317
1409
|
}
|
|
1318
1410
|
});
|
|
@@ -1638,57 +1730,67 @@
|
|
|
1638
1730
|
}, 50);
|
|
1639
1731
|
});
|
|
1640
1732
|
|
|
1733
|
+
var restoreOverlay = document.getElementById('restore-overlay');
|
|
1734
|
+
function showRestoreOverlay() {
|
|
1735
|
+
if (restoreOverlay) restoreOverlay.classList.add('visible');
|
|
1736
|
+
}
|
|
1737
|
+
function hideRestoreOverlay() {
|
|
1738
|
+
if (restoreOverlay) restoreOverlay.classList.remove('visible');
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
var waitingInitData = false;
|
|
1742
|
+
var initDataTimer = null;
|
|
1743
|
+
|
|
1744
|
+
function showInitOverlay(text) {
|
|
1745
|
+
var ov = document.getElementById('init-overlay');
|
|
1746
|
+
if (ov) {
|
|
1747
|
+
ov.textContent = text || '正在初始化';
|
|
1748
|
+
ov.classList.add('visible');
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function hideInitOverlay() {
|
|
1752
|
+
var ov = document.getElementById('init-overlay');
|
|
1753
|
+
if (ov) ov.classList.remove('visible');
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1641
1756
|
function connect() {
|
|
1757
|
+
if (ws && ws.readyState <= 1) return;
|
|
1642
1758
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1643
1759
|
ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
|
|
1644
1760
|
|
|
1645
1761
|
ws.onopen = function() {
|
|
1646
|
-
|
|
1762
|
+
document.getElementById('reconnect-overlay').classList.remove('visible');
|
|
1763
|
+
lastMobileCols = 0;
|
|
1764
|
+
lastMobileRows = 0;
|
|
1647
1765
|
resize();
|
|
1648
1766
|
rebindTouchScroll();
|
|
1649
1767
|
};
|
|
1650
1768
|
|
|
1651
1769
|
ws.onclose = function() {
|
|
1652
1770
|
ws = null;
|
|
1653
|
-
|
|
1654
|
-
term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
|
|
1771
|
+
document.getElementById('reconnect-overlay').classList.add('visible');
|
|
1655
1772
|
setTimeout(connect, 2000);
|
|
1656
1773
|
};
|
|
1657
1774
|
|
|
1658
|
-
var loadingOverlay = document.getElementById('loading-overlay');
|
|
1659
|
-
var loadingShowTime = Date.now();
|
|
1660
|
-
var loadingMinMs = 600;
|
|
1661
|
-
var loadingHideTimer = null;
|
|
1662
|
-
function setLoadingText(text) {
|
|
1663
|
-
if (loadingOverlay && !loadingOverlay.classList.contains('hidden')) {
|
|
1664
|
-
loadingOverlay.textContent = text;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
function hideLoading() {
|
|
1668
|
-
if (!loadingOverlay || loadingOverlay.classList.contains('hidden')) return;
|
|
1669
|
-
if (loadingHideTimer) return; // 已在等待中
|
|
1670
|
-
var elapsed = Date.now() - loadingShowTime;
|
|
1671
|
-
if (elapsed >= loadingMinMs) {
|
|
1672
|
-
loadingOverlay.classList.add('hidden');
|
|
1673
|
-
} else {
|
|
1674
|
-
loadingHideTimer = setTimeout(function() {
|
|
1675
|
-
loadingOverlay.classList.add('hidden');
|
|
1676
|
-
}, loadingMinMs - elapsed);
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
1775
|
ws.onmessage = function(e) {
|
|
1681
1776
|
try {
|
|
1682
1777
|
var msg = JSON.parse(e.data);
|
|
1683
1778
|
if (msg.type === 'data') {
|
|
1684
|
-
hideLoading();
|
|
1685
1779
|
if (isTransitioning) {
|
|
1686
1780
|
term.write(msg.data);
|
|
1687
1781
|
clearTimeout(transitionEndTimer);
|
|
1688
1782
|
transitionEndTimer = setTimeout(function() {
|
|
1689
1783
|
terminalEl.classList.remove('transitioning');
|
|
1690
1784
|
isTransitioning = false;
|
|
1691
|
-
},
|
|
1785
|
+
}, 500);
|
|
1786
|
+
} else if (waitingInitData) {
|
|
1787
|
+
throttledWrite(msg.data);
|
|
1788
|
+
clearTimeout(initDataTimer);
|
|
1789
|
+
initDataTimer = setTimeout(function() {
|
|
1790
|
+
hideInitOverlay();
|
|
1791
|
+
hideRestoreOverlay();
|
|
1792
|
+
waitingInitData = false;
|
|
1793
|
+
}, 500);
|
|
1692
1794
|
} else if (!isCreatingNewSession) {
|
|
1693
1795
|
throttledWrite(msg.data);
|
|
1694
1796
|
}
|
|
@@ -1700,31 +1802,25 @@
|
|
|
1700
1802
|
}
|
|
1701
1803
|
}
|
|
1702
1804
|
else if (msg.type === 'state') {
|
|
1703
|
-
// 重连时清掉旧终端内容(如"连接断开"提示)
|
|
1704
1805
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1705
1806
|
writeBuffer = '';
|
|
1706
1807
|
term.reset();
|
|
1707
1808
|
term.clear();
|
|
1708
|
-
// 同步模式 UI
|
|
1709
1809
|
if (msg.mode) {
|
|
1710
1810
|
currentMode = msg.mode;
|
|
1711
1811
|
modeSelect.value = msg.mode;
|
|
1712
1812
|
}
|
|
1713
1813
|
if (msg.running) {
|
|
1714
|
-
setLoadingText('正在连接');
|
|
1715
|
-
hideLoading();
|
|
1716
1814
|
preloadData();
|
|
1717
1815
|
}
|
|
1718
|
-
// 服务端还没启动进程时,查最近会话并自动恢复
|
|
1719
1816
|
if (!msg.running && !mobileInitSent) {
|
|
1720
1817
|
mobileInitSent = true;
|
|
1721
|
-
|
|
1818
|
+
showInitOverlay('正在查询会话');
|
|
1722
1819
|
fetch('/api/last-sessions')
|
|
1723
1820
|
.then(function(r) { return r.json(); })
|
|
1724
1821
|
.then(function(data) {
|
|
1725
1822
|
var oc = data.opencode;
|
|
1726
1823
|
var cl = data.claude;
|
|
1727
|
-
// 取 mtime 最新的那个
|
|
1728
1824
|
var useOc = oc && (!cl || oc.mtime > cl.mtime);
|
|
1729
1825
|
var mode = useOc ? 'opencode' : 'claude';
|
|
1730
1826
|
var sessionId = useOc ? (oc && oc.id) : (cl && cl.id);
|
|
@@ -1733,7 +1829,7 @@
|
|
|
1733
1829
|
claudeProject = cl.project;
|
|
1734
1830
|
}
|
|
1735
1831
|
currentMode = mode;
|
|
1736
|
-
|
|
1832
|
+
showInitOverlay('正在启动 ' + (mode === 'claude' ? 'Claude' : 'OpenCode'));
|
|
1737
1833
|
ws.send(JSON.stringify({ type: 'init', mode: mode, sessionId: sessionId || null }));
|
|
1738
1834
|
})
|
|
1739
1835
|
.catch(function() {
|
|
@@ -1758,7 +1854,7 @@
|
|
|
1758
1854
|
term.clear();
|
|
1759
1855
|
}
|
|
1760
1856
|
else if (msg.type === 'restored') {
|
|
1761
|
-
|
|
1857
|
+
waitingInitData = true;
|
|
1762
1858
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1763
1859
|
writeBuffer = '';
|
|
1764
1860
|
term.reset();
|
|
@@ -1769,14 +1865,17 @@
|
|
|
1769
1865
|
}
|
|
1770
1866
|
else if (msg.type === 'restore-error') {
|
|
1771
1867
|
isTransitioning = false;
|
|
1868
|
+
hideInitOverlay();
|
|
1869
|
+
hideRestoreOverlay();
|
|
1772
1870
|
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1773
1871
|
}
|
|
1774
1872
|
else if (msg.type === 'started') {
|
|
1775
|
-
|
|
1873
|
+
waitingInitData = true;
|
|
1776
1874
|
rebindTouchScroll();
|
|
1777
1875
|
preloadData();
|
|
1778
1876
|
}
|
|
1779
1877
|
else if (msg.type === 'new-session-ok') {
|
|
1878
|
+
waitingInitData = true;
|
|
1780
1879
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1781
1880
|
writeBuffer = '';
|
|
1782
1881
|
term.reset();
|
|
@@ -1784,11 +1883,9 @@
|
|
|
1784
1883
|
term.write(msg.buffer);
|
|
1785
1884
|
}
|
|
1786
1885
|
isCreatingNewSession = false;
|
|
1787
|
-
isTransitioning = false;
|
|
1788
1886
|
}
|
|
1789
1887
|
else if (msg.type === 'new-session-error') {
|
|
1790
1888
|
isCreatingNewSession = false;
|
|
1791
|
-
isTransitioning = false;
|
|
1792
1889
|
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1793
1890
|
}
|
|
1794
1891
|
} catch(err) {}
|
|
@@ -1804,24 +1901,56 @@
|
|
|
1804
1901
|
}, 2000);
|
|
1805
1902
|
}
|
|
1806
1903
|
|
|
1807
|
-
window.addEventListener('resize', resize);
|
|
1808
1904
|
if (isMobile) {
|
|
1905
|
+
// 移动端:不监听 window resize(键盘弹出/收起会触发导致重复内容)
|
|
1906
|
+
// 仅监听屏幕旋转
|
|
1809
1907
|
window.addEventListener('orientationchange', function() {
|
|
1810
1908
|
setTimeout(resize, 200);
|
|
1811
1909
|
});
|
|
1910
|
+
} else {
|
|
1911
|
+
window.addEventListener('resize', resize);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// 监听终端容器大小变化(全屏切换、地址栏收起等),键盘弹出时跳过
|
|
1915
|
+
// 只做本地 term.resize 填充空间,不发服务端,避免 SIGWINCH 导致重复内容
|
|
1916
|
+
if (isMobile && typeof ResizeObserver !== 'undefined') {
|
|
1917
|
+
var termResizeTimer = null;
|
|
1918
|
+
new ResizeObserver(function() {
|
|
1919
|
+
if (mobileKbOpen || !term) return;
|
|
1920
|
+
if (termResizeTimer) clearTimeout(termResizeTimer);
|
|
1921
|
+
termResizeTimer = setTimeout(function() {
|
|
1922
|
+
var cellDims = getCellDims();
|
|
1923
|
+
if (!cellDims || !cellDims.height) return;
|
|
1924
|
+
var termEl = document.getElementById('terminal');
|
|
1925
|
+
if (!termEl) return;
|
|
1926
|
+
var rows = Math.max(5, Math.min(Math.floor(termEl.clientHeight / cellDims.height), 100));
|
|
1927
|
+
if (rows !== lastMobileRows) {
|
|
1928
|
+
lastMobileRows = rows;
|
|
1929
|
+
lastMobileCols = MOBILE_COLS;
|
|
1930
|
+
term.resize(MOBILE_COLS, rows);
|
|
1931
|
+
}
|
|
1932
|
+
}, 150);
|
|
1933
|
+
}).observe(document.getElementById('terminal'));
|
|
1812
1934
|
}
|
|
1813
1935
|
|
|
1814
|
-
//
|
|
1936
|
+
// 页面卸载前保存输入缓存,并通知服务端退出
|
|
1815
1937
|
window.addEventListener('beforeunload', function() {
|
|
1816
1938
|
if (currentInputBuffer) {
|
|
1817
1939
|
saveInputCache();
|
|
1818
1940
|
}
|
|
1941
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1942
|
+
ws.send(JSON.stringify({ type: 'quit' }));
|
|
1943
|
+
}
|
|
1819
1944
|
});
|
|
1820
1945
|
|
|
1821
|
-
// 页面可见性变化时保存缓存
|
|
1946
|
+
// 页面可见性变化时保存缓存 + 尝试重连
|
|
1822
1947
|
document.addEventListener('visibilitychange', function() {
|
|
1823
|
-
if (document.hidden
|
|
1824
|
-
saveInputCache();
|
|
1948
|
+
if (document.hidden) {
|
|
1949
|
+
if (currentInputBuffer) saveInputCache();
|
|
1950
|
+
} else {
|
|
1951
|
+
if (!ws || ws.readyState > 1) {
|
|
1952
|
+
connect();
|
|
1953
|
+
}
|
|
1825
1954
|
}
|
|
1826
1955
|
});
|
|
1827
1956
|
|
|
@@ -2057,27 +2186,23 @@
|
|
|
2057
2186
|
|
|
2058
2187
|
|
|
2059
2188
|
function loadSession(session) {
|
|
2060
|
-
console.log('[restore] 直接恢复会话:', session.title);
|
|
2061
2189
|
currentSessionData = session;
|
|
2062
|
-
|
|
2063
|
-
// 关闭历史栏
|
|
2064
2190
|
toggleHistoryBar();
|
|
2065
|
-
|
|
2066
|
-
// 阻止旧数据写入
|
|
2191
|
+
showRestoreOverlay();
|
|
2067
2192
|
isTransitioning = true;
|
|
2068
2193
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2069
2194
|
writeBuffer = '';
|
|
2070
2195
|
term.reset();
|
|
2071
|
-
|
|
2072
2196
|
if (ws && ws.readyState === 1) {
|
|
2073
2197
|
if (currentMode !== 'opencode') {
|
|
2074
2198
|
isTransitioning = false;
|
|
2199
|
+
hideRestoreOverlay();
|
|
2075
2200
|
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
2076
2201
|
return;
|
|
2077
2202
|
}
|
|
2078
|
-
// 静默恢复
|
|
2079
2203
|
ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
|
|
2080
2204
|
} else {
|
|
2205
|
+
hideRestoreOverlay();
|
|
2081
2206
|
term.write('错误: WebSocket 未连接\r\n');
|
|
2082
2207
|
}
|
|
2083
2208
|
}
|
|
@@ -2258,10 +2383,10 @@
|
|
|
2258
2383
|
}
|
|
2259
2384
|
|
|
2260
2385
|
function restoreClaudeSession(sessionId, project) {
|
|
2261
|
-
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
2262
2386
|
claudeSessionId = sessionId;
|
|
2263
2387
|
if (project) claudeProject = project;
|
|
2264
2388
|
toggleHistoryBar();
|
|
2389
|
+
showRestoreOverlay();
|
|
2265
2390
|
isTransitioning = true;
|
|
2266
2391
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2267
2392
|
writeBuffer = '';
|
|
@@ -2269,11 +2394,13 @@
|
|
|
2269
2394
|
if (ws && ws.readyState === 1) {
|
|
2270
2395
|
if (currentMode !== 'claude') {
|
|
2271
2396
|
isTransitioning = false;
|
|
2397
|
+
hideRestoreOverlay();
|
|
2272
2398
|
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
2273
2399
|
return;
|
|
2274
2400
|
}
|
|
2275
2401
|
ws.send(JSON.stringify({ type: 'restore', sessionId: sessionId }));
|
|
2276
2402
|
} else {
|
|
2403
|
+
hideRestoreOverlay();
|
|
2277
2404
|
term.write('错误: WebSocket 未连接\r\n');
|
|
2278
2405
|
}
|
|
2279
2406
|
}
|
|
@@ -2633,38 +2760,29 @@
|
|
|
2633
2760
|
|
|
2634
2761
|
function loadDiffContent(file) {
|
|
2635
2762
|
var area = document.getElementById('git-diff-content-area');
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
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
|
-
}
|
|
2763
|
+
var d = diffChanges.find(function(c) { return c.file === file; });
|
|
2764
|
+
if (!d) {
|
|
2765
|
+
area.innerHTML = '<div class="git-diff-error">无 diff 数据</div>';
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
var html = '<div class="git-diff-content-header">';
|
|
2769
|
+
html += '<span class="git-diff-content-path">' + escapeHtml(d.file) + '</span>';
|
|
2770
|
+
html += '<span class="git-diff-badge">' + (d.is_new ? 'NEW' : d.is_deleted ? 'DEL' : 'DIFF') + '</span>';
|
|
2771
|
+
html += '</div>';
|
|
2772
|
+
html += '<div class="git-diff-content-scroll">';
|
|
2773
|
+
|
|
2774
|
+
if (d.is_binary) {
|
|
2775
|
+
html += '<div class="git-diff-loading" style="font-style:italic;">二进制文件</div>';
|
|
2776
|
+
} else if (d.is_large) {
|
|
2777
|
+
html += '<div class="git-diff-loading" style="color:#e2c08d;">文件过大: ' + (d.size / (1024 * 1024)).toFixed(2) + ' MB</div>';
|
|
2778
|
+
} else if (d.unified_diff) {
|
|
2779
|
+
html += renderUnifiedDiff(d.unified_diff);
|
|
2780
|
+
} else {
|
|
2781
|
+
html += '<div class="git-diff-loading" style="color:#666;">无变更内容</div>';
|
|
2782
|
+
}
|
|
2661
2783
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
})
|
|
2665
|
-
.catch(function(err) {
|
|
2666
|
-
area.innerHTML = '<div class="git-diff-error">加载失败: ' + escapeHtml(err.message) + '</div>';
|
|
2667
|
-
});
|
|
2784
|
+
html += '</div>';
|
|
2785
|
+
area.innerHTML = html;
|
|
2668
2786
|
}
|
|
2669
2787
|
|
|
2670
2788
|
function renderUnifiedDiff(diffText) {
|
|
@@ -2697,13 +2815,13 @@
|
|
|
2697
2815
|
html += '<tr class="diff-line diff-line-add">';
|
|
2698
2816
|
html += '<td class="diff-line-num"></td>';
|
|
2699
2817
|
html += '<td class="diff-line-num">' + newLine + '</td>';
|
|
2700
|
-
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2818
|
+
html += '<td class="diff-line-content"><span class="diff-sign">+</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2701
2819
|
newLine++;
|
|
2702
2820
|
} else if (line.startsWith('-')) {
|
|
2703
2821
|
html += '<tr class="diff-line diff-line-del">';
|
|
2704
2822
|
html += '<td class="diff-line-num">' + oldLine + '</td>';
|
|
2705
2823
|
html += '<td class="diff-line-num"></td>';
|
|
2706
|
-
html += '<td class="diff-line-content">' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2824
|
+
html += '<td class="diff-line-content"><span class="diff-sign">-</span>' + escapeHtml(line.substring(1)) + '</td></tr>';
|
|
2707
2825
|
oldLine++;
|
|
2708
2826
|
} else if (line.startsWith(' ') || (line === '' && i < lines.length - 1)) {
|
|
2709
2827
|
html += '<tr class="diff-line">';
|
|
@@ -2811,6 +2929,15 @@
|
|
|
2811
2929
|
});
|
|
2812
2930
|
}
|
|
2813
2931
|
|
|
2932
|
+
function formatDocContent(text) {
|
|
2933
|
+
if (!text) return '';
|
|
2934
|
+
try {
|
|
2935
|
+
return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
|
|
2936
|
+
} catch (e) {
|
|
2937
|
+
return '<pre>' + escapeHtml(text) + '</pre>';
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2814
2941
|
function loadDocContent(file) {
|
|
2815
2942
|
var area = document.getElementById('docs-content-area');
|
|
2816
2943
|
area.innerHTML = '<div class="docs-loading">加载中...</div>';
|
|
@@ -2821,7 +2948,7 @@
|
|
|
2821
2948
|
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">' + escapeHtml(data.error) + '</div>';
|
|
2822
2949
|
return;
|
|
2823
2950
|
}
|
|
2824
|
-
area.innerHTML = '<
|
|
2951
|
+
area.innerHTML = '<div class="docs-md-content">' + formatDocContent(data.content) + '</div>';
|
|
2825
2952
|
})
|
|
2826
2953
|
.catch(function() {
|
|
2827
2954
|
area.innerHTML = '<div class="docs-loading" style="color:#ff6b6b;">加载失败</div>';
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -434,11 +434,17 @@ function writeToPty(data) {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
function resizePty(cols, rows) {
|
|
437
|
+
const sameSize = (cols === lastPtyCols && rows === lastPtyRows);
|
|
437
438
|
lastPtyCols = cols;
|
|
438
439
|
lastPtyRows = rows;
|
|
440
|
+
if (sameSize) return;
|
|
439
441
|
[claudeProcess, opencodeProcess].forEach(proc => {
|
|
440
442
|
if (proc) {
|
|
441
|
-
try {
|
|
443
|
+
try {
|
|
444
|
+
// 先设不同尺寸再设目标尺寸,保证即使进程已是该尺寸也能触发 SIGWINCH
|
|
445
|
+
proc.resize(Math.max(2, cols - 1), rows);
|
|
446
|
+
proc.resize(cols, rows);
|
|
447
|
+
} catch {}
|
|
442
448
|
}
|
|
443
449
|
});
|
|
444
450
|
}
|
|
@@ -603,6 +609,7 @@ const requestHandler = async (req, res) => {
|
|
|
603
609
|
return;
|
|
604
610
|
}
|
|
605
611
|
|
|
612
|
+
|
|
606
613
|
// API: 获取服务端口信息
|
|
607
614
|
if (req.url === '/api/port') {
|
|
608
615
|
res.writeHead(200, {
|
|
@@ -655,7 +662,7 @@ const requestHandler = async (req, res) => {
|
|
|
655
662
|
return;
|
|
656
663
|
}
|
|
657
664
|
|
|
658
|
-
// API: 获取 git status
|
|
665
|
+
// API: 获取 git status(含每个文件的 unified_diff,批量获取优化)
|
|
659
666
|
if (req.url === '/api/git-status') {
|
|
660
667
|
res.writeHead(200, {
|
|
661
668
|
'Content-Type': 'application/json',
|
|
@@ -663,13 +670,76 @@ const requestHandler = async (req, res) => {
|
|
|
663
670
|
});
|
|
664
671
|
try {
|
|
665
672
|
const gitCwd = process.env.PROJECT_DIR || process.cwd();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
673
|
+
|
|
674
|
+
// 并行执行: git status + git diff --numstat + git diff (批量获取)
|
|
675
|
+
const [statusResult, numstatResult, diffResult] = await Promise.all([
|
|
676
|
+
execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }),
|
|
677
|
+
execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--numstat', 'HEAD'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }).catch(() => ({ stdout: '' })),
|
|
678
|
+
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 || '' })),
|
|
679
|
+
]);
|
|
680
|
+
|
|
681
|
+
const changes = statusResult.stdout.split('\n').filter(Boolean).map(line => ({
|
|
670
682
|
status: line.substring(0, 2).trim(),
|
|
671
683
|
file: line.substring(3),
|
|
672
684
|
})).filter(c => !/^core-/.test(c.file));
|
|
685
|
+
|
|
686
|
+
// 解析 numstat 识别二进制文件
|
|
687
|
+
const binaryFiles = new Set();
|
|
688
|
+
numstatResult.stdout.split('\n').filter(Boolean).forEach(line => {
|
|
689
|
+
if (line.startsWith('-\t-\t')) binaryFiles.add(line.split('\t')[2]);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// 将批量 diff 输出按文件拆分
|
|
693
|
+
const diffMap = {};
|
|
694
|
+
const diffParts = diffResult.stdout.split(/^diff --git /m);
|
|
695
|
+
for (let i = 1; i < diffParts.length; i++) {
|
|
696
|
+
const part = diffParts[i];
|
|
697
|
+
// 提取文件名: "a/path b/path\n..."
|
|
698
|
+
const firstLine = part.substring(0, part.indexOf('\n'));
|
|
699
|
+
const bMatch = firstLine.match(/ b\/(.+)$/);
|
|
700
|
+
if (bMatch) diffMap[bMatch[1]] = 'diff --git ' + part;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// 填充每个文件的 diff 信息
|
|
704
|
+
const untrackedFiles = [];
|
|
705
|
+
for (const c of changes) {
|
|
706
|
+
if (c.file.includes('..') || c.file.startsWith('/')) continue;
|
|
707
|
+
c.is_new = c.status === 'A' || c.status === '??';
|
|
708
|
+
c.is_deleted = c.status === 'D';
|
|
709
|
+
c.is_binary = binaryFiles.has(c.file);
|
|
710
|
+
if (c.is_binary) continue;
|
|
711
|
+
|
|
712
|
+
// 检查大文件
|
|
713
|
+
if (!c.is_deleted) {
|
|
714
|
+
try {
|
|
715
|
+
const filePath = join(gitCwd, c.file);
|
|
716
|
+
if (existsSync(filePath)) {
|
|
717
|
+
const stat = statSync(filePath);
|
|
718
|
+
if (stat.size > 5 * 1024 * 1024) { c.is_large = true; c.size = stat.size; continue; }
|
|
719
|
+
}
|
|
720
|
+
} catch {}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (c.status === '??') {
|
|
724
|
+
// untracked 文件需要单独处理
|
|
725
|
+
untrackedFiles.push(c);
|
|
726
|
+
} else {
|
|
727
|
+
c.unified_diff = diffMap[c.file] || '';
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 对 untracked 文件并行获取 diff
|
|
732
|
+
if (untrackedFiles.length > 0) {
|
|
733
|
+
await Promise.all(untrackedFiles.map(async (c) => {
|
|
734
|
+
try {
|
|
735
|
+
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 });
|
|
736
|
+
c.unified_diff = diffOut;
|
|
737
|
+
} catch (e) {
|
|
738
|
+
c.unified_diff = e.stdout || '';
|
|
739
|
+
}
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
|
|
673
743
|
res.end(JSON.stringify({ changes, cwd: gitCwd }));
|
|
674
744
|
} catch (err) {
|
|
675
745
|
res.end(JSON.stringify({ changes: [], cwd: process.env.PROJECT_DIR || process.cwd(), error: err.message }));
|
|
@@ -1118,15 +1188,32 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1118
1188
|
LOG('[reconnect] 重启失败:', e.message);
|
|
1119
1189
|
}
|
|
1120
1190
|
isSwitching = false;
|
|
1121
|
-
},
|
|
1122
|
-
} else if (
|
|
1123
|
-
|
|
1191
|
+
}, 100);
|
|
1192
|
+
} else if (currentProcess) {
|
|
1193
|
+
// 连接已运行的进程:发送缓冲区内容 + 重置尺寸触发 SIGWINCH 重绘
|
|
1194
|
+
if (outputBuffer) {
|
|
1195
|
+
ws.send(JSON.stringify({ type: 'data', data: outputBuffer }));
|
|
1196
|
+
}
|
|
1197
|
+
lastPtyCols = 0;
|
|
1198
|
+
lastPtyRows = 0;
|
|
1124
1199
|
}
|
|
1125
1200
|
|
|
1126
1201
|
|
|
1202
|
+
let wsBuf = '';
|
|
1203
|
+
let wsFlushTimer = null;
|
|
1204
|
+
const flushWs = () => {
|
|
1205
|
+
wsFlushTimer = null;
|
|
1206
|
+
if (wsBuf && ws.readyState === 1) {
|
|
1207
|
+
ws.send(JSON.stringify({ type: 'data', data: wsBuf }));
|
|
1208
|
+
wsBuf = '';
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1127
1211
|
const listener = (data) => {
|
|
1128
1212
|
if (ws.readyState === 1 && !isSwitching) {
|
|
1129
|
-
|
|
1213
|
+
wsBuf += data;
|
|
1214
|
+
if (!wsFlushTimer) {
|
|
1215
|
+
wsFlushTimer = setTimeout(flushWs, 16);
|
|
1216
|
+
}
|
|
1130
1217
|
}
|
|
1131
1218
|
};
|
|
1132
1219
|
dataListeners.push(listener);
|
|
@@ -1141,7 +1228,6 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1141
1228
|
ws.on('message', async (raw) => {
|
|
1142
1229
|
try {
|
|
1143
1230
|
const msg = JSON.parse(raw);
|
|
1144
|
-
LOG(`[WS msg] type=${msg.type}, currentProcess=${!!currentProcess}, currentMode=${currentMode}`);
|
|
1145
1231
|
|
|
1146
1232
|
if (msg.type === 'input') {
|
|
1147
1233
|
// 进程已退出时,自动重新启动(参考 cc-viewer 逻辑)
|
|
@@ -1189,6 +1275,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1189
1275
|
}
|
|
1190
1276
|
} else if (msg.type === 'restore') {
|
|
1191
1277
|
// 恢复会话(支持 opencode 和 claude)
|
|
1278
|
+
LOG(`[restore] received: sessionId=${msg.sessionId}, mode=${currentMode}, isSwitching=${isSwitching}`);
|
|
1192
1279
|
if (msg.sessionId) {
|
|
1193
1280
|
LOG(`[restore] 恢复 ${currentMode} 会话: ${msg.sessionId}`);
|
|
1194
1281
|
|
|
@@ -1214,8 +1301,8 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1214
1301
|
// 清空输出缓冲
|
|
1215
1302
|
outputBuffer = '';
|
|
1216
1303
|
|
|
1217
|
-
//
|
|
1218
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1304
|
+
// 等待进程退出
|
|
1305
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1219
1306
|
cleanupOrphanProcesses();
|
|
1220
1307
|
|
|
1221
1308
|
// 启动进程,传入 session ID
|
|
@@ -1328,7 +1415,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1328
1415
|
}, 5000);
|
|
1329
1416
|
}
|
|
1330
1417
|
} catch (err) {
|
|
1331
|
-
|
|
1418
|
+
console.log('[WS] Error:', err.message, err.stack);
|
|
1332
1419
|
}
|
|
1333
1420
|
});
|
|
1334
1421
|
|
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** — 移动端键盘交互优化
|