claude-opencode-viewer 2.6.43 → 2.6.45

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
@@ -365,9 +365,32 @@
365
365
  overscroll-behavior: contain;
366
366
  }
367
367
 
368
- #terminal.transitioning {
369
- opacity: 0.3;
370
- transition: opacity 0.3s ease;
368
+ #terminal.transitioning .xterm { visibility: hidden; }
369
+ @keyframes loading-dots {
370
+ 0% { content: ''; }
371
+ 25% { content: '.'; }
372
+ 50% { content: '..'; }
373
+ 75% { content: '...'; }
374
+ }
375
+ #switch-overlay {
376
+ display: none;
377
+ position: absolute;
378
+ top: 0; left: 0; right: 0; bottom: 0;
379
+ background: #0a0a0a;
380
+ color: #ccc;
381
+ align-items: flex-start;
382
+ padding-top: 40px;
383
+ justify-content: center;
384
+ font-size: 15px;
385
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
386
+ letter-spacing: 1px;
387
+ z-index: 10;
388
+ }
389
+ #switch-overlay::after {
390
+ content: '';
391
+ animation: loading-dots 1.2s steps(4, end) infinite;
392
+ }
393
+ #terminal.transitioning #switch-overlay { display: flex;
371
394
  }
372
395
 
373
396
 
@@ -994,7 +1017,8 @@
994
1017
 
995
1018
  <div id="content">
996
1019
  <div id="terminal-container">
997
- <div id="terminal">
1020
+ <div id="terminal" style="position:relative;">
1021
+ <div id="switch-overlay">正在切换</div>
998
1022
  <div id="select-text-layer">
999
1023
  <div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
1000
1024
  <pre id="select-text-pre"></pre>
package/index.html CHANGED
@@ -4,7 +4,33 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
6
  <title>Claude OpenCode Viewer</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
7
+ <style id="loading-style">
8
+ @keyframes loading-dots {
9
+ 0% { content: ''; }
10
+ 25% { content: '.'; }
11
+ 50% { content: '..'; }
12
+ 75% { content: '...'; }
13
+ }
14
+ #loading-overlay {
15
+ position: fixed;
16
+ top: 0; left: 0; right: 0; bottom: 0;
17
+ background: #0a0a0a;
18
+ color: #ccc;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ font-size: 16px;
23
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
24
+ letter-spacing: 1px;
25
+ z-index: 99999;
26
+ }
27
+ #loading-overlay::after {
28
+ content: '';
29
+ animation: loading-dots 1.2s steps(4, end) infinite;
30
+ }
31
+ #loading-overlay.hidden { display: none !important; }
32
+ </style>
33
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" media="print" onload="this.media='all'">
8
34
  <style>
9
35
  * { margin: 0; padding: 0; box-sizing: border-box; }
10
36
  html, body { margin: 0; padding: 0; overflow: hidden; }
@@ -28,7 +54,8 @@
28
54
  background: #111;
29
55
  border-bottom: 1px solid #222;
30
56
  display: flex;
31
- align-items: center;
57
+ align-items: flex-start;
58
+ padding-top: 40px;
32
59
  justify-content: space-between;
33
60
  flex-shrink: 0;
34
61
  height: 40px;
@@ -64,7 +91,8 @@
64
91
 
65
92
  #session-history-header {
66
93
  display: flex;
67
- align-items: center;
94
+ align-items: flex-start;
95
+ padding-top: 40px;
68
96
  justify-content: space-between;
69
97
  padding: 12px 16px;
70
98
  background: #111;
@@ -98,7 +126,8 @@
98
126
 
99
127
  .session-item {
100
128
  display: flex;
101
- align-items: center;
129
+ align-items: flex-start;
130
+ padding-top: 40px;
102
131
  gap: 8px;
103
132
  padding: 8px 12px;
104
133
  background: #1a1a1a;
@@ -182,7 +211,8 @@
182
211
  cursor: pointer;
183
212
  border-radius: 4px;
184
213
  display: flex;
185
- align-items: center;
214
+ align-items: flex-start;
215
+ padding-top: 40px;
186
216
  gap: 3px;
187
217
  flex-shrink: 0;
188
218
  }
