claude-opencode-viewer 2.6.45 → 2.6.47

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-pc.html +33 -2
  2. package/index.html +161 -197
  3. package/package.json +1 -1
package/index-pc.html CHANGED
@@ -390,8 +390,26 @@
390
390
  content: '';
391
391
  animation: loading-dots 1.2s steps(4, end) infinite;
392
392
  }
393
- #terminal.transitioning #switch-overlay { display: flex;
393
+ #terminal.transitioning #switch-overlay { display: flex; }
394
+ #init-overlay {
395
+ display: none;
396
+ position: absolute;
397
+ top: 0; left: 0; right: 0; bottom: 0;
398
+ background: #0a0a0a;
399
+ color: #ccc;
400
+ align-items: center;
401
+ justify-content: center;
402
+ font-size: 18px;
403
+ font-weight: 600;
404
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
405
+ letter-spacing: 1px;
406
+ z-index: 10;
407
+ }
408
+ #init-overlay::after {
409
+ content: '';
410
+ animation: loading-dots 1.2s steps(4, end) infinite;
394
411
  }
412
+ #init-overlay.visible { display: flex; }
395
413
 
396
414
 
397
415
  /* 选择模式:原位文本层 */
@@ -1019,6 +1037,7 @@
1019
1037
  <div id="terminal-container">
1020
1038
  <div id="terminal" style="position:relative;">
1021
1039
  <div id="switch-overlay">正在切换</div>
1040
+ <div id="init-overlay"></div>
1022
1041
  <div id="select-text-layer">
1023
1042
  <div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
1024
1043
  <pre id="select-text-pre"></pre>
@@ -1054,7 +1073,7 @@
1054
1073
  selectionBackground: '#264f78',
1055
1074
  },
1056
1075
  allowProposedApi: true,
1057
- scrollback: isIOS ? 200 : isMobile ? 1000 : 3000,
1076
+ scrollback: isIOS ? 2000 : isMobile ? 5000 : 50000,
1058
1077
  smoothScrollDuration: 0,
1059
1078
  scrollOnUserInput: true,
1060
1079
  });
@@ -1591,11 +1610,19 @@
1591
1610
  currentMode = mode;
1592
1611
  modeSelect.value = mode;
1593
1612
  document.getElementById('mode-label').textContent = '';
1613
+ var label = mode === 'claude' ? 'Claude' : 'OpenCode';
1614
+ var initOv = document.getElementById('init-overlay');
1615
+ initOv.textContent = '正在启动 ' + label + (sessionId ? '(恢复会话)' : '');
1616
+ initOv.classList.add('visible');
1594
1617
  var msg = { type: 'init', mode: mode };
1595
1618
  if (sessionId) msg.sessionId = sessionId;
1596
1619
  ws.send(JSON.stringify(msg));
1597
1620
  }
1598
1621
 
1622
+ function hideInitOverlay() {
1623
+ document.getElementById('init-overlay').classList.remove('visible');
1624
+ }
1625
+
1599
1626
  function getTimeAgo(ts) {
1600
1627
  var diff = Date.now() - ts;
1601
1628
  var min = Math.floor(diff / 60000);
@@ -1671,6 +1698,7 @@
1671
1698
  }
1672
1699
  }
1673
1700
  else if (msg.type === 'restored') {
1701
+ hideInitOverlay();
1674
1702
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1675
1703
  writeBuffer = '';
1676
1704
  term.reset();
@@ -1679,13 +1707,16 @@
1679
1707
  }
1680
1708
  }
1681
1709
  else if (msg.type === 'restore-error') {
1710
+ hideInitOverlay();
1682
1711
  term.write('恢复失败: ' + msg.error + '\r\n');
1683
1712
  }
1684
1713
  else if (msg.type === 'started') {
1714
+ hideInitOverlay();
1685
1715
  rebindTouchScroll();
1686
1716
  preloadData();
1687
1717
  }
