claude-opencode-viewer 2.6.44 → 2.6.46

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 +30 -4
  2. package/index.html +192 -180
  3. package/package.json +1 -1
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>
@@ -1567,6 +1591,8 @@
1567
1591
  currentMode = mode;
1568
1592
  modeSelect.value = mode;
1569
1593
  document.getElementById('mode-label').textContent = '';
1594
+ var label = mode === 'claude' ? 'Claude' : 'OpenCode';
1595
+ term.write('\r\n 正在启动 ' + label + (sessionId ? '(恢复会话)' : '') + '...\r\n');
1570
1596
  var msg = { type: 'init', mode: mode };
1571
1597
  if (sessionId) msg.sessionId = sessionId;
1572
1598
  ws.send(JSON.stringify(msg));
package/index.html CHANGED
@@ -30,11 +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
+ /* 隐藏 xterm textarea 的闪烁光标 (cc-viewer TerminalPanel.module.css) */
39
+ .xterm-helper-textarea { caret-color: transparent !important; }
40
+
38
41
  /* 参考 cc-viewer 的 App.jsx 行 1319: 移动端容器使用 100vw/100vh */
39
42
  #layout {
40
43
  position: fixed;
@@ -54,7 +57,8 @@
54
57
  background: #111;
55
58
  border-bottom: 1px solid #222;
56
59
  display: flex;
57
- align-items: center;
60
+ align-items: flex-start;
61
+ padding-top: 40px;
58
62
  justify-content: space-between;
59
63
  flex-shrink: 0;
60
64
  height: 40px;
@@ -90,7 +94,8 @@
90
94
 
91
95
  #session-history-header {
92
96
  display: flex;
93
- align-items: center;
97
+ align-items: flex-start;
98
+ padding-top: 40px;
94
99
  justify-content: space-between;
95
100
  padding: 12px 16px;
96
101
  background: #111;
@@ -124,7 +129,8 @@
124
129
 
125
130
  .session-item {
126
131
  display: flex;
127
- align-items: center;
132
+ align-items: flex-start;
133
+ padding-top: 40px;
128
134
  gap: 8px;
129
135
  padding: 8px 12px;
130
136
  background: #1a1a1a;
@@ -208,7 +214,8 @@
208
214
  cursor: pointer;
209
215
  border-radius: 4px;
210
216
  display: flex;
211
- align-items: center;
217
+ align-items: flex-start;
218
+ padding-top: 40px;
212
219
  gap: 3px;
213
220
  flex-shrink: 0;
214
221
  }
@@ -249,7 +256,8 @@
249
256
  cursor: pointer;
250
257
  border-radius: 50%;
251
258
  display: flex;
252
- align-items: center;
259
+ align-items: flex-start;
260
+ padding-top: 40px;
253
261
  justify-content: center;
254
262
  transition: all 0.15s;
255
263
  -webkit-tap-highlight-color: transparent;
@@ -294,7 +302,8 @@
294
302
 
295
303
  #claude-detail-header {
296
304
  display: flex;
297
- align-items: center;
305
+ align-items: flex-start;
306
+ padding-top: 40px;
298
307
  padding: 10px 12px;
299
308
  background: #111;
300
309
  border-bottom: 1px solid #222;
@@ -347,7 +356,8 @@
347
356
  #mode-switcher {
348
357
  display: flex;
349
358
  gap: 4px;
350
- align-items: center;
359
+ align-items: flex-start;
360
+ padding-top: 40px;
351
361
  }
352
362
 
353
363
  #mode-label {
@@ -393,16 +403,33 @@
393
403
  overscroll-behavior: contain;
394
404
  }
395
405
 