@@ -223,7 +253,8 @@
223
253
  cursor: pointer;
224
254
  border-radius: 50%;
225
255
  display: flex;
226
- align-items: center;
256
+ align-items: flex-start;
257
+ padding-top: 40px;
227
258
  justify-content: center;
228
259
  transition: all 0.15s;
229
260
  -webkit-tap-highlight-color: transparent;
@@ -268,7 +299,8 @@
268
299
 
269
300
  #claude-detail-header {
270
301
  display: flex;
271
- align-items: center;
302
+ align-items: flex-start;
303
+ padding-top: 40px;
272
304
  padding: 10px 12px;
273
305
  background: #111;
274
306
  border-bottom: 1px solid #222;
@@ -321,7 +353,8 @@
321
353
  #mode-switcher {
322
354
  display: flex;
323
355
  gap: 4px;
324
- align-items: center;
356
+ align-items: flex-start;
357
+ padding-top: 40px;
325
358
  }
326
359
 
327
360
  #mode-label {
@@ -359,19 +392,6 @@
359
392
  background: #0a0a0a;
360
393
  position: relative;
361
394
  }
362
- #loading-overlay {
363
- position: fixed;
364
- top: 0; left: 0; right: 0; bottom: 0;
365
- background: #0a0a0a;
366
- color: #888;
367
- display: flex;
368
- align-items: center;
369
- justify-content: center;
370
- font-size: 15px;
371
- z-index: 9999;
372
- }
373
- #loading-overlay.hidden { display: none; }
374
-
375
395
  #terminal {
376
396
  flex: 1;
377
397
  overflow: hidden;
@@ -380,10 +400,26 @@
380
400
  overscroll-behavior: contain;
381
401
  }
382
402
 
383
- #terminal.transitioning {
384
- opacity: 0.3;
385
- transition: opacity 0.3s ease;
403
+ #terminal.transitioning .xterm { visibility: hidden; }
404
+ #switch-overlay {
405
+ display: none;
406
+ position: absolute;
407
+ top: 0; left: 0; right: 0; bottom: 0;
408
+ background: #0a0a0a;
409
+ color: #ccc;
410
+ align-items: flex-start;
411
+ padding-top: 40px;
412
+ justify-content: center;
413
+ font-size: 16px;
414
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
415
+ letter-spacing: 1px;
416
+ z-index: 10;
417
+ }
418
+ #switch-overlay::after {
419
+ content: '';
420
+ animation: loading-dots 1.2s steps(4, end) infinite;
386
421
  }
422
+ #terminal.transitioning #switch-overlay { display: flex; }
387
423
 
388
424
  /* 参考 cc-viewer 的 TerminalPanel.module.css: 虚拟键盘栏 */
389
425
  #virtual-keybar {
@@ -413,7 +449,8 @@
413
449
  min-width: 44px;
414
450
  min-height: 44px;
415
451
  display: flex;
416
- align-items: center;
452
+ align-items: flex-start;
453
+ padding-top: 40px;
417
454
  justify-content: center;
418
455
  border: none;
419
456
  outline: none;
@@ -442,7 +479,8 @@
442
479
  }
443
480
  #msg-viewer-header {
444
481
  display: flex;
445
- align-items: center;
482
+ align-items: flex-start;
483
+ padding-top: 40px;
446
484
  justify-content: space-between;
447
485
  padding: 10px 14px;
448
486
  background: #111;
@@ -569,7 +607,8 @@
569
607
 
570
608
  #git-diff-header {
571
609
  display: flex;
572
- align-items: center;
610
+ align-items: flex-start;
611
+ padding-top: 40px;
573
612
  justify-content: space-between;
574
613
  padding: 12px 16px;