1688
1718
  else if (msg.type === 'new-session-ok') {
1719
+ hideInitOverlay();
1689
1720
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1690
1721
  writeBuffer = '';
1691
1722
  term.reset();
package/index.html CHANGED
@@ -30,12 +30,14 @@
30
30
  }
31
31
  #loading-overlay.hidden { display: none !important; }
32
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'">
33
+ <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
34
  <style>
35
35
  * { margin: 0; padding: 0; box-sizing: border-box; }
36
36
  html, body { margin: 0; padding: 0; overflow: hidden; }
37
37
 
38
- /* 参考 cc-viewer 的 App.jsx 行 1319: 移动端容器使用 100vw/100vh */
38
+ /* 隐藏 xterm textarea 的闪烁光标 (cc-viewer TerminalPanel.module.css) */
39
+ .xterm-helper-textarea { caret-color: transparent !important; }
40
+
39
41
  #layout {
40
42
  position: fixed;
41
43
  top: 0;
@@ -54,8 +56,7 @@
54
56
  background: #111;
55
57
  border-bottom: 1px solid #222;
56
58
  display: flex;
57
- align-items: flex-start;
58
- padding-top: 40px;
59
+ align-items: center;
59
60
  justify-content: space-between;
60
61
  flex-shrink: 0;
61
62
  height: 40px;
@@ -91,8 +92,7 @@
91
92
 
92
93
  #session-history-header {
93
94
  display: flex;
94
- align-items: flex-start;
95
- padding-top: 40px;
95
+ align-items: center;
96
96
  justify-content: space-between;
97
97
  padding: 12px 16px;
98
98
  background: #111;
@@ -126,8 +126,7 @@
126
126
 
127
127
  .session-item {
128
128
  display: flex;
129
- align-items: flex-start;
130
- padding-top: 40px;
129
+ align-items: center;
131
130
  gap: 8px;
132
131
  padding: 8px 12px;
133
132
  background: #1a1a1a;
@@ -211,8 +210,7 @@
211
210
  cursor: pointer;
212
211
  border-radius: 4px;
213
212
  display: flex;
214
- align-items: flex-start;
215
- padding-top: 40px;
213
+ align-items: center;
216
214
  gap: 3px;
217
215
  flex-shrink: 0;
218
216
  }
@@ -253,8 +251,7 @@
253
251
  cursor: pointer;
254
252
  border-radius: 50%;
255
253
  display: flex;
256
- align-items: flex-start;
257
- padding-top: 40px;
254
+ align-items: center;
258
255
  justify-content: center;
259
256
  transition: all 0.15s;
260
257
  -webkit-tap-highlight-color: transparent;
@@ -299,8 +296,7 @@
299
296
 
300
297
  #claude-detail-header {
301
298
  display: flex;
302
- align-items: flex-start;
303
- padding-top: 40px;
299
+ align-items: center;
304
300
  padding: 10px 12px;
305
301
  background: #111;
306
302
  border-bottom: 1px solid #222;
@@ -353,8 +349,7 @@
353
349
  #mode-switcher {
354
350
  display: flex;
355
351
  gap: 4px;
356
- align-items: flex-start;
357
- padding-top: 40px;
352
+ align-items: center;
358
353
  }
359
354
 
360
355
  #mode-label {
@@ -407,8 +402,7 @@
407
402
  top: 0; left: 0; right: 0; bottom: 0;
408
403
  background: #0a0a0a;
409
404
  color: #ccc;
410
- align-items: flex-start;
411
- padding-top: 40px;
405
+ align-items: center;
412
406
  justify-content: center;
413
407
  font-size: 16px;
414
408
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
@@ -426,6 +420,7 @@
426
420
  display: flex;
427
421
  gap: 6px;
428
422
  padding: 8px 10px;
423
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
429
424
  background: #111;
430
425
  border-top: 1px solid #222;
431
426
  overflow-x: auto;