396
- #terminal.transitioning {
397
- opacity: 0.3;
398
- transition: opacity 0.3s ease;
406
+ #terminal.transitioning .xterm { visibility: hidden; }
407
+ #switch-overlay {
408
+ display: none;
409
+ position: absolute;
410
+ top: 0; left: 0; right: 0; bottom: 0;
411
+ background: #0a0a0a;
412
+ color: #ccc;
413
+ align-items: flex-start;
414
+ padding-top: 40px;
415
+ justify-content: center;
416
+ font-size: 16px;
417
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
418
+ letter-spacing: 1px;
419
+ z-index: 10;
420
+ }
421
+ #switch-overlay::after {
422
+ content: '';
423
+ animation: loading-dots 1.2s steps(4, end) infinite;
399
424
  }
425
+ #terminal.transitioning #switch-overlay { display: flex; }
400
426
 
401
427
  /* 参考 cc-viewer 的 TerminalPanel.module.css: 虚拟键盘栏 */
402
428
  #virtual-keybar {
403
429
  display: flex;
404
430
  gap: 6px;
405
431
  padding: 8px 10px;
432
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
406
433
  background: #111;
407
434
  border-top: 1px solid #222;
408
435
  overflow-x: auto;
@@ -421,21 +448,19 @@
421
448
  font-family: Menlo, Monaco, monospace;
422
449
  cursor: pointer;
423
450
  user-select: none;
451
+ -webkit-user-select: none;
424
452
  -webkit-tap-highlight-color: transparent;
453
+ -webkit-touch-callout: none;
425
454
  touch-action: pan-x;
426
455
  min-width: 44px;
427
456
  min-height: 44px;
428
457
  display: flex;
429
458
  align-items: center;
430
459
  justify-content: center;
431
- border: none;
432
460
  outline: none;
433
- -webkit-user-select: none;
434
- -moz-user-select: none;
435
- -ms-user-select: none;
436
461
  }
437
462
 
438
- .virtual-key:active {
463
+ .virtual-key-pressed {
439
464
  background: #333;
440
465
  border-color: #555;
441
466
  color: #fff;
@@ -444,18 +469,20 @@
444
469
  /* 消息查看器 */
445
470
  #message-viewer {
446
471
  display: none;
447
- position: fixed;
448
- top: 0; left: 0; right: 0; bottom: 0;
472
+ position: absolute;
473
+ inset: 0;
449
474
  background: #0a0a0a;
450
475
  z-index: 1000;
451
476
  flex-direction: column;
477
+ overflow: hidden;
452
478
  }
453
479
  #message-viewer.visible {
454
480
  display: flex;
455
481
  }
456
482
  #msg-viewer-header {
457
483
  display: flex;
458
- align-items: center;
484
+ align-items: flex-start;
485
+ padding-top: 40px;
459
486
  justify-content: space-between;
460
487
  padding: 10px 14px;
461
488
  background: #111;
@@ -481,61 +508,68 @@
481
508
  }
482
509
  #msg-viewer-content {
483
510
  flex: 1;
484
- overflow-y: auto;
511
+ overflow: auto;
485
512
  -webkit-overflow-scrolling: touch;
513
+ overscroll-behavior: contain;
486
514
  padding: 12px;
487
515
  }
