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.
Files changed (3) hide show
  1. package/index.html +157 -70
  2. package/package.json +1 -1
  3. package/server.js +32 -16
package/index.html CHANGED
@@ -11,24 +11,62 @@
11
11
  50% { content: '..'; }
12
12
  75% { content: '...'; }
13
13
  }
14
- #loading-overlay {
15
- position: fixed;
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: 99999;
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
- #loading-overlay::after {
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
- if (isIOS && window.visualViewport) {
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
- layoutEl.style.position = 'fixed';
1318
- layoutEl.style.top = vv.offsetTop + 'px';
1319
- layoutEl.style.height = vv.height + 'px';
1320
- layoutEl.style.width = '100%';
1321
- layoutEl.style.left = '0';
1322
- setTimeout(mobileFixedResize, 50);
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
- // 移动端固定尺寸计算:基于 #terminal 元素实际高度,确保终端与按钮栏齐平
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
- term.scrollToBottom();
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
- setLoadingText('正在连接服务');
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
- term.reset();
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
- }, 2000);
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
- setLoadingText('正在查询会话');
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
- setLoadingText('正在启动 ' + (mode === 'claude' ? 'Claude' : 'OpenCode'));
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
- hideLoading();
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 && currentInputBuffer) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.49",
3
+ "version": "2.6.50",
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
@@ -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 { proc.resize(cols, rows); } catch {}
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
- }, 200);
1191
+ }, 100);
1185
1192
  } 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 {}
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
- ws.send(JSON.stringify({ type: 'data', data }));
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, 500));
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
- LOG('[WS] Error:', err.message);
1418
+ console.log('[WS] Error:', err.message, err.stack);
1403
1419
  }
1404
1420
  });
1405
1421