@@ -444,22 +439,19 @@
444
439
  font-family: Menlo, Monaco, monospace;
445
440
  cursor: pointer;
446
441
  user-select: none;
442
+ -webkit-user-select: none;
447
443
  -webkit-tap-highlight-color: transparent;
444
+ -webkit-touch-callout: none;
448
445
  touch-action: pan-x;
449
446
  min-width: 44px;
450
447
  min-height: 44px;
451
448
  display: flex;
452
- align-items: flex-start;
453
- padding-top: 40px;
449
+ align-items: center;
454
450
  justify-content: center;
455
- border: none;
456
451
  outline: none;
457
- -webkit-user-select: none;
458
- -moz-user-select: none;
459
- -ms-user-select: none;
460
452
  }
461
453
 
462
- .virtual-key:active {
454
+ .virtual-key-pressed {
463
455
  background: #333;
464
456
  border-color: #555;
465
457
  color: #fff;
@@ -468,19 +460,19 @@
468
460
  /* 消息查看器 */
469
461
  #message-viewer {
470
462
  display: none;
471
- position: fixed;
472
- top: 0; left: 0; right: 0; bottom: 0;
463
+ position: absolute;
464
+ inset: 0;
473
465
  background: #0a0a0a;
474
466
  z-index: 1000;
475
467
  flex-direction: column;
468
+ overflow: hidden;
476
469
  }
477
470
  #message-viewer.visible {
478
471
  display: flex;
479
472
  }
480
473
  #msg-viewer-header {
481
474
  display: flex;
482
- align-items: flex-start;
483
- padding-top: 40px;
475
+ align-items: center;
484
476
  justify-content: space-between;
485
477
  padding: 10px 14px;
486
478
  background: #111;
@@ -506,61 +498,68 @@
506
498
  }
507
499
  #msg-viewer-content {
508
500
  flex: 1;
509
- overflow-y: auto;
501
+ overflow: auto;
510
502
  -webkit-overflow-scrolling: touch;
503
+ overscroll-behavior: contain;
511
504
  padding: 12px;
512
505
  }
