claude-opencode-viewer 2.6.45 → 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 +2 -0
  2. package/index.html +142 -163
  3. package/package.json +1 -1
package/index-pc.html CHANGED
@@ -1591,6 +1591,8 @@
1591
1591
  currentMode = mode;
1592
1592
  modeSelect.value = mode;
1593
1593
  document.getElementById('mode-label').textContent = '';
1594
+ var label = mode === 'claude' ? 'Claude' : 'OpenCode';
1595
+ term.write('\r\n 正在启动 ' + label + (sessionId ? '(恢复会话)' : '') + '...\r\n');
1594
1596
  var msg = { type: 'init', mode: mode };
1595
1597
  if (sessionId) msg.sessionId = sessionId;
1596
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;
@@ -426,6 +429,7 @@
426
429
  display: flex;
427
430
  gap: 6px;
428
431
  padding: 8px 10px;
432
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
429
433
  background: #111;
430
434
  border-top: 1px solid #222;
431
435
  overflow-x: auto;
@@ -444,22 +448,19 @@
444
448
  font-family: Menlo, Monaco, monospace;
445
449
  cursor: pointer;
446
450
  user-select: none;
451
+ -webkit-user-select: none;
447
452
  -webkit-tap-highlight-color: transparent;
453
+ -webkit-touch-callout: none;
448
454
  touch-action: pan-x;
449
455
  min-width: 44px;
450
456
  min-height: 44px;
451
457
  display: flex;
452
- align-items: flex-start;
453
- padding-top: 40px;
458
+ align-items: center;
454
459
  justify-content: center;
455
- border: none;
456
460
  outline: none;
457
- -webkit-user-select: none;
458
- -moz-user-select: none;
459
- -ms-user-select: none;
460
461
  }
461
462
 
462
- .virtual-key:active {
463
+ .virtual-key-pressed {
463
464
  background: #333;
464
465
  border-color: #555;
465
466
  color: #fff;
@@ -468,11 +469,12 @@
468
469
  /* 消息查看器 */
469
470
  #message-viewer {
470
471
  display: none;
471
- position: fixed;
472
- top: 0; left: 0; right: 0; bottom: 0;
472
+ position: absolute;
473
+ inset: 0;
473
474
  background: #0a0a0a;
474
475
  z-index: 1000;
475
476
  flex-direction: column;
477
+ overflow: hidden;
476
478
  }
477
479
  #message-viewer.visible {
478
480
  display: flex;
@@ -506,61 +508,68 @@
506
508
  }
507
509
  #msg-viewer-content {
508
510
  flex: 1;
509
- overflow-y: auto;
511
+ overflow: auto;
510
512
  -webkit-overflow-scrolling: touch;
513
+ overscroll-behavior: contain;
511
514
  padding: 12px;