575
614
  background: #111;
@@ -593,7 +632,8 @@
593
632
 
594
633
  .git-diff-file-item {
595
634
  display: flex;
596
- align-items: center;
635
+ align-items: flex-start;
636
+ padding-top: 40px;
597
637
  padding: 6px 12px;
598
638
  cursor: pointer;
599
639
  color: #ccc;
@@ -637,7 +677,8 @@
637
677
 
638
678
  .git-diff-content-header {
639
679
  display: flex;
640
- align-items: center;
680
+ align-items: flex-start;
681
+ padding-top: 40px;
641
682
  gap: 10px;
642
683
  padding: 8px 12px;
643
684
  border-bottom: 1px solid #2a2a2a;
@@ -677,7 +718,8 @@
677
718
  flex: 1;
678
719
  display: flex;
679
720
  flex-direction: column;
680
- align-items: center;
721
+ align-items: flex-start;
722
+ padding-top: 40px;
681
723
  justify-content: center;
682
724
  gap: 12px;
683
725
  color: #333;
@@ -710,7 +752,8 @@
710
752
  #docs-bar.visible { display: flex; }
711
753
  #docs-header {
712
754
  display: flex;
713
- align-items: center;
755
+ align-items: flex-start;
756
+ padding-top: 40px;
714
757
  justify-content: space-between;
715
758
  padding: 12px 16px;
716
759
  background: #111;
@@ -730,7 +773,8 @@
730
773
  }
731
774
  .docs-file-item {
732
775
  display: flex;
733
- align-items: center;
776
+ align-items: flex-start;
777
+ padding-top: 40px;
734
778
  padding: 8px 12px;
735
779
  cursor: pointer;
736
780
  color: #ccc;
@@ -776,7 +820,8 @@
776
820
  flex: 1;
777
821
  display: flex;
778
822
  flex-direction: column;
779
- align-items: center;
823
+ align-items: flex-start;
824
+ padding-top: 40px;
780
825
  justify-content: center;
781
826
  gap: 12px;
782
827
  color: #333;
@@ -857,7 +902,7 @@
857
902
  </head>
858
903
  <body>
859
904
  <!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
860
- <div id="loading-overlay">正在初始化...</div>
905
+ <div id="loading-overlay">正在初始化</div>
861
906
  <div id="layout">
862
907
  <div id="header">
863
908
  <div style="display: flex; gap: 4px; align-items: center; overflow-x: auto; flex: 1; min-width: 0;">
@@ -1041,7 +1086,8 @@
1041
1086
 
1042
1087
  <div id="content">
1043
1088
  <div id="terminal-container">
1044
- <div id="terminal">
1089
+ <div id="terminal" style="position:relative;">
1090
+ <div id="switch-overlay">正在切换</div>
1045
1091
  </div>
1046
1092
  <div id="virtual-keybar">
1047
1093
  <div class="virtual-key" data-key="up">↑</div>
@@ -1587,6 +1633,7 @@
1587
1633
  ws = new WebSocket(proto + '//' + location.host + basePath + '/ws');
1588
1634
 
1589
1635
  ws.onopen = function() {
1636
+ setLoadingText('正在连接服务');
1590
1637
  resize();
1591
1638
  rebindTouchScroll();
1592
1639
  };
@@ -1599,9 +1646,24 @@
1599
1646
  };
1600
1647
 
1601
1648
  var loadingOverlay = document.getElementById('loading-overlay');
1602
- function hideLoading() {
1649
+ var loadingShowTime = Date.now();
1650
+ var loadingMinMs = 600;
1651
+ var loadingHideTimer = null;
1652
+ function setLoadingText(text) {
1603
1653
  if (loadingOverlay && !loadingOverlay.classList.contains('hidden')) {
1654
+ loadingOverlay.textContent = text;
1655
+ }
1656
+ }
1657
+ function hideLoading() {
1658
+ if (!loadingOverlay || loadingOverlay.classList.contains('hidden')) return;
1659
+ if (loadingHideTimer) return; // 已在等待中
1660
+ var elapsed = Date.now() - loadingShowTime;
1661
+ if (elapsed >= loadingMinMs) {
1604
1662
  loadingOverlay.classList.add('hidden');
1663
+ } else {
1664
+ loadingHideTimer = setTimeout(function() {
1665
+ loadingOverlay.classList.add('hidden');
1666
+ }, loadingMinMs - elapsed);
1605
1667
  }
1606
1668
  }
1607
1669
 
@@ -1621,12 +1683,25 @@
1621
1683
  }
1622
1684
  }
