claude-opencode-viewer 2.4.1 → 2.5.0
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.html +277 -14
- package/package.json +1 -1
- package/server.js +26 -0
package/index.html
CHANGED
|
@@ -487,22 +487,117 @@
|
|
|
487
487
|
border-color: #555;
|
|
488
488
|
color: #fff;
|
|
489
489
|
}
|
|
490
|
+
|
|
491
|
+
/* 选择模式:原位文本层 */
|
|
492
|
+
#terminal.select-mode .xterm-screen {
|
|
493
|
+
visibility: hidden;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#select-text-layer {
|
|
497
|
+
display: none;
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
500
|
+
overflow-y: auto;
|
|
501
|
+
-webkit-overflow-scrolling: touch;
|
|
502
|
+
background: #0a0a0a;
|
|
503
|
+
padding: 4px 8px;
|
|
504
|
+
touch-action: auto;
|
|
505
|
+
z-index: 10;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#select-text-layer.visible {
|
|
509
|
+
display: block;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#select-hint {
|
|
513
|
+
position: sticky;
|
|
514
|
+
top: 0;
|
|
515
|
+
background: rgba(30,30,30,0.95);
|
|
516
|
+
color: #888;
|
|
517
|
+
font-size: 11px;
|
|
518
|
+
text-align: center;
|
|
519
|
+
padding: 6px 0;
|
|
520
|
+
border-bottom: 1px solid #333;
|
|
521
|
+
z-index: 1;
|
|
522
|
+
-webkit-user-select: none;
|
|
523
|
+
user-select: none;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#select-text-layer pre {
|
|
527
|
+
margin: 0;
|
|
528
|
+
color: #d4d4d4;
|
|
529
|
+
font-family: Menlo, Monaco, "Courier New", monospace;
|
|
530
|
+
font-size: 11px;
|
|
531
|
+
line-height: 1.4;
|
|
532
|
+
white-space: pre-wrap;
|
|
533
|
+
word-break: break-all;
|
|
534
|
+
-webkit-user-select: text;
|
|
535
|
+
user-select: text;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#select-mode-close {
|
|
539
|
+
position: absolute;
|
|
540
|
+
top: 6px;
|
|
541
|
+
right: 6px;
|
|
542
|
+
z-index: 20;
|
|
543
|
+
display: none;
|
|
544
|
+
background: rgba(50,50,50,0.9);
|
|
545
|
+
border: 1px solid #555;
|
|
546
|
+
color: #ccc;
|
|
547
|
+
width: 28px;
|
|
548
|
+
height: 28px;
|
|
549
|
+
border-radius: 50%;
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
line-height: 26px;
|
|
552
|
+
text-align: center;
|
|
553
|
+
cursor: pointer;
|
|
554
|
+
-webkit-user-select: none;
|
|
555
|
+
user-select: none;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* 复制成功提示 */
|
|
559
|
+
#copy-toast {
|
|
560
|
+
display: none;
|
|
561
|
+
position: fixed;
|
|
562
|
+
top: 50%;
|
|
563
|
+
left: 50%;
|
|
564
|
+
transform: translate(-50%, -50%);
|
|
565
|
+
background: rgba(40, 167, 69, 0.9);
|
|
566
|
+
color: #fff;
|
|
567
|
+
padding: 10px 24px;
|
|
568
|
+
border-radius: 8px;
|
|
569
|
+
font-size: 14px;
|
|
570
|
+
z-index: 9999;
|
|
571
|
+
pointer-events: none;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#copy-toast.show {
|
|
575
|
+
display: block;
|
|
576
|
+
}
|
|
490
577
|
</style>
|
|
491
578
|
</head>
|
|
492
579
|
<body>
|
|
493
580
|
<!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
|
|
494
581
|
<div id="layout">
|
|
495
582
|
<div id="header">
|
|
496
|
-
<div style="
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
583
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
584
|
+
<button class="history-toggle-btn" id="new-session-btn">
|
|
585
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
586
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
587
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
588
|
+
</svg>
|
|
589
|
+
<span>新会话</span>
|
|
590
|
+
</button>
|
|
591
|
+
<button class="history-toggle-btn" id="history-toggle">
|
|
592
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
593
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
594
|
+
<polyline points="12 6 12 12 16 14"></polyline>
|
|
595
|
+
</svg>
|
|
596
|
+
<span>历史</span>
|
|
597
|
+
</button>
|
|
598
|
+
</div>
|
|
504
599
|
<div id="mode-switcher">
|
|
505
|
-
<span id="mode-label"
|
|
600
|
+
<span id="mode-label"></span>
|
|
506
601
|
<select id="mode-select">
|
|
507
602
|
<option value="opencode">OpenCode</option>
|
|
508
603
|
<option value="claude">Claude</option>
|
|
@@ -576,7 +671,13 @@
|
|
|
576
671
|
|
|
577
672
|
<div id="content">
|
|
578
673
|
<div id="terminal-container">
|
|
579
|
-
<div id="terminal"
|
|
674
|
+
<div id="terminal">
|
|
675
|
+
<div id="select-text-layer">
|
|
676
|
+
<div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
|
|
677
|
+
<pre id="select-text-pre"></pre>
|
|
678
|
+
</div>
|
|
679
|
+
<button id="select-mode-close">✕</button>
|
|
680
|
+
</div>
|
|
580
681
|
<div id="virtual-keybar">
|
|
581
682
|
<div class="virtual-key" data-key="up">↑</div>
|
|
582
683
|
<div class="virtual-key" data-key="down">↓</div>
|
|
@@ -588,11 +689,13 @@
|
|
|
588
689
|
<div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
|
|
589
690
|
<div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
|
|
590
691
|
<div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
|
|
692
|
+
<div class="virtual-key" id="btn-copy">复制</div>
|
|
591
693
|
</div>
|
|
592
694
|
</div>
|
|
593
695
|
</div>
|
|
594
696
|
</div>
|
|
595
697
|
|
|
698
|
+
<div id="copy-toast">已复制</div>
|
|
596
699
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
597
700
|
<script>
|
|
598
701
|
(function() {
|
|
@@ -829,17 +932,40 @@
|
|
|
829
932
|
}
|
|
830
933
|
}
|
|
831
934
|
|
|
935
|
+
// 长按检测
|
|
936
|
+
var longPressTimer = null;
|
|
937
|
+
var longPressTriggered = false;
|
|
938
|
+
var LONG_PRESS_DELAY = 500; // ms
|
|
939
|
+
|
|
940
|
+
function clearLongPress() {
|
|
941
|
+
if (longPressTimer) {
|
|
942
|
+
clearTimeout(longPressTimer);
|
|
943
|
+
longPressTimer = null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
832
947
|
function handleTouchStart(e) {
|
|
833
948
|
console.log('[scroll] touchstart');
|
|
834
949
|
stopMomentum();
|
|
950
|
+
longPressTriggered = false;
|
|
951
|
+
clearLongPress();
|
|
835
952
|
if (e.touches.length !== 1) return;
|
|
836
953
|
lastY = e.touches[0].clientY;
|
|
837
954
|
lastTime = performance.now();
|
|
838
955
|
velocitySamples = [];
|
|
956
|
+
|
|
957
|
+
// 启动长按计时器
|
|
958
|
+
longPressTimer = setTimeout(function() {
|
|
959
|
+
longPressTriggered = true;
|
|
960
|
+
longPressTimer = null;
|
|
961
|
+
openSelectMode();
|
|
962
|
+
}, LONG_PRESS_DELAY);
|
|
839
963
|
}
|
|
840
964
|
|
|
841
965
|
function handleTouchMove(e) {
|
|
842
966
|
if (e.touches.length !== 1) return;
|
|
967
|
+
// 有移动则取消长按
|
|
968
|
+
clearLongPress();
|
|
843
969
|
var y = e.touches[0].clientY;
|
|
844
970
|
var now = performance.now();
|
|
845
971
|
var dt = now - lastTime;
|
|
@@ -863,6 +989,16 @@
|
|
|
863
989
|
|
|
864
990
|
function handleTouchEnd() {
|
|
865
991
|
console.log('[scroll] touchend');
|
|
992
|
+
clearLongPress();
|
|
993
|
+
|
|
994
|
+
// 长按已触发,不执行滚动惯性
|
|
995
|
+
if (longPressTriggered) {
|
|
996
|
+
longPressTriggered = false;
|
|
997
|
+
pendingDy = 0;
|
|
998
|
+
pixelAccum = 0;
|
|
999
|
+
velocitySamples = [];
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
866
1002
|
|
|
867
1003
|
if (scrollRaf) {
|
|
868
1004
|
cancelAnimationFrame(scrollRaf);
|
|
@@ -1048,10 +1184,16 @@
|
|
|
1048
1184
|
ws.onmessage = function(e) {
|
|
1049
1185
|
try {
|
|
1050
1186
|
var msg = JSON.parse(e.data);
|
|
1051
|
-
if (msg.type === 'data')
|
|
1187
|
+
if (msg.type === 'data') {
|
|
1188
|
+
if (!isCreatingNewSession) {
|
|
1189
|
+
throttledWrite(msg.data);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1052
1192
|
else if (msg.type === 'exit') {
|
|
1053
|
-
|
|
1054
|
-
|
|
1193
|
+
if (!isCreatingNewSession) {
|
|
1194
|
+
throttledWrite('\r\n\x1b[33m[进程已退出: ' + msg.exitCode + ']\x1b[0m\r\n');
|
|
1195
|
+
throttledWrite('\x1b[90m按 Enter 键重新启动 ' + currentMode + '...\x1b[0m\r\n');
|
|
1196
|
+
}
|
|
1055
1197
|
}
|
|
1056
1198
|
else if (msg.type === 'mode') {
|
|
1057
1199
|
endTransition(msg.mode);
|
|
@@ -1080,6 +1222,14 @@
|
|
|
1080
1222
|
// 恢复失败
|
|
1081
1223
|
term.write('\x1b[31m✗ 恢复失败: ' + msg.error + '\x1b[0m\r\n');
|
|
1082
1224
|
}
|
|
1225
|
+
else if (msg.type === 'new-session-ok') {
|
|
1226
|
+
isCreatingNewSession = false;
|
|
1227
|
+
term.clear();
|
|
1228
|
+
}
|
|
1229
|
+
else if (msg.type === 'new-session-error') {
|
|
1230
|
+
isCreatingNewSession = false;
|
|
1231
|
+
term.write('\x1b[31m✗ 新会话启动失败: ' + msg.error + '\x1b[0m\r\n');
|
|
1232
|
+
}
|
|
1083
1233
|
} catch(err) {}
|
|
1084
1234
|
};
|
|
1085
1235
|
|
|
@@ -1455,12 +1605,27 @@
|
|
|
1455
1605
|
|
|
1456
1606
|
if (historyBarVisible) {
|
|
1457
1607
|
historyBar.classList.add('visible');
|
|
1458
|
-
|
|
1608
|
+
if (currentMode === 'opencode') {
|
|
1609
|
+
loadSessions();
|
|
1610
|
+
} else {
|
|
1611
|
+
// claude 模式暂无历史会话功能
|
|
1612
|
+
var sessionList = document.getElementById('session-list');
|
|
1613
|
+
sessionList.innerHTML = '<div class="session-empty">Claude Code 暂不支持历史会话</div>';
|
|
1614
|
+
}
|
|
1459
1615
|
} else {
|
|
1460
1616
|
historyBar.classList.remove('visible');
|
|
1461
1617
|
}
|
|
1462
1618
|
}
|
|
1463
1619
|
|
|
1620
|
+
// 新会话按钮
|
|
1621
|
+
var isCreatingNewSession = false;
|
|
1622
|
+
document.getElementById('new-session-btn').addEventListener('click', function() {
|
|
1623
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1624
|
+
isCreatingNewSession = true;
|
|
1625
|
+
term.clear();
|
|
1626
|
+
ws.send(JSON.stringify({ type: 'new-session' }));
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1464
1629
|
// 绑定历史按钮
|
|
1465
1630
|
document.getElementById('history-toggle').addEventListener('click', function() {
|
|
1466
1631
|
toggleHistoryBar();
|
|
@@ -1526,6 +1691,104 @@
|
|
|
1526
1691
|
}
|
|
1527
1692
|
});
|
|
1528
1693
|
|
|
1694
|
+
// 提取终端缓冲区文本
|
|
1695
|
+
function getTerminalText() {
|
|
1696
|
+
var buf = term.buffer.active;
|
|
1697
|
+
var lines = [];
|
|
1698
|
+
for (var i = 0; i < buf.length; i++) {
|
|
1699
|
+
var line = buf.getLine(i);
|
|
1700
|
+
if (line) lines.push(line.translateToString(true));
|
|
1701
|
+
}
|
|
1702
|
+
// 去除尾部空行
|
|
1703
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
1704
|
+
lines.pop();
|
|
1705
|
+
}
|
|
1706
|
+
return lines.join('\n');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// 复制到剪贴板
|
|
1710
|
+
function copyToClipboard(text) {
|
|
1711
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1712
|
+
navigator.clipboard.writeText(text).then(showCopyToast).catch(function() {
|
|
1713
|
+
fallbackCopy(text);
|
|
1714
|
+
});
|
|
1715
|
+
} else {
|
|
1716
|
+
fallbackCopy(text);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function fallbackCopy(text) {
|
|
1721
|
+
var ta = document.createElement('textarea');
|
|
1722
|
+
ta.value = text;
|
|
1723
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
1724
|
+
document.body.appendChild(ta);
|
|
1725
|
+
ta.select();
|
|
1726
|
+
document.execCommand('copy');
|
|
1727
|
+
document.body.removeChild(ta);
|
|
1728
|
+
showCopyToast();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function showCopyToast() {
|
|
1732
|
+
var toast = document.getElementById('copy-toast');
|
|
1733
|
+
toast.classList.add('show');
|
|
1734
|
+
setTimeout(function() { toast.classList.remove('show'); }, 1200);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// "复制" 按钮
|
|
1738
|
+
document.getElementById('btn-copy').addEventListener('touchend', function(e) {
|
|
1739
|
+
e.preventDefault();
|
|
1740
|
+
var text = getTerminalText();
|
|
1741
|
+
if (text) copyToClipboard(text);
|
|
1742
|
+
});
|
|
1743
|
+
document.getElementById('btn-copy').addEventListener('click', function(e) {
|
|
1744
|
+
e.preventDefault();
|
|
1745
|
+
var text = getTerminalText();
|
|
1746
|
+
if (text) copyToClipboard(text);
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// 方案2: 长按进入选择模式 — 原位显示可选纯文本
|
|
1750
|
+
var selectTextLayer = document.getElementById('select-text-layer');
|
|
1751
|
+
var selectTextPre = document.getElementById('select-text-pre');
|
|
1752
|
+
var selectModeClose = document.getElementById('select-mode-close');
|
|
1753
|
+
var inSelectMode = false;
|
|
1754
|
+
|
|
1755
|
+
function openSelectMode() {
|
|
1756
|
+
if (inSelectMode) return;
|
|
1757
|
+
inSelectMode = true;
|
|
1758
|
+
// 收起键盘
|
|
1759
|
+
var xtermTa = terminalEl.querySelector('.xterm-helper-textarea');
|
|
1760
|
+
if (xtermTa) xtermTa.blur();
|
|
1761
|
+
document.activeElement && document.activeElement.blur();
|
|
1762
|
+
var text = getTerminalText();
|
|
1763
|
+
selectTextPre.textContent = text || '(终端内容为空)';
|
|
1764
|
+
terminalEl.classList.add('select-mode');
|
|
1765
|
+
selectTextLayer.classList.add('visible');
|
|
1766
|
+
selectModeClose.style.display = 'block';
|
|
1767
|
+
unbindTouchScroll();
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function closeSelectMode() {
|
|
1771
|
+
if (!inSelectMode) return;
|
|
1772
|
+
inSelectMode = false;
|
|
1773
|
+
terminalEl.classList.remove('select-mode');
|
|
1774
|
+
selectTextLayer.classList.remove('visible');
|
|
1775
|
+
selectModeClose.style.display = 'none';
|
|
1776
|
+
window.getSelection().removeAllRanges();
|
|
1777
|
+
rebindTouchScroll();
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
selectModeClose.addEventListener('click', function(e) {
|
|
1781
|
+
e.preventDefault();
|
|
1782
|
+
e.stopPropagation();
|
|
1783
|
+
closeSelectMode();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
selectModeClose.addEventListener('touchend', function(e) {
|
|
1787
|
+
e.preventDefault();
|
|
1788
|
+
e.stopPropagation();
|
|
1789
|
+
closeSelectMode();
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1529
1792
|
// 初始化虚拟按键事件
|
|
1530
1793
|
setupVirtualKeyEvents();
|
|
1531
1794
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -535,6 +535,32 @@ wss.on('connection', (ws, req) => {
|
|
|
535
535
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
|
+
} else if (msg.type === 'new-session') {
|
|
539
|
+
// 开启新会话:杀掉当前进程,重新启动不带 session 参数的 opencode
|
|
540
|
+
const mode = currentMode;
|
|
541
|
+
console.log(`[new-session] 开启新会话, mode=${mode}`);
|
|
542
|
+
|
|
543
|
+
if (mode === 'opencode' && opencodeProcess) {
|
|
544
|
+
try { opencodeProcess.kill(); } catch {}
|
|
545
|
+
opencodeProcess = null;
|
|
546
|
+
currentProcess = null;
|
|
547
|
+
} else if (mode === 'claude' && claudeProcess) {
|
|
548
|
+
try { claudeProcess.kill(); } catch {}
|
|
549
|
+
claudeProcess = null;
|
|
550
|
+
currentProcess = null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
outputBuffer = '';
|
|
554
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
555
|
+
|
|
556
|
+
// 先通知前端准备好,再启动新进程
|
|
557
|
+
ws.send(JSON.stringify({ type: 'new-session-ok', mode }));
|
|
558
|
+
try {
|
|
559
|
+
await spawnProcess(mode);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
console.error('[new-session] 启动失败:', e.message);
|
|
562
|
+
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
563
|
+
}
|
|
538
564
|
}
|
|
539
565
|
} catch (err) {
|
|
540
566
|
console.error('[WS] Error:', err.message);
|