488
- .msg-item {
489
- margin-bottom: 16px;
490
- padding: 10px 12px;
491
- border-radius: 8px;
492
- border: 1px solid #222;
493
- }
494
- .msg-user {
495
- background: #1a2332;
496
- border-color: #2a4a7c;
497
- }
498
- .msg-assistant {
499
- background: #1a2e1a;
500
- border-color: #2a5a3a;
501
- }
502
- .msg-role {
503
- font-size: 11px;
504
- color: #888;
505
- font-weight: 600;
506
- text-transform: uppercase;
507
- margin-bottom: 6px;
508
- }
509
- .msg-text {
510
- color: #ddd;
511
- font-size: 13px;
512
- line-height: 1.6;
516
+ /* 聊天气泡布局(参考 cc-viewer ChatMessage) */
517
+ .msg-row { display: flex; gap: 8px; padding: 6px 12px; align-items: flex-start; }
518
+ .msg-row-end { display: flex; gap: 8px; padding: 6px 12px; justify-content: flex-end; align-items: flex-start; }
519
+ .msg-avatar {
520
+ width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
521
+ display: flex; align-items: center; justify-content: center;
522
+ font-size: 12px; color: #fff; font-weight: 600;
523
+ }
524
+ .msg-content-col { min-width: 0; max-width: 85%; }
525
+ .msg-label { font-size: 10px; color: #888; margin-bottom: 2px; }
526
+ .msg-label-right { text-align: right; }
527
+ .msg-bubble {
528
+ border-radius: 8px; border: 1px solid #333; padding: 8px 12px;
529
+ font-size: 13px; line-height: 1.6; word-break: break-word;
530
+ -webkit-user-select: text; user-select: text;
531
+ }
532
+ .msg-bubble-user {
533
+ background: #1668dc; color: #fff; border-color: #4a9eff;
513
534
  white-space: pre-wrap;
514
- word-break: break-word;
515
- -webkit-user-select: text;
516
- user-select: text;
517
- }
518
- .msg-text pre {
519
- background: #0d0d0d;
520
- border: 1px solid #333;
521
- border-radius: 4px;
522
- padding: 8px;
523
- overflow-x: auto;
524
- font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
525
- font-size: 12px;
526
- line-height: 1.4;
527
- white-space: pre;
528
- -webkit-overflow-scrolling: touch;
529
- }
530
- .msg-tool {
531
- margin-top: 6px;
532
- padding: 6px 8px;
533
- background: #1a1a0a;
534
- border: 1px solid #333;
535
- border-radius: 4px;
536
- font-size: 11px;
537
- color: #f0ad4e;
538
535
  }
536
+ .msg-bubble-assistant {
537
+ background: #111; color: #ddd; border-color: #2a2a2a;
538
+ }
539
+ /* 工具标签 */
540
+ .msg-tools { display: flex; flex-wrap: wrap; gap: 4px; padding: 2px 0 4px; }
541
+ .msg-tool-tag {
542
+ display: inline-block; padding: 2px 8px; border-radius: 4px;
543
+ background: #1a1a2a; border: 1px solid #333; color: #aaa;
544
+ font-size: 11px; white-space: nowrap;
545
+ }
546
+ /* Markdown 富文本(助手气泡内) */
547
+ .msg-bubble-assistant pre {
548
+ background: #0d1117; border: 1px solid #2a2a2a; border-radius: 6px;
549
+ padding: 12px; overflow-x: auto; font-size: 13px; line-height: 1.5;
550
+ }
551
+ .msg-bubble-assistant code {
552
+ background: #14141F; padding: 2px 6px; border-radius: 4px;
553
+ font-size: 13px; color: #aeafff;
554
+ }
555
+ .msg-bubble-assistant pre code { background: none; padding: 0; color: inherit; }
556
+ .msg-bubble-assistant p { margin: 6px 0; }
557
+ .msg-bubble-assistant ul, .msg-bubble-assistant ol { padding-left: 20px; margin: 6px 0; }
558
+ .msg-bubble-assistant li { margin: 2px 0; }
559
+ .msg-bubble-assistant h1, .msg-bubble-assistant h2, .msg-bubble-assistant h3 { margin: 12px 0 6px 0; color: #fff; }
560
+ .msg-bubble-assistant h1 { font-size: 1.3em; }
561
+ .msg-bubble-assistant h2 { font-size: 1.15em; }
562
+ .msg-bubble-assistant h3 { font-size: 1.05em; }
563
+ .msg-bubble-assistant blockquote {
564
+ border-left: 3px solid #3b82f6; margin: 8px 0; padding: 4px 12px; color: #888;
565
+ }
566
+ .msg-bubble-assistant table { border-collapse: collapse; margin: 8px 0; font-size: 13px; }
567
+ .msg-bubble-assistant th, .msg-bubble-assistant td { border: 1px solid #6b7280; padding: 6px 10px; }
568
+ .msg-bubble-assistant th { background: #1e1e1e; color: #fff; }
569
+ .msg-bubble-assistant a { color: #60a5fa; }
570
+ .msg-bubble-assistant img { max-width: 100%; height: auto; border-radius: 6px; }
571
+ .msg-bubble-assistant hr { border: none; border-top: 1px solid #2a2a2a; margin: 12px 0; }
572
+ .msg-bubble-assistant strong { color: #e5e5e5; }
539
573
  .msg-empty {
540
574
  text-align: center;
541
575
  padding: 40px 20px;
@@ -582,7 +616,8 @@
582
616
 
583
617
  #git-diff-header {
584
618
  display: flex;
585
- align-items: center;
619
+ align-items: flex-start;
620
+ padding-top: 40px;
586
621
  justify-content: space-between;
587
622
  padding: 12px 16px;
588
623
  background: #111;
@@ -606,7 +641,8 @@
606
641
 
607
642
  .git-diff-file-item {
608
643
  display: flex;
609
- align-items: center;
644
+ align-items: flex-start;
645
+ padding-top: 40px;
610
646
  padding: 6px 12px;
611
647
  cursor: pointer;
612
648
  color: #ccc;
@@ -650,7 +686,8 @@
650
686
 
651
687
  .git-diff-content-header {
652
688
  display: flex;
653
- align-items: center;
689
+ align-items: flex-start;
690
+ padding-top: 40px;
654
691
  gap: 10px;
655
692
  padding: 8px 12px;
656
693
  border-bottom: 1px solid #2a2a2a;
@@ -690,7 +727,8 @@
690
727
  flex: 1;
691
728
  display: flex;
692
729
  flex-direction: column;
693
- align-items: center;
730
+ align-items: flex-start;
731
+ padding-top: 40px;
694
732
  justify-content: center;
695
733
  gap: 12px;
696
734
  color: #333;
@@ -723,7 +761,8 @@
723
761
  #docs-bar.visible { display: flex; }
724
762
  #docs-header {
725
763
  display: flex;
726
- align-items: center;
764
+ align-items: flex-start;
765
+ padding-top: 40px;
727
766
  justify-content: space-between;
728
767
  padding: 12px 16px;
729
768
  background: #111;
@@ -743,7 +782,8 @@
743
782
  }
744
783
  .docs-file-item {
745
784
  display: flex;
746
- align-items: center;
785
+ align-items: flex-start;
786
+ padding-top: 40px;
747
787
  padding: 8px 12px;
748
788
  cursor: pointer;
749
789
  color: #ccc;
@@ -789,7 +829,8 @@
789
829
  flex: 1;
790
830
  display: flex;
791
831
  flex-direction: column;
792
- align-items: center;
832
+ align-items: flex-start;
833
+ padding-top: 40px;
793
834
  justify-content: center;
794
835
  gap: 12px;
795
836
  color: #333;
@@ -1054,7 +1095,8 @@
1054
1095
 
1055
1096
  <div id="content">
1056
1097
  <div id="terminal-container">
1057
- <div id="terminal">
1098
+ <div id="terminal" style="position:relative;">
1099
+ <div id="switch-overlay">正在切换</div>
1058
1100
  </div>
1059
1101
  <div id="virtual-keybar">
1060
1102
  <div class="virtual-key" data-key="up">↑</div>
@@ -1069,21 +1111,24 @@
1069
1111
  </div>
1070
1112
  </div>
1071
1113
  </div>
1072
- </div>
1073
1114
 
1074
- <div id="message-viewer">
1075
- <div id="msg-viewer-header">
1076
- <span>会话消息</span>
1077
- <button id="msg-viewer-close">✕</button>
1078
- </div>
1079
- <div id="msg-viewer-content">
1080
- <div class="msg-empty">加载中...</div>
1115
+ <div id="message-viewer">
1116
+ <div id="msg-viewer-header">
1117
+ <span>会话消息</span>
1118
+ <button id="msg-viewer-close">✕</button>
1119
+ </div>
1120
+ <div id="msg-viewer-content">
1121
+ <div class="msg-empty">加载中...</div>
1122
+ </div>
1081
1123
  </div>
1082
1124
  </div>
1083
1125
 
1084
1126
  <div id="copy-toast">已复制</div>
1085
- <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
1086
- <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
1127
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/lib/xterm.min.js"></script>
1128
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.19.0/lib/addon-webgl.min.js"></script>
1129
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.9.0/lib/addon-unicode11.min.js"></script>
1130
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
1131
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
1087
1132
  <script>
1088
1133
  (function() {
1089
1134
  var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
@@ -1096,6 +1141,9 @@
1096
1141
 
1097
1142
  var term = new Terminal({
1098
1143
  cursorBlink: !isMobile,
1144
+ cursorStyle: 'bar',
1145
+ cursorWidth: 1,
1146
+ cursorInactiveStyle: 'none',
1099
1147
  fontSize: fontSize,
1100
1148
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
1101
1149
  theme: {
@@ -1112,6 +1160,15 @@
1112
1160
 
1113
1161
  term.open(document.getElementById('terminal'));
1114
1162
 
1163
+ // Unicode 11 宽字符支持:box-drawing、CJK、emoji 等字符宽度精确计算
1164
+ if (window.Unicode11Addon) {
1165
+ try {
1166
+ var unicode11 = new Unicode11Addon.Unicode11Addon();
1167
+ term.loadAddon(unicode11);
1168
+ term.unicode.activeVersion = '11';
1169
+ } catch(e) {}
1170
+ }
1171
+
1115
1172
  // WebGL 渲染器:GPU 加速绘制,非 iOS 设备启用(iOS WebGL 性能差)
1116
1173
  if (!isIOS && window.WebglAddon) {
1117
1174
  try {
@@ -1801,6 +1858,11 @@
1801
1858
  if (seq && ws && ws.readyState === 1 && !isTransitioning) {
1802
1859
  ws.send(JSON.stringify({ type: 'input', data: seq }));
1803
1860
  }
1861
+ // 手机端主动 blur xterm textarea,防止系统键盘弹出
1862
+ if (isMobile) {
1863
+ var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
1864
+ if (xtermTa) xtermTa.blur();
1865
+ }
1804
1866
  }
1805
1867
 
1806
1868
  function scrollTerminal(lines) {
@@ -1808,111 +1870,62 @@
1808
1870
  doScroll(lines);
1809
1871
  }
1810
1872
 
1811
- // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
1812
- // 每个按键独立绑定事件,不在容器上绑定
1873
+ // 虚拟按键触摸处理
1813
1874
  var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
1814
- var scrollInterval = null; // 长按滚动定时器
1815
1875
 
1816
1876
  function setupVirtualKeyEvents() {
1817
1877
  var keys = document.querySelectorAll('.virtual-key');
1818
1878
  keys.forEach(function(key) {
1819
- // 防止元素获得焦点
1820
1879
  key.setAttribute('tabindex', '-1');
1821
1880
 
1822
- // 移动端触摸事件
1823
1881
  key.addEventListener('touchstart', function(e) {
1824
1882
  var touch = e.touches[0];
1825
1883
  vkStartX = touch.clientX;
1826
1884
  vkStartY = touch.clientY;
1827
1885
  vkMoved = false;
1828
1886
  vkTarget = e.currentTarget;
1829
- vkTarget.style.background = '#333';
1830
-
1831
- // 如果是滚动按钮,阻止默认行为(防止键盘弹出)并启动持续滚动
1832
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1833
- if (scrollLines) {
1834
- e.preventDefault(); // 关键:阻止默认行为,防止键盘弹出
1835
- scrollTerminal(parseInt(scrollLines, 10));
1836
- scrollInterval = setInterval(function() {
1837
- scrollTerminal(parseInt(scrollLines, 10));
1838
- }, 100);
1839
- }
1840
- }, { passive: false }); // 必须是 false 才能调用 preventDefault()
1887
+ vkTarget.classList.add('virtual-key-pressed');
1888
+ }, { passive: true });
1841
1889
 
1842
1890
  key.addEventListener('touchmove', function(e) {
1843
1891
  if (vkMoved) return;
1844
1892
  var touch = e.touches[0];
1845
1893
  var dx = touch.clientX - vkStartX;
1846
1894
  var dy = touch.clientY - vkStartY;
1847
- if (dx * dx + dy * dy > 64) { // 8px 阈值
1895
+ if (dx * dx + dy * dy > 64) {
1848
1896
  vkMoved = true;
1849
- if (vkTarget) {
1850
- vkTarget.style.background = '';
1851
- }
1852
1897
  }
1853
1898
  }, { passive: true });
1854
1899
 
1855
1900
  key.addEventListener('touchend', function(e) {
1856
- // 清除滚动定时器
1857
- if (scrollInterval) {
1858
- clearInterval(scrollInterval);
1859
- scrollInterval = null;
1860
- }
1861
-
1901
+ e.preventDefault();
1862
1902
  if (vkTarget) {
1863
- vkTarget.style.background = '';
1903
+ vkTarget.classList.remove('virtual-key-pressed');
1864
1904
  vkTarget = null;
1865
1905
  }
1866
-
1867
- // 如果没有移动,触发按键功能并阻止默认行为
1868
1906
  if (!vkMoved) {
1869
- e.preventDefault(); // 阻止默认行为
1870
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1871
- if (!scrollLines) {
1872
- // 非滚动按钮才触发按键
1907
+ var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
1908
+ if (xtermTa) xtermTa.blur();
1909
+ var scrollAttr = e.currentTarget.getAttribute('data-scroll');
1910
+ if (scrollAttr) {
1911
+ scrollTerminal(parseInt(scrollAttr, 10));
1912
+ } else {
1873
1913
  var keyName = e.currentTarget.getAttribute('data-key');
1874
1914
  sendKey(keyName);
1875
1915
  }
1876
1916
  }
1877
1917
  }, { passive: false });
1878
1918
 
1879
- // PC端点击支持
1880
1919
  key.addEventListener('click', function(e) {
1881
1920
  e.preventDefault();
1882
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1883
- if (scrollLines) {
1884
- scrollTerminal(parseInt(scrollLines, 10));
1921
+ var scrollAttr = e.currentTarget.getAttribute('data-scroll');
1922
+ if (scrollAttr) {
1923
+ scrollTerminal(parseInt(scrollAttr, 10));
1885
1924
  } else {
1886
1925
  var keyName = e.currentTarget.getAttribute('data-key');
1887
1926
  sendKey(keyName);
1888
1927
  }
1889
1928
  });
1890
-
1891
- // PC端鼠标按下支持(长按滚动)
1892
- key.addEventListener('mousedown', function(e) {
1893
- e.preventDefault();
1894
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1895
- if (scrollLines) {
1896
- scrollTerminal(parseInt(scrollLines, 10));
1897
- scrollInterval = setInterval(function() {
1898
- scrollTerminal(parseInt(scrollLines, 10));
1899
- }, 100);
1900
- }
1901
- });
1902
-
1903
- key.addEventListener('mouseup', function(e) {
1904
- if (scrollInterval) {
1905
- clearInterval(scrollInterval);
1906
- scrollInterval = null;
1907
- }
1908
- });
1909
-
1910
- key.addEventListener('mouseleave', function(e) {
1911
- if (scrollInterval) {
1912
- clearInterval(scrollInterval);
1913
- scrollInterval = null;
1914
- }
1915
- });
1916
1929
  });
1917
1930
  }
1918
1931
 
@@ -2371,7 +2384,6 @@
2371
2384
 
2372
2385
  messageViewer.classList.add('visible');
2373
2386
  msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
2374
- unbindTouchScroll();
2375
2387
 
2376
2388
  if (currentMode === 'claude') {
2377
2389
  // Claude 模式:从 JSONL 文件读取消息
@@ -2432,22 +2444,12 @@
2432
2444
  }
2433
2445
 
2434
2446
  function formatMsgText(text) {
2435
- // markdown 代码块转为 <pre>,其余部分转义
2436
- var parts = text.split(/(```[\s\S]*?```)/g);
2437
- var result = '';
2438
- for (var i = 0; i < parts.length; i++) {
2439
- var p = parts[i];
2440
- if (p.startsWith('```') && p.endsWith('```')) {
2441
- // 去掉首尾 ```(可能带语言标记)
2442
- var inner = p.slice(3, -3);
2443
- var nlIdx = inner.indexOf('\n');
2444
- if (nlIdx !== -1) inner = inner.slice(nlIdx + 1);
2445
- result += '<pre>' + escapeHtml(inner) + '</pre>';
2446
- } else {
2447
- result += escapeHtml(p);
2448
- }
2447
+ if (!text) return '';
2448
+ try {
2449
+ return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
2450
+ } catch (e) {
2451
+ return escapeHtml(text);
2449
2452
  }
2450
- return result;
2451
2453
  }
2452
2454
 
2453
2455
  function renderMessages(messages) {
@@ -2458,19 +2460,30 @@
2458
2460
  var html = '';
2459
2461
  messages.forEach(function(msg) {
2460
2462
  var role = msg.role || 'unknown';
2461
- var roleLabel = role === 'user' ? '用户' : role === 'assistant' ? '助手' : role;
2462
- var cls = role === 'user' ? 'msg-user' : role === 'assistant' ? 'msg-assistant' : '';
2463
- html += '<div class="msg-item ' + cls + '">';
2464
- html += '<div class="msg-role">' + roleLabel + '</div>';
2465
- if (msg.text) {
2466
- html += '<div class="msg-text">' + formatMsgText(msg.text) + '</div>';
2467
- }
2468
- if (msg.toolCalls && msg.toolCalls.length > 0) {
2469
- msg.toolCalls.forEach(function(tc) {
2470
- html += '<div class="msg-tool">🔧 ' + escapeHtml(tc.name || 'tool') + '</div>';
2471
- });
2463
+ if (role === 'user') {
2464
+ html += '<div class="msg-row-end">';
2465
+ html += '<div class="msg-content-col">';
2466
+ html += '<div class="msg-label msg-label-right">用户</div>';
2467
+ html += '<div class="msg-bubble msg-bubble-user">' + escapeHtml(msg.text || '') + '</div>';
2468
+ html += '</div>';
2469
+ html += '<div class="msg-avatar" style="background:#1e40af">U</div>';
2470
+ html += '</div>';
2471
+ } else if (role === 'assistant') {
2472
+ html += '<div class="msg-row">';
2473
+ html += '<div class="msg-avatar" style="background:#000;border:1px solid #333">A</div>';
2474
+ html += '<div class="msg-content-col">';
2475
+ html += '<div class="msg-label">助手</div>';
2476
+ html += '<div class="msg-bubble msg-bubble-assistant">' + formatMsgText(msg.text || '') + '</div>';
2477
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
2478
+ html += '<div class="msg-tools">';
2479
+ msg.toolCalls.forEach(function(tc) {
2480
+ html += '<span class="msg-tool-tag">🔧 ' + escapeHtml(tc.name || 'tool') + '</span>';
2481
+ });
2482
+ html += '</div>';
2483
+ }
2484
+ html += '</div>';
2485
+ html += '</div>';
2472
2486
  }
2473
- html += '</div>';
2474
2487
  });
2475
2488
  msgViewerContent.innerHTML = html;
2476
2489
  msgViewerContent.scrollTop = msgViewerContent.scrollHeight;
@@ -2482,7 +2495,6 @@
2482
2495
 
2483
2496
  function closeMessageViewer() {
2484
2497
  messageViewer.classList.remove('visible');
2485
- rebindTouchScroll();
2486
2498
  }
2487
2499
 
2488
2500
  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.44",
3
+ "version": "2.6.46",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",