claude-opencode-viewer 2.6.49 → 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.html +157 -70
- package/package.json +1 -1
- package/server.js +32 -16
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; }
|
|
@@ -934,7 +974,6 @@
|
|
|
934
974
|
</head>
|
|
935
975
|
<body>
|
|
936
976
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
937
|
-
<div id="loading-overlay">正在初始化</div>
|
|
938
977
|
<div id="layout">
|
|
939
978
|
<div id="header">
|
|
940
979
|
<div style="display: flex; gap: 4px; align-items: center; overflow-x: auto; flex: 1; min-width: 0;">
|
|
@@ -1120,6 +1159,9 @@
|
|
|
1120
1159
|
<div id="terminal-container">
|
|
1121
1160
|
<div id="terminal" style="position:relative;">
|
|
1122
1161
|
<div id="switch-overlay">正在切换</div>
|
|
1162
|
+
<div id="restore-overlay">正在恢复会话</div>
|
|
1163
|
+
<div id="init-overlay">正在初始化</div>
|
|
1164
|
+
<div id="reconnect-overlay">连接断开,正在重连</div>
|
|
1123
1165
|
</div>
|
|
1124
1166
|
<div id="virtual-keybar">
|
|
1125
1167
|
<div class="virtual-key" data-key="up">↑</div>
|
|
@@ -1309,24 +1351,31 @@
|
|
|
1309
1351
|
// iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
|
|
1310
1352
|
// 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
|
|
1311
1353
|
// 事件同步可见区域的高度和偏移,用 fixed 定位将布局锁定在可见区域内。
|
|
1312
|
-
|
|
1354
|
+
var mobileKbOpen = false;
|
|
1355
|
+
if (isMobile && window.visualViewport) {
|
|
1313
1356
|
var layoutEl = document.getElementById('layout');
|
|
1357
|
+
var initVVH = window.visualViewport.height;
|
|
1314
1358
|
var onVVChange = function() {
|
|
1315
1359
|
if (!layoutEl) return;
|
|
1316
1360
|
var vv = window.visualViewport;
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
+
}
|
|
1323
1372
|
};
|
|
1324
1373
|
window.visualViewport.addEventListener('resize', onVVChange);
|
|
1325
1374
|
window.visualViewport.addEventListener('scroll', onVVChange);
|
|
1326
|
-
onVVChange();
|
|
1327
1375
|
}
|
|
1328
1376
|
|
|
1329
|
-
//
|
|
1377
|
+
// 移动端固定尺寸计算:用屏幕全高减去固定区域,保证全屏时也填满
|
|
1378
|
+
var lastMobileCols = 0, lastMobileRows = 0;
|
|
1330
1379
|
function mobileFixedResize() {
|
|
1331
1380
|
if (!term) return;
|
|
1332
1381
|
var cellDims = getCellDims();
|
|
@@ -1350,9 +1399,12 @@
|
|
|
1350
1399
|
var newCellDims = getCellDims();
|
|
1351
1400
|
var lineHeight = (newCellDims && newCellDims.height) || cellDims.height;
|
|
1352
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;
|
|
1353
1406
|
term.resize(MOBILE_COLS, rows);
|
|
1354
|
-
|
|
1355
|
-
if (ws && ws.readyState === 1 && !isTransitioning) {
|
|
1407
|
+
if (sizeChanged && ws && ws.readyState === 1) {
|
|
1356
1408
|
ws.send(JSON.stringify({ type: 'resize', cols: MOBILE_COLS, rows: rows, mobile: true }));
|
|
1357
1409
|
}
|
|
1358
1410
|
});
|
|
@@ -1678,57 +1730,67 @@
|
|
|
1678
1730
|
}, 50);
|
|
1679
1731
|
});
|
|
1680
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
|
+
|
|
1681
1756
|
function connect() {
|
|
1757
|
+
if (ws && ws.readyState <= 1) return;
|
|
1682
1758
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1683
1759
|
ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
|
|
1684
1760
|
|
|
1685
1761
|
ws.onopen = function() {
|
|
1686
|
-
|
|
1762
|
+
document.getElementById('reconnect-overlay').classList.remove('visible');
|
|
1763
|
+
lastMobileCols = 0;
|
|
1764
|
+
lastMobileRows = 0;
|
|
1687
1765
|
resize();
|
|
1688
1766
|
rebindTouchScroll();
|
|
1689
1767
|
};
|
|
1690
1768
|
|
|
1691
1769
|
ws.onclose = function() {
|
|
1692
1770
|
ws = null;
|
|
1693
|
-
|
|
1694
|
-
term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
|
|
1771
|
+
document.getElementById('reconnect-overlay').classList.add('visible');
|
|
1695
1772
|
setTimeout(connect, 2000);
|
|
1696
1773
|
};
|
|
1697
1774
|
|
|
1698
|
-
var loadingOverlay = document.getElementById('loading-overlay');
|
|
1699
|
-
var loadingShowTime = Date.now();
|
|
1700
|
-
var loadingMinMs = 600;
|
|
1701
|
-
var loadingHideTimer = null;
|
|
1702
|
-
function setLoadingText(text) {
|
|
1703
|
-
if (loadingOverlay && !loadingOverlay.classList.contains('hidden')) {
|
|
1704
|
-
loadingOverlay.textContent = text;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
function hideLoading() {
|
|
1708
|
-
if (!loadingOverlay || loadingOverlay.classList.contains('hidden')) return;
|
|
1709
|
-
if (loadingHideTimer) return; // 已在等待中
|
|
1710
|
-
var elapsed = Date.now() - loadingShowTime;
|
|
1711
|
-
if (elapsed >= loadingMinMs) {
|
|
1712
|
-
loadingOverlay.classList.add('hidden');
|
|
1713
|
-
} else {
|
|
1714
|
-
loadingHideTimer = setTimeout(function() {
|
|
1715
|
-
loadingOverlay.classList.add('hidden');
|
|
1716
|
-
}, loadingMinMs - elapsed);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
1775
|
ws.onmessage = function(e) {
|
|
1721
1776
|
try {
|
|
1722
1777
|
var msg = JSON.parse(e.data);
|
|
1723
1778
|
if (msg.type === 'data') {
|
|
1724
|
-
hideLoading();
|
|
1725
1779
|
if (isTransitioning) {
|
|
1726
1780
|
term.write(msg.data);
|
|
1727
1781
|
clearTimeout(transitionEndTimer);
|
|
1728
1782
|
transitionEndTimer = setTimeout(function() {
|
|
1729
1783
|
terminalEl.classList.remove('transitioning');
|
|
1730
1784
|
isTransitioning = false;
|
|
1731
|
-
},
|
|
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);
|
|
1732
1794
|
} else if (!isCreatingNewSession) {
|
|
1733
1795
|
throttledWrite(msg.data);
|
|
1734
1796
|
}
|
|
@@ -1740,31 +1802,25 @@
|
|
|
1740
1802
|
}
|
|
1741
1803
|
}
|
|
1742
1804
|
else if (msg.type === 'state') {
|
|
1743
|
-
// 重连时清掉旧终端内容(如"连接断开"提示)
|
|
1744
1805
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1745
1806
|
writeBuffer = '';
|
|
1746
1807
|
term.reset();
|
|
1747
1808
|
term.clear();
|
|
1748
|
-
// 同步模式 UI
|
|
1749
1809
|
if (msg.mode) {
|
|
1750
1810
|
currentMode = msg.mode;
|
|
1751
1811
|
modeSelect.value = msg.mode;
|
|
1752
1812
|
}
|
|
1753
1813
|
if (msg.running) {
|
|
1754
|
-
setLoadingText('正在连接');
|
|
1755
|
-
hideLoading();
|
|
1756
1814
|
preloadData();
|
|
1757
1815
|
}
|
|
1758
|
-
// 服务端还没启动进程时,查最近会话并自动恢复
|
|
1759
1816
|
if (!msg.running && !mobileInitSent) {
|
|
1760
1817
|
mobileInitSent = true;
|
|
1761
|
-
|
|
1818
|
+
showInitOverlay('正在查询会话');
|
|
1762
1819
|
fetch('/api/last-sessions')
|
|
1763
1820
|
.then(function(r) { return r.json(); })
|
|
1764
1821
|
.then(function(data) {
|
|
1765
1822
|
var oc = data.opencode;
|
|
1766
1823
|
var cl = data.claude;
|
|
1767
|
-
// 取 mtime 最新的那个
|
|
1768
1824
|
var useOc = oc && (!cl || oc.mtime > cl.mtime);
|
|
1769
1825
|
var mode = useOc ? 'opencode' : 'claude';
|
|
1770
1826
|
var sessionId = useOc ? (oc && oc.id) : (cl && cl.id);
|
|
@@ -1773,7 +1829,7 @@
|
|
|
1773
1829
|
claudeProject = cl.project;
|
|
1774
1830
|
}
|
|
1775
1831
|
currentMode = mode;
|
|
1776
|
-
|
|
1832
|
+
showInitOverlay('正在启动 ' + (mode === 'claude' ? 'Claude' : 'OpenCode'));
|
|
1777
1833
|
ws.send(JSON.stringify({ type: 'init', mode: mode, sessionId: sessionId || null }));
|
|
1778
1834
|
})
|
|
1779
1835
|
.catch(function() {
|
|
@@ -1798,7 +1854,7 @@
|
|
|
1798
1854
|
term.clear();
|
|
1799
1855
|
}
|
|
1800
1856
|
else if (msg.type === 'restored') {
|
|
1801
|
-
|
|
1857
|
+
waitingInitData = true;
|
|
1802
1858
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1803
1859
|
writeBuffer = '';
|
|
1804
1860
|
term.reset();
|
|
@@ -1809,14 +1865,17 @@
|
|
|
1809
1865
|
}
|
|
1810
1866
|
else if (msg.type === 'restore-error') {
|
|
1811
1867
|
isTransitioning = false;
|
|
1868
|
+
hideInitOverlay();
|
|
1869
|
+
hideRestoreOverlay();
|
|
1812
1870
|
term.write('恢复失败: ' + msg.error + '\r\n');
|
|
1813
1871
|
}
|
|
1814
1872
|
else if (msg.type === 'started') {
|
|
1815
|
-
|
|
1873
|
+
waitingInitData = true;
|
|
1816
1874
|
rebindTouchScroll();
|
|
1817
1875
|
preloadData();
|
|
1818
1876
|
}
|
|
1819
1877
|
else if (msg.type === 'new-session-ok') {
|
|
1878
|
+
waitingInitData = true;
|
|
1820
1879
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
1821
1880
|
writeBuffer = '';
|
|
1822
1881
|
term.reset();
|
|
@@ -1824,11 +1883,9 @@
|
|
|
1824
1883
|
term.write(msg.buffer);
|
|
1825
1884
|
}
|
|
1826
1885
|
isCreatingNewSession = false;
|
|
1827
|
-
isTransitioning = false;
|
|
1828
1886
|
}
|
|
1829
1887
|
else if (msg.type === 'new-session-error') {
|
|
1830
1888
|
isCreatingNewSession = false;
|
|
1831
|
-
isTransitioning = false;
|
|
1832
1889
|
term.write('新会话启动失败: ' + msg.error + '\r\n');
|
|
1833
1890
|
}
|
|
1834
1891
|
} catch(err) {}
|
|
@@ -1844,24 +1901,56 @@
|
|
|
1844
1901
|
}, 2000);
|
|
1845
1902
|
}
|
|
1846
1903
|
|
|
1847
|
-
window.addEventListener('resize', resize);
|
|
1848
1904
|
if (isMobile) {
|
|
1905
|
+
// 移动端:不监听 window resize(键盘弹出/收起会触发导致重复内容)
|
|
1906
|
+
// 仅监听屏幕旋转
|
|
1849
1907
|
window.addEventListener('orientationchange', function() {
|
|
1850
1908
|
setTimeout(resize, 200);
|
|
1851
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'));
|
|
1852
1934
|
}
|
|
1853
1935
|
|
|
1854
|
-
//
|
|
1936
|
+
// 页面卸载前保存输入缓存,并通知服务端退出
|
|
1855
1937
|
window.addEventListener('beforeunload', function() {
|
|
1856
1938
|
if (currentInputBuffer) {
|
|
1857
1939
|
saveInputCache();
|
|
1858
1940
|
}
|
|
1941
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1942
|
+
ws.send(JSON.stringify({ type: 'quit' }));
|
|
1943
|
+
}
|
|
1859
1944
|
});
|
|
1860
1945
|
|
|
1861
|
-
// 页面可见性变化时保存缓存
|
|
1946
|
+
// 页面可见性变化时保存缓存 + 尝试重连
|
|
1862
1947
|
document.addEventListener('visibilitychange', function() {
|
|
1863
|
-
if (document.hidden
|
|
1864
|
-
saveInputCache();
|
|
1948
|
+
if (document.hidden) {
|
|
1949
|
+
if (currentInputBuffer) saveInputCache();
|
|
1950
|
+
} else {
|
|
1951
|
+
if (!ws || ws.readyState > 1) {
|
|
1952
|
+
connect();
|
|
1953
|
+
}
|
|
1865
1954
|
}
|
|
1866
1955
|
});
|
|
1867
1956
|
|
|
@@ -2097,27 +2186,23 @@
|
|
|
2097
2186
|
|
|
2098
2187
|
|
|
2099
2188
|
function loadSession(session) {
|
|
2100
|
-
console.log('[restore] 直接恢复会话:', session.title);
|
|
2101
2189
|
currentSessionData = session;
|
|
2102
|
-
|
|
2103
|
-
// 关闭历史栏
|
|
2104
2190
|
toggleHistoryBar();
|
|
2105
|
-
|
|
2106
|
-
// 阻止旧数据写入
|
|
2191
|
+
showRestoreOverlay();
|
|
2107
2192
|
isTransitioning = true;
|
|
2108
2193
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2109
2194
|
writeBuffer = '';
|
|
2110
2195
|
term.reset();
|
|
2111
|
-
|
|
2112
2196
|
if (ws && ws.readyState === 1) {
|
|
2113
2197
|
if (currentMode !== 'opencode') {
|
|
2114
2198
|
isTransitioning = false;
|
|
2199
|
+
hideRestoreOverlay();
|
|
2115
2200
|
term.write('错误: 请先切换到 OpenCode 模式\r\n');
|
|
2116
2201
|
return;
|
|
2117
2202
|
}
|
|
2118
|
-
// 静默恢复
|
|
2119
2203
|
ws.send(JSON.stringify({ type: 'restore', sessionId: session.id }));
|
|
2120
2204
|
} else {
|
|
2205
|
+
hideRestoreOverlay();
|
|
2121
2206
|
term.write('错误: WebSocket 未连接\r\n');
|
|
2122
2207
|
}
|
|
2123
2208
|
}
|
|
@@ -2298,10 +2383,10 @@
|
|
|
2298
2383
|
}
|
|
2299
2384
|
|
|
2300
2385
|
function restoreClaudeSession(sessionId, project) {
|
|
2301
|
-
console.log('[restore] 恢复 Claude 会话:', sessionId);
|
|
2302
2386
|
claudeSessionId = sessionId;
|
|
2303
2387
|
if (project) claudeProject = project;
|
|
2304
2388
|
toggleHistoryBar();
|
|
2389
|
+
showRestoreOverlay();
|
|
2305
2390
|
isTransitioning = true;
|
|
2306
2391
|
if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
|
|
2307
2392
|
writeBuffer = '';
|
|
@@ -2309,11 +2394,13 @@
|
|
|
2309
2394
|
if (ws && ws.readyState === 1) {
|
|
2310
2395
|
if (currentMode !== 'claude') {
|
|
2311
2396
|
isTransitioning = false;
|
|
2397
|
+
hideRestoreOverlay();
|
|
2312
2398
|
term.write('错误: 请先切换到 Claude 模式\r\n');
|
|
2313
2399
|
return;
|
|
2314
2400
|
}
|
|
2315
2401
|
ws.send(JSON.stringify({ type: 'restore', sessionId: sessionId }));
|
|
2316
2402
|
} else {
|
|
2403
|
+
hideRestoreOverlay();
|
|
2317
2404
|
term.write('错误: WebSocket 未连接\r\n');
|
|
2318
2405
|
}
|
|
2319
2406
|
}
|
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, {
|
|
@@ -1181,23 +1188,32 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1181
1188
|
LOG('[reconnect] 重启失败:', e.message);
|
|
1182
1189
|
}
|
|
1183
1190
|
isSwitching = false;
|
|
1184
|
-
},
|
|
1191
|
+
}, 100);
|
|
1185
1192
|
} else if (currentProcess) {
|
|
1186
|
-
//
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
try { currentProcess.resize(lastPtyCols, lastPtyRows); } catch {}
|
|
1193
|
-
}, 50);
|
|
1194
|
-
} catch {}
|
|
1193
|
+
// 连接已运行的进程:发送缓冲区内容 + 重置尺寸触发 SIGWINCH 重绘
|
|
1194
|
+
if (outputBuffer) {
|
|
1195
|
+
ws.send(JSON.stringify({ type: 'data', data: outputBuffer }));
|
|
1196
|
+
}
|
|
1197
|
+
lastPtyCols = 0;
|
|
1198
|
+
lastPtyRows = 0;
|
|
1195
1199
|
}
|
|
1196
1200
|
|
|
1197
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
|
+
};
|
|
1198
1211
|
const listener = (data) => {
|
|
1199
1212
|
if (ws.readyState === 1 && !isSwitching) {
|
|
1200
|
-
|
|
1213
|
+
wsBuf += data;
|
|
1214
|
+
if (!wsFlushTimer) {
|
|
1215
|
+
wsFlushTimer = setTimeout(flushWs, 16);
|
|
1216
|
+
}
|
|
1201
1217
|
}
|
|
1202
1218
|
};
|
|
1203
1219
|
dataListeners.push(listener);
|
|
@@ -1212,7 +1228,6 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1212
1228
|
ws.on('message', async (raw) => {
|
|
1213
1229
|
try {
|
|
1214
1230
|
const msg = JSON.parse(raw);
|
|
1215
|
-
LOG(`[WS msg] type=${msg.type}, currentProcess=${!!currentProcess}, currentMode=${currentMode}`);
|
|
1216
1231
|
|
|
1217
1232
|
if (msg.type === 'input') {
|
|
1218
1233
|
// 进程已退出时,自动重新启动(参考 cc-viewer 逻辑)
|
|
@@ -1260,6 +1275,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1260
1275
|
}
|
|
1261
1276
|
} else if (msg.type === 'restore') {
|
|
1262
1277
|
// 恢复会话(支持 opencode 和 claude)
|
|
1278
|
+
LOG(`[restore] received: sessionId=${msg.sessionId}, mode=${currentMode}, isSwitching=${isSwitching}`);
|
|
1263
1279
|
if (msg.sessionId) {
|
|
1264
1280
|
LOG(`[restore] 恢复 ${currentMode} 会话: ${msg.sessionId}`);
|
|
1265
1281
|
|
|
@@ -1285,8 +1301,8 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1285
1301
|
// 清空输出缓冲
|
|
1286
1302
|
outputBuffer = '';
|
|
1287
1303
|
|
|
1288
|
-
//
|
|
1289
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1304
|
+
// 等待进程退出
|
|
1305
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1290
1306
|
cleanupOrphanProcesses();
|
|
1291
1307
|
|
|
1292
1308
|
// 启动进程,传入 session ID
|
|
@@ -1399,7 +1415,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1399
1415
|
}, 5000);
|
|
1400
1416
|
}
|
|
1401
1417
|
} catch (err) {
|
|
1402
|
-
|
|
1418
|
+
console.log('[WS] Error:', err.message, err.stack);
|
|
1403
1419
|
}
|
|
1404
1420
|
});
|
|
1405
1421
|
|