512
515
  }
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;
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;
538
534
  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
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; }
564
573
  .msg-empty {
565
574
  text-align: center;
566
575
  padding: 40px 20px;
@@ -1102,21 +1111,24 @@
1102
1111
  </div>
1103
1112
  </div>
1104
1113
  </div>
1105
- </div>
1106
1114
 
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>
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>
1114
1123
  </div>
1115
1124
  </div>
1116
1125
 
1117
1126
  <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>
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>
1120
1132
  <script>
1121
1133
  (function() {
1122
1134
  var isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
@@ -1129,6 +1141,9 @@
1129
1141
 
1130
1142
  var term = new Terminal({
1131
1143
  cursorBlink: !isMobile,
1144
+ cursorStyle: 'bar',
1145
+ cursorWidth: 1,
1146
+ cursorInactiveStyle: 'none',
1132
1147
  fontSize: fontSize,
1133
1148
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
1134
1149
  theme: {
@@ -1145,6 +1160,15 @@
1145
1160
 
1146
1161
  term.open(document.getElementById('terminal'));
1147
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
+
1148
1172
  // WebGL 渲染器:GPU 加速绘制,非 iOS 设备启用(iOS WebGL 性能差)
1149
1173
  if (!isIOS && window.WebglAddon) {
1150
1174
  try {
@@ -1834,6 +1858,11 @@
1834
1858
  if (seq && ws && ws.readyState === 1 && !isTransitioning) {
1835
1859
  ws.send(JSON.stringify({ type: 'input', data: seq }));
1836
1860
  }
1861
+ // 手机端主动 blur xterm textarea,防止系统键盘弹出
1862
+ if (isMobile) {
1863
+ var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
1864
+ if (xtermTa) xtermTa.blur();
1865
+ }
1837
1866
  }
1838
1867
 
1839
1868
  function scrollTerminal(lines) {
@@ -1841,111 +1870,62 @@
1841
1870
  doScroll(lines);
1842
1871
  }
1843
1872
 
1844
- // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
1845
- // 每个按键独立绑定事件,不在容器上绑定
1873
+ // 虚拟按键触摸处理
1846
1874
  var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
1847
- var scrollInterval = null; // 长按滚动定时器
1848
1875
 
1849
1876
  function setupVirtualKeyEvents() {
1850
1877
  var keys = document.querySelectorAll('.virtual-key');
1851
1878
  keys.forEach(function(key) {
1852
- // 防止元素获得焦点
1853
1879
  key.setAttribute('tabindex', '-1');
1854
1880
 
1855
- // 移动端触摸事件
1856
1881
  key.addEventListener('touchstart', function(e) {
1857
1882
  var touch = e.touches[0];
1858
1883
  vkStartX = touch.clientX;
1859
1884
  vkStartY = touch.clientY;
1860
1885
  vkMoved = false;
1861
1886
  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()
1887
+ vkTarget.classList.add('virtual-key-pressed');
1888
+ }, { passive: true });
1874
1889
 
1875
1890
  key.addEventListener('touchmove', function(e) {
1876
1891
  if (vkMoved) return;
1877
1892
  var touch = e.touches[0];
1878
1893
  var dx = touch.clientX - vkStartX;
1879
1894
  var dy = touch.clientY - vkStartY;
1880
- if (dx * dx + dy * dy > 64) { // 8px 阈值
1895
+ if (dx * dx + dy * dy > 64) {
1881
1896
  vkMoved = true;
1882
- if (vkTarget) {
1883
- vkTarget.style.background = '';
1884
- }
1885
1897
  }
1886
1898
  }, { passive: true });
1887
1899
 
1888
1900
  key.addEventListener('touchend', function(e) {
1889
- // 清除滚动定时器
1890
- if (scrollInterval) {
1891
- clearInterval(scrollInterval);
1892
- scrollInterval = null;
1893
- }
1894
-
1901
+ e.preventDefault();
1895
1902
  if (vkTarget) {
1896
- vkTarget.style.background = '';
1903
+ vkTarget.classList.remove('virtual-key-pressed');
1897
1904
  vkTarget = null;
1898
1905
  }
1899
-
1900
- // 如果没有移动,触发按键功能并阻止默认行为
1901
1906
  if (!vkMoved) {
1902
- e.preventDefault(); // 阻止默认行为
1903
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1904
- if (!scrollLines) {
1905
- // 非滚动按钮才触发按键
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 {
1906
1913
  var keyName = e.currentTarget.getAttribute('data-key');
1907
1914
  sendKey(keyName);
1908
1915
  }
1909
1916
  }
1910
1917
  }, { passive: false });
1911
1918
 
1912
- // PC端点击支持
1913
1919
  key.addEventListener('click', function(e) {
1914
1920
  e.preventDefault();
1915
- var scrollLines = e.currentTarget.getAttribute('data-scroll');
1916
- if (scrollLines) {
1917
- scrollTerminal(parseInt(scrollLines, 10));
1921
+ var scrollAttr = e.currentTarget.getAttribute('data-scroll');
1922
+ if (scrollAttr) {
1923
+ scrollTerminal(parseInt(scrollAttr, 10));
1918
1924
  } else {
1919
1925
  var keyName = e.currentTarget.getAttribute('data-key');
1920
1926
  sendKey(keyName);
1921
1927
  }
1922
1928
  });
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
1929
  });
1950
1930
  }
1951
1931
 
@@ -2404,7 +2384,6 @@
2404
2384
 
2405
2385
  messageViewer.classList.add('visible');
2406
2386
  msgViewerContent.innerHTML = '<div class="msg-empty">加载中...</div>';
2407
- unbindTouchScroll();
2408
2387
 
2409
2388
  if (currentMode === 'claude') {
2410
2389
  // Claude 模式:从 JSONL 文件读取消息
@@ -2465,22 +2444,12 @@
2465
2444
  }
2466
2445
 
2467
2446
  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
- }
2447
+ if (!text) return '';
2448
+ try {
2449
+ return DOMPurify.sanitize(marked.parse(text, { breaks: true }));
2450
+ } catch (e) {
2451
+ return escapeHtml(text);
2482
2452
  }
2483
- return result;
2484
2453
  }
2485
2454
 
2486
2455
  function renderMessages(messages) {
@@ -2491,19 +2460,30 @@
2491
2460
  var html = '';
2492
2461
  messages.forEach(function(msg) {
2493
2462
  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
- });
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>';
2505
2486
  }
2506
- html += '</div>';
2507
2487
  });
2508
2488
  msgViewerContent.innerHTML = html;
2509
2489
  msgViewerContent.scrollTop = msgViewerContent.scrollHeight;
@@ -2515,7 +2495,6 @@
2515
2495
 
2516
2496
  function closeMessageViewer() {
2517
2497
  messageViewer.classList.remove('visible');
2518
- rebindTouchScroll();
2519
2498
  }
2520
2499
 
2521
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.45",
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",