1623
1685
  else if (msg.type === 'state') {
1686
+ // 重连时清掉旧终端内容(如"连接断开"提示)
1687
+ if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1688
+ writeBuffer = '';
1689
+ term.reset();
1690
+ term.clear();
1691
+ // 同步模式 UI
1692
+ if (msg.mode) {
1693
+ currentMode = msg.mode;
1694
+ modeSelect.value = msg.mode;
1695
+ }
1624
1696
  if (msg.running) {
1697
+ setLoadingText('正在连接');
1698
+ hideLoading();
1625
1699
  preloadData();
1626
1700
  }
1627
1701
  // 服务端还没启动进程时,查最近会话并自动恢复
1628
1702
  if (!msg.running && !mobileInitSent) {
1629
1703
  mobileInitSent = true;
1704
+ setLoadingText('正在查询会话');
1630
1705
  fetch('/api/last-sessions')
1631
1706
  .then(function(r) { return r.json(); })
1632
1707
  .then(function(data) {
@@ -1641,6 +1716,7 @@
1641
1716
  claudeProject = cl.project;
1642
1717
  }
1643
1718
  currentMode = mode;
1719
+ setLoadingText('正在启动 ' + (mode === 'claude' ? 'Claude' : 'OpenCode'));
1644
1720
  ws.send(JSON.stringify({ type: 'init', mode: mode, sessionId: sessionId || null }));
1645
1721
  })
1646
1722
  .catch(function() {
@@ -1663,13 +1739,6 @@
1663
1739
  writeBuffer = '';
1664
1740
  term.clear();
1665
1741
  }
1666
- else if (msg.type === 'state') {
1667
- if (msg.mode) {
1668
- currentMode = msg.mode;
1669
- modeSelect.value = msg.mode;
1670
- modeIndicator.textContent = msg.mode === 'claude' ? 'Claude' : 'OpenCode';
1671
- }
1672
- }
1673
1742
  else if (msg.type === 'restored') {
1674
1743
  // 会话恢复成功,清除所有残留
1675
1744
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.43",
3
+ "version": "2.6.45",
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
@@ -295,6 +295,8 @@ async function spawnProcess(mode, sessionId = null) {
295
295
  });
296
296
 
297
297
  proc.onData((data) => {
298
+ // 忽略已被替换的旧进程输出
299
+ if (currentProcess !== proc) return;
298
300
  outputBuffer += data;
299
301
  if (outputBuffer.length > MAX_BUFFER) {
300
302
  const rawStart = outputBuffer.length - MAX_BUFFER;
@@ -1216,8 +1218,7 @@ wssInst.on('connection', (ws, req) => {
1216
1218
  outputBuffer = '';
1217
1219
  try {
1218
1220
  await spawnProcess(mode, msg.sessionId || null);
1219
- ws.send(JSON.stringify({ type: 'mode', mode: currentMode }));
1220
- ws.send(JSON.stringify({ type: 'started', sessionId: msg.sessionId || null }));
1221
+ ws.send(JSON.stringify({ type: 'started', mode: currentMode, sessionId: msg.sessionId || null }));
1221
1222
  } catch (e) {
1222
1223
  LOG('[init] 启动失败:', e.message);
1223
1224
  ws.send(JSON.stringify({ type: 'start-error', error: e.message }));