513
- .msg-item {
514
- margin-bottom: 16px;
515
- padding: 10px 12px;
516
- border-radius: 8px;
517
- border: 1px solid #222;
518
- }
519
- .msg-user {
520
- background: #1a2332;
521
- border-color: #2a4a7c;
522
- }
523
- .msg-assistant {
524
- background: #1a2e1a;
525
- border-color: #2a5a3a;
526
- }
527
- .msg-role {
528
- font-size: 11px;
529
- color: #888;
530
- font-weight: 600;
531
- text-transform: uppercase;
532
- margin-bottom: 6px;
533
- }
534
- .msg-text {
535
- color: #ddd;
536
- font-size: 13px;
537
- line-height: 1.6;
506
+ /* 聊天气泡布局(参考 cc-viewer ChatMessage) */
507
+ .msg-row { display: flex; gap: 8px; padding: 6px 12px; align-items: flex-start; }
508
+ .msg-row-end { display: flex; gap: 8px; padding: 6px 12px; justify-content: flex-end; align-items: flex-start; }
509
+ .msg-avatar {
510
+ width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
511
+ display: flex; align-items: center; justify-content: center;
512
+ font-size: 12px; color: #fff; font-weight: 600;
513
+ }
514
+ .msg-content-col { min-width: 0; max-width: 85%; }
515
+ .msg-label { font-size: 10px; color: #888; margin-bottom: 2px; }
516
+ .msg-label-right { text-align: right; }
517
+ .msg-bubble {
518
+ border-radius: 8px; border: 1px solid #333; padding: 8px 12px;
519
+ font-size: 13px; line-height: 1.6; word-break: break-word;
520
+ -webkit-user-select: text; user-select: text;
521
+ }
522
+ .msg-bubble-user {
523
+ background: #1668dc; color: #fff; border-color: #4a9eff;
538
524
  white-space: pre-wrap;
539
- word-break: break-word;
540
- -webkit-user-select: text;
541
- user-select: text;
542
- }
543
- .msg-text pre {
544
- background: #0d0d0d;
545
- border: 1px solid #333;
546
- border-radius: 4px;
547
- padding: 8px;
548
- overflow-x: auto;
549
- font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
550
- font-size: 12px;
551
- line-height: 1.4;
552
- white-space: pre;
553
- -webkit-overflow-scrolling: touch;
554
- }
555
- .msg-tool {
556
- margin-top: 6px;
557
- padding: 6px 8px;
558
- background: #1a1a0a;
559
- border: 1px solid #333;
560
- border-radius: 4px;
561
- font-size: 11px;
562
- color: #f0ad4e;
563
525
  }
526
+ .msg-bubble-assistant {
527
+ background: #111; color: #ddd; border-color: #2a2a2a;
528
+ }
529
+ /* 工具标签 */
530
+ .msg-tools { display: flex; flex-wrap: wrap; gap: 4px; padding: 2px 0 4px; }
531
+ .msg-tool-tag {
532
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
533
+ background: #1a1a2a; border: 1px solid #333; color: #aaa;
534
+ font-size: 11px; white-space: nowrap;
535
+ }
536
+ /* Markdown 富文本(助手气泡内) */
537
+ .msg-bubble-assistant pre {
538
+ background: #0d1117; border: 1px solid #2a2a2a; border-radius: 6px;
539
+ padding: 12px; overflow-x: auto; font-size: 13px; line-height: 1.5;
540
+ }
541
+ .msg-bubble-assistant code {
542
+ background: #14141F; padding: 2px 6px; border-radius: 4px;
543
+ font-size: 13px; color: #aeafff;
544
+ }
545
+ .msg-bubble-assistant pre code { background: none; padding: 0; color: inherit; }
546
+ .msg-bubble-assistant p { margin: 6px 0; }
547
+ .msg-bubble-assistant ul, .msg-bubble-assistant ol { padding-left: 20px; margin: 6px 0; }
548
+ .msg-bubble-assistant li { margin: 2px 0; }
549
+ .msg-bubble-assistant h1, .msg-bubble-assistant h2, .msg-bubble-assistant h3 { margin: 12px 0 6px 0; color: #fff; }
550
+ .msg-bubble-assistant h1 { font-size: 1.3em; }
551
+ .msg-bubble-assistant h2 { font-size: 1.15em; }
552
+ .msg-bubble-assistant h3 { font-size: 1.05em; }
553
+ .msg-bubble-assistant blockquote {
554
+ border-left: 3px solid #3b82f6; margin: 8px 0; padding: 4px 12px; color: #888;
555
+ }
556
+ .msg-bubble-assistant table { border-collapse: collapse; margin: 8px 0; font-size: 13px; }
557
+ .msg-bubble-assistant th, .msg-bubble-assistant td { border: 1px solid #6b7280; padding: 6px 10px; }
558
+ .msg-bubble-assistant th { background: #1e1e1e; color: #fff; }
559
+ .msg-bubble-assistant a { color: #60a5fa; }
560
+ .msg-bubble-assistant img { max-width: 100%; height: auto; border-radius: 6px; }
561
+ .msg-bubble-assistant hr { border: none; border-top: 1px solid #2a2a2a; margin: 12px 0; }
562
+ .msg-bubble-assistant strong { color: #e5e5e5; }
564
563
  .msg-empty {
565
564
  text-align: center;
566
565
  padding: 40px 20px;
@@ -607,8 +606,7 @@
607
606
 
608
607
  #git-diff-header {
609
608
  display: flex;
610
- align-items: flex-start;
611
- padding-top: 40px;
609
+ align-items: center;
612
610
  justify-content: space-between;
613
611
  padding: 12px 16px;
614
612
  background: #111;
@@ -632,8 +630,7 @@
632
630
 
633
631
  .git-diff-file-item {
634
632
  display: flex;
635
- align-items: flex-start;
636
- padding-top: 40px;
633
+ align-items: center;
637
634
  padding: 6px 12px;
638
635
  cursor: pointer;
639
636
  color: #ccc;
@@ -677,8 +674,7 @@
677
674
 
678
675
  .git-diff-content-header {
679
676
  display: flex;
680
- align-items: flex-start;
681
- padding-top: 40px;
677
+ align-items: center;
682
678
  gap: 10px;
683
679
  padding: 8px 12px;
684
680
  border-bottom: 1px solid #2a2a2a;
@@ -718,8 +714,7 @@
718
714
  flex: 1;
719
715
  display: flex;
720
716
  flex-direction: column;
721
- align-items: flex-start;
722
- padding-top: 40px;
717
+ align-items: center;
723
718
  justify-content: center;
724
719
  gap: 12px;
725
720
  color: #333;
@@ -752,8 +747,7 @@
752
747
  #docs-bar.visible { display: flex; }
753
748
  #docs-header {
754
749
  display: flex;
755
- align-items: flex-start;
756
- padding-top: 40px;
750
+ align-items: center;
757
751
  justify-content: space-between;
758
752
  padding: 12px 16px;
759
753
  background: #111;
@@ -773,8 +767,7 @@
773
767
  }
774
768
  .docs-file-item {
775
769
  display: flex;
776
- align-items: flex-start;
777
- padding-top: 40px;
770
+ align-items: center;
778
771
  padding: 8px 12px;
779
772
  cursor: pointer;
780
773
  color: #ccc;
@@ -820,8 +813,7 @@
820
813
  flex: 1;
821
814
  display: flex;
822
815
  flex-direction: column;
823
- align-items: flex-start;
824
- padding-top: 40px;
816
+ align-items: center;
825
817
  justify-content: center;
826
818
  gap: 12px;
827
819
  color: #333;
@@ -1102,21 +1094,24 @@
1102
1094
  </div>
1103
1095
  </div>
1104
1096
  </div>
1105
- </div>
1106
1097
 
1107
- <div id="message-viewer">
1108
- <div id="msg-viewer-header">
1109
- <span>会话消息</span>
1110
- <button id="msg-viewer-close">✕</button>
1111
- </div>
1112
- <div id="msg-viewer-content">
1113
- <div class="msg-empty">加载中...</div>
1098
+ <div id="message-viewer">
1099
+ <div id="msg-viewer-header">
1100
+ <span>会话消息</span>
1101
+ <button id="msg-viewer-close">✕</button>
1102
+ </div>
1103
+ <div id="msg-viewer-content">
1104
+ <div class="msg-empty">加载中...</div>
1105
+ </div>
1114
1106
  </div>
1115
1107
  </div>
1116
1108
 
1117
1109
  <div id="copy-toast">已复制</div>
1118
- <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
1119
- <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
1110
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/lib/xterm.min.js"></script>
1111
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.19.0/lib/addon-webgl.min.js"></script>
1112
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.9.0/lib/addon-unicode11.min.js"></script>
1113
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
1114
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
1120
1115
  <script>
1121
1116
  (function() {
1122
1117
  var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
@@ -1129,6 +1124,9 @@
1129
1124
 
1130
1125
  var term = new Terminal({
1131
1126
  cursorBlink: !isMobile,
1127
+ cursorStyle: 'bar',
1128
+ cursorWidth: 1,
1129
+ cursorInactiveStyle: 'none',
1132
1130
  fontSize: fontSize,
1133
1131
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
1134
1132
  theme: {
@@ -1138,13 +1136,23 @@
1138
1136
  selectionBackground: '#264f78',
1139
1137
  },
1140
1138
  allowProposedApi: true,
1141
- scrollback: isIOS ? 200 : isMobile ? 1000 : 3000,
1139
+ scrollback: isIOS ? 2000 : isMobile ? 5000 : 50000,
1142
1140
  smoothScrollDuration: 0,
1143
1141
  scrollOnUserInput: true,
1144
1142
  });
1145
1143
 
1146
1144
  term.open(document.getElementById('terminal'));
1147
1145
 
1146
+
1147
+ // Unicode 11 宽字符支持:box-drawing、CJK、emoji 等字符宽度精确计算
1148
+ if (window.Unicode11Addon) {
1149
+ try {
1150
+ var unicode11 = new Unicode11Addon.Unicode11Addon();
1151
+ term.loadAddon(unicode11);
1152
+ term.unicode.activeVersion = '11';
1153
+ } catch(e) {}
1154
+ }
1155
+
1148
1156
  // WebGL 渲染器:GPU 加速绘制,非 iOS 设备启用(iOS WebGL 性能差)
1149
1157
  if (!isIOS && window.WebglAddon) {
1150
1158
  try {
@@ -1302,6 +1310,7 @@
1302
1310
  var lineHeight = (newCellDims && newCellDims.height) || cellDims.height;
1303
1311
  var rows = Math.max(5, Math.min(Math.floor(availH / lineHeight), 100));
1304
1312
  term.resize(MOBILE_COLS, rows);
1313
+ term.scrollToBottom();
1305
1314
  if (ws && ws.readyState === 1 && !isTransitioning) {
1306
1315
  ws.send(JSON.stringify({ type: 'resize', cols: MOBILE_COLS, rows: rows, mobile: true }));
1307
1316
  }
@@ -1834,6 +1843,11 @@
1834
1843
  if (seq && ws && ws.readyState === 1 && !isTransitioning) {
1835
1844
  ws.send(JSON.stringify({ type: 'input', data: seq }));
1836
1845
  }
1846
+ // 手机端主动 blur xterm textarea,防止系统键盘弹出
1847
+ if (isMobile) {
1848
+ var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
1849
+ if (xtermTa) xtermTa.blur();
1850
+ }
1837
1851
  }
1838
1852
 
1839
1853
  function scrollTerminal(lines) {
@@ -1841,111 +1855,62 @@
1841
1855
  doScroll(lines);
1842
1856
  }
1843
1857
 
1844
- // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
1845
- // 每个按键独立绑定事件,不在容器上绑定
1858
+ // 虚拟按键触摸处理
1846
1859
  var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
1847
- var scrollInterval = null; // 长按滚动定时器
1848
1860
 
1849
1861
  function setupVirtualKeyEvents() {
1850
1862
  var keys = document.querySelectorAll('.virtual-key');
1851
1863
  keys.forEach(function(key) {
1852
- // 防止元素获得焦点
1853
1864
  key.setAttribute('tabindex', '-1');
1854
1865
 
1855
- // 移动端触摸事件
1856
1866
  key.addEventListener('touchstart', function(e) {
1857
1867
  var touch = e.touches[0];
1858
1868
  vkStartX = touch.clientX;
1859
1869
  vkStartY = touch.clientY;
1860
1870
  vkMoved = false;
1861
1871
  vkTarget = e.currentTarget;
1862
- vkTarget.style.background = '#333';
1863
-
1864
- // 如果是滚动按钮,阻止默认行为(防止键盘弹出)并启动持续滚动
1865
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1866
- if (scrollLines) {
1867
- e.preventDefault(); // 关键:阻止默认行为,防止键盘弹出
1868
- scrollTerminal(parseInt(scrollLines, 10));
1869
- scrollInterval = setInterval(function() {
1870
- scrollTerminal(parseInt(scrollLines, 10));
1871
- }, 100);
1872
- }
1873
- }, { passive: false }); // 必须是 false 才能调用 preventDefault()
1872
+ vkTarget.classList.add('virtual-key-pressed');
1873
+ }, { passive: true });
1874
1874
 
1875
1875
  key.addEventListener('touchmove', function(e) {
1876
1876
  if (vkMoved) return;
1877
1877
  var touch = e.touches[0];
1878
1878
  var dx = touch.clientX - vkStartX;
1879
1879
  var dy = touch.clientY - vkStartY;
1880
- if (dx * dx + dy * dy > 64) { // 8px 阈值
1880
+ if (dx * dx + dy * dy > 64) {
1881
1881
  vkMoved = true;
1882
- if (vkTarget) {
1883
- vkTarget.style.background = '';
1884
- }
1885
1882
  }
1886
1883
  }, { passive: true });
1887
1884
 
1888
1885
  key.addEventListener('touchend', function(e) {
1889
- // 清除滚动定时器
1890
- if (scrollInterval) {
1891
- clearInterval(scrollInterval);
1892
- scrollInterval = null;
1893
- }
1894
-
1886
+ e.preventDefault();
1895
1887
  if (vkTarget) {
1896
- vkTarget.style.background = '';
1888
+ vkTarget.classList.remove('virtual-key-pressed');
1897
1889
  vkTarget = null;
1898
1890
  }
1899
-
1900
- // 如果没有移动,触发按键功能并阻止默认行为
1901
1891
  if (!vkMoved) {
1902
- e.preventDefault(); // 阻止默认行为
1903
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1904
- if (!scrollLines) {
1905
- // 非滚动按钮才触发按键
1892
+ var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
1893
+ if (xtermTa) xtermTa.blur();
1894
+ var scrollAttr = e.currentTarget.getAttribute('data-scroll');
1895
+ if (scrollAttr) {
1896
+ scrollTerminal(parseInt(scrollAttr, 10));
1897
+ } else {
1906
1898
  var keyName = e.currentTarget.getAttribute('data-key');
1907
1899
  sendKey(keyName);
1908
1900
  }
1909
1901
  }
1910
1902
  }, { passive: false });
1911
1903
 
1912
- // PC端点击支持
1913
1904
  key.addEventListener('click', function(e) {
1914
1905
  e.preventDefault();
1915
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1916
- if (scrollLines) {
1917
- scrollTerminal(parseInt(scrollLines, 10));
1906
+ var scrollAttr = e.currentTarget.getAttribute('data-scroll');
1907
+ if (scrollAttr) {
1908
+ scrollTerminal(parseInt(scrollAttr, 10));
1918
1909
  } else {
1919
1910
  var keyName = e.currentTarget.getAttribute('data-key');
1920
1911
  sendKey(keyName);
1921
1912
  }
1922
1913
  });
1923
-
1924
- // PC端鼠标按下支持(长按滚动)
1925
- key.addEventListener('mousedown', function(e) {
1926
- e.preventDefault();
1927
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1928
- if (scrollLines) {
1929
- scrollTerminal(parseInt(scrollLines, 10));
1930
- scrollInterval = setInterval(function() {
1931
- scrollTerminal(parseInt(scrollLines, 10));
1932
- }, 100);
1933
- }
1934
- });
1935
-
1936
- key.addEventListener('mouseup', function(e) {
1937
- if (scrollInterval) {
1938
- clearInterval(scrollInterval);
1939
- scrollInterval = null;
1940
- }
1941
- });
1942
-
1943
- key.addEventListener('mouseleave', function(e) {
1944
- if (scrollInterval) {
1945
- clearInterval(scrollInterval);
1946
- scrollInterval = null;
1947
- }
1948
- });
1949
1914
  });
1950
1915
  }
1951
1916
 
@@ -2404,7 +2369,6 @@
2404
2369
 
2405
2370
  messageViewer.classList.add('visible');
2406
2371
  msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
2407
- unbindTouchScroll();
2408
2372
 
2409
2373
  if (currentMode === 'claude') {
2410
2374
  // Claude 模式:从 JSONL 文件读取消息
@@ -2465,22 +2429,12 @@
2465
2429
  }
2466
2430
 
2467
2431
  function formatMsgText(text) {
2468
- // markdown 代码块转为 <pre>,其余部分转义
2469
- var parts = text.split(/(```[\s\S]*?```)/g);
2470
- var result = '';
2471
- for (var i = 0; i < parts.length; i++) {
2472
- var p = parts[i];
2473
- if (p.startsWith('```') && p.endsWith('```')) {
2474
- // 去掉首尾 ```(可能带语言标记)
2475
- var inner = p.slice(3, -3);
2476
- var nlIdx = inner.indexOf('\n');
2477
- if (nlIdx !== -1) inner = inner.slice(nlIdx + 1);
2478
- result += '<pre>' + escapeHtml(inner) + '</pre>';
2479
- } else {
2480
- result += escapeHtml(p);
2481
- }
2432
+ if (!text) return '';
2433
+ try {
2434
+ return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
2435
+ } catch (e) {
2436
+ return escapeHtml(text);
2482
2437
  }
2483
- return result;
2484
2438
  }
2485
2439
 
2486
2440
  function renderMessages(messages) {
@@ -2491,19 +2445,30 @@
2491
2445
  var html = '';
2492
2446
  messages.forEach(function(msg) {
2493
2447
  var role = msg.role || 'unknown';
2494
- var roleLabel = role === 'user' ? '用户' : role === 'assistant' ? '助手' : role;
2495
- var cls = role === 'user' ? 'msg-user' : role === 'assistant' ? 'msg-assistant' : '';
2496
- html += '<div class="msg-item ' + cls + '">';
2497
- html += '<div class="msg-role">' + roleLabel + '</div>';
2498
- if (msg.text) {
2499
- html += '<div class="msg-text">' + formatMsgText(msg.text) + '</div>';
2500
- }
2501
- if (msg.toolCalls && msg.toolCalls.length > 0) {
2502
- msg.toolCalls.forEach(function(tc) {
2503
- html += '<div class="msg-tool">🔧 ' + escapeHtml(tc.name || 'tool') + '</div>';
2504
- });
2448
+ if (role === 'user') {
2449
+ html += '<div class="msg-row-end">';
2450
+ html += '<div class="msg-content-col">';
2451
+ html += '<div class="msg-label msg-label-right">用户</div>';
2452
+ html += '<div class="msg-bubble msg-bubble-user">' + escapeHtml(msg.text || '') + '</div>';
2453
+ html += '</div>';
2454
+ html += '<div class="msg-avatar" style="background:#1e40af">U</div>';
2455
+ html += '</div>';
2456
+ } else if (role === 'assistant') {
2457
+ html += '<div class="msg-row">';
2458
+ html += '<div class="msg-avatar" style="background:#000;border:1px solid #333">A</div>';
2459
+ html += '<div class="msg-content-col">';
2460
+ html += '<div class="msg-label">助手</div>';
2461
+ html += '<div class="msg-bubble msg-bubble-assistant">' + formatMsgText(msg.text || '') + '</div>';
2462
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
2463
+ html += '<div class="msg-tools">';
2464
+ msg.toolCalls.forEach(function(tc) {
2465
+ html += '<span class="msg-tool-tag">🔧 ' + escapeHtml(tc.name || 'tool') + '</span>';
2466
+ });
2467
+ html += '</div>';
2468
+ }
2469
+ html += '</div>';
2470
+ html += '</div>';
2505
2471
  }
2506
- html += '</div>';
2507
2472
  });
2508
2473
  msgViewerContent.innerHTML = html;
2509
2474
  msgViewerContent.scrollTop = msgViewerContent.scrollHeight;
@@ -2515,7 +2480,6 @@
2515
2480
 
2516
2481
  function closeMessageViewer() {
2517
2482
  messageViewer.classList.remove('visible');
2518
- rebindTouchScroll();
2519
2483
  }
2520
2484
 
2521
2485
  msgViewerClose.addEventListener('click', function(e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.45",
3
+ "version": "2.6.47",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",