claude-opencode-viewer 2.0.0 → 2.1.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 +190 -30
- package/package.json +1 -1
- package/server.js +3 -0
package/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Claude OpenCode Viewer</title>
|
|
7
7
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
8
8
|
<style>
|
|
9
9
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -80,17 +80,20 @@
|
|
|
80
80
|
border-top: 1px solid #222;
|
|
81
81
|
flex-shrink: 0;
|
|
82
82
|
}
|
|
83
|
-
#virtual-keybar-
|
|
83
|
+
#virtual-keybar-row {
|
|
84
84
|
display: flex;
|
|
85
85
|
gap: 6px;
|
|
86
|
-
padding:
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
padding: 8px 10px;
|
|
87
|
+
overflow-x: auto;
|
|
88
|
+
-webkit-overflow-scrolling: touch;
|
|
89
|
+
scrollbar-width: none;
|
|
89
90
|
}
|
|
90
|
-
#virtual-keybar-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
#virtual-keybar-row::-webkit-scrollbar {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
94
|
+
#virtual-keybar-row button {
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
min-width: 56px;
|
|
94
97
|
height: 40px;
|
|
95
98
|
border: 1px solid #333;
|
|
96
99
|
border-radius: 6px;
|
|
@@ -98,11 +101,36 @@
|
|
|
98
101
|
color: #ccc;
|
|
99
102
|
font-size: 13px;
|
|
100
103
|
touch-action: manipulation;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
#virtual-keybar-row button:active {
|
|
107
|
+
background: #333;
|
|
108
|
+
}
|
|
109
|
+
#input-bar {
|
|
110
|
+
display: none;
|
|
111
|
+
background: #111;
|
|
112
|
+
border-top: 1px solid #222;
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
padding: 8px 10px;
|
|
115
|
+
}
|
|
116
|
+
#input-bar input {
|
|
117
|
+
width: 100%;
|
|
118
|
+
padding: 10px 12px;
|
|
119
|
+
background: #1a1a1a;
|
|
120
|
+
border: 1px solid #333;
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
color: #d4d4d4;
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
font-family: Menlo, Monaco, monospace;
|
|
125
|
+
outline: none;
|
|
126
|
+
}
|
|
127
|
+
#input-bar input:focus {
|
|
128
|
+
border-color: #4ade80;
|
|
101
129
|
}
|
|
102
|
-
#virtual-keybar-row1 button:active, #virtual-keybar-row2 button:active { background: #333; }
|
|
103
130
|
@media (max-width: 768px) {
|
|
104
131
|
#mode-switcher { display: block; }
|
|
105
132
|
#virtual-keybar { display: block; }
|
|
133
|
+
#input-bar { display: block; }
|
|
106
134
|
}
|
|
107
135
|
</style>
|
|
108
136
|
</head>
|
|
@@ -119,23 +147,21 @@
|
|
|
119
147
|
<option value="opencode">OpenCode</option>
|
|
120
148
|
</select>
|
|
121
149
|
</div>
|
|
122
|
-
|
|
150
|
+
|
|
151
|
+
<div id="input-bar">
|
|
152
|
+
<input type="text" id="input-field" placeholder="输入命令..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
123
155
|
<div id="virtual-keybar">
|
|
124
|
-
<div id="virtual-keybar-
|
|
125
|
-
<button data-key="
|
|
126
|
-
<button data-key="
|
|
127
|
-
<button data-key="
|
|
128
|
-
<button data-key="
|
|
129
|
-
<button data-key="
|
|
130
|
-
">
|
|
131
|
-
<button data-key="
|
|
132
|
-
|
|
133
|
-
<div id="virtual-keybar-row2">
|
|
134
|
-
<button data-key="">⌫</button>
|
|
135
|
-
<button data-key="">^C</button>
|
|
136
|
-
<button data-key="">^D</button>
|
|
137
|
-
<button data-key=" ">Space</button>
|
|
138
|
-
<button data-key=" ">Tab</button>
|
|
156
|
+
<div id="virtual-keybar-row">
|
|
157
|
+
<button data-key="up">↑</button>
|
|
158
|
+
<button data-key="down">↓</button>
|
|
159
|
+
<button data-key="left">←</button>
|
|
160
|
+
<button data-key="right">→</button>
|
|
161
|
+
<button data-key="enter">Enter</button>
|
|
162
|
+
<button data-key="esc">Esc</button>
|
|
163
|
+
<button data-key="ctrlc">^C</button>
|
|
164
|
+
<button data-key="tab">Tab</button>
|
|
139
165
|
</div>
|
|
140
166
|
</div>
|
|
141
167
|
|
|
@@ -437,6 +463,10 @@
|
|
|
437
463
|
|
|
438
464
|
term.onData(function(d) {
|
|
439
465
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'input', data: d }));
|
|
466
|
+
// 点击终端输入时,滚动到底部
|
|
467
|
+
setTimeout(function() {
|
|
468
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
469
|
+
}, 50);
|
|
440
470
|
});
|
|
441
471
|
}
|
|
442
472
|
|
|
@@ -445,11 +475,141 @@
|
|
|
445
475
|
window.addEventListener('orientationchange', function() { setTimeout(resize, 200); });
|
|
446
476
|
}
|
|
447
477
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
478
|
+
// 输入框状态
|
|
479
|
+
var inputField = document.getElementById('input-field');
|
|
480
|
+
var inputFocused = false;
|
|
481
|
+
|
|
482
|
+
if (inputField) {
|
|
483
|
+
// 跟踪输入框焦点状态
|
|
484
|
+
inputField.addEventListener('focus', function() {
|
|
485
|
+
inputFocused = true;
|
|
486
|
+
// iOS 上需要临时恢复 body 滚动能力
|
|
487
|
+
if (isIOS && document.body.style.position === 'fixed') {
|
|
488
|
+
var scrollTop = -parseFloat(document.body.style.top || '0');
|
|
489
|
+
document.body.style.position = '';
|
|
490
|
+
document.body.style.top = '';
|
|
491
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
492
|
+
// 恢复 fixed 定位
|
|
493
|
+
setTimeout(function() {
|
|
494
|
+
document.body.style.position = 'fixed';
|
|
495
|
+
document.body.style.top = '-' + scrollTop + 'px';
|
|
496
|
+
}, 300);
|
|
497
|
+
} else {
|
|
498
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
inputField.addEventListener('blur', function() {
|
|
503
|
+
inputFocused = false;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// 输入框回车处理
|
|
507
|
+
inputField.addEventListener('keydown', function(e) {
|
|
508
|
+
if (e.key === 'Enter' && ws && ws.readyState === 1 && !isTransitioning) {
|
|
509
|
+
var value = inputField.value;
|
|
510
|
+
if (value) {
|
|
511
|
+
console.log('[input enter] sending:', value);
|
|
512
|
+
ws.send(JSON.stringify({ type: 'input', data: value + '\r' }));
|
|
513
|
+
inputField.value = '';
|
|
514
|
+
}
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// 移动端触摸输入框时阻止终端获焦
|
|
520
|
+
if (isMobile) {
|
|
521
|
+
inputField.addEventListener('touchstart', function(e) {
|
|
522
|
+
e.stopPropagation();
|
|
523
|
+
}, { passive: true });
|
|
451
524
|
}
|
|
452
|
-
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 虚拟按键映射表
|
|
528
|
+
var KEY_MAP = {
|
|
529
|
+
'up': '\x1b[A',
|
|
530
|
+
'down': '\x1b[B',
|
|
531
|
+
'left': '\x1b[D',
|
|
532
|
+
'right': '\x1b[C',
|
|
533
|
+
'esc': '\x1b',
|
|
534
|
+
'ctrlc': '\x03',
|
|
535
|
+
'tab': '\t'
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
function sendKey(keyName) {
|
|
539
|
+
// Enter 键特殊处理:根据焦点位置决定行为
|
|
540
|
+
if (keyName === 'enter') {
|
|
541
|
+
if (inputFocused && inputField && inputField.value) {
|
|
542
|
+
// 焦点在输入框且有内容:只发送输入框内容,不额外发送 \r
|
|
543
|
+
var value = inputField.value;
|
|
544
|
+
console.log('[enter] from input:', value);
|
|
545
|
+
ws.send(JSON.stringify({ type: 'input', data: value + '\r' }));
|
|
546
|
+
inputField.value = '';
|
|
547
|
+
// 让输入框失去焦点,防止终端 textarea 同时收到输入
|
|
548
|
+
inputField.blur();
|
|
549
|
+
} else if (inputFocused && inputField) {
|
|
550
|
+
// 焦点在输入框但内容为空:不发送任何内容,避免误触
|
|
551
|
+
console.log('[enter] input focused but empty, ignored');
|
|
552
|
+
} else {
|
|
553
|
+
// 焦点在终端:直接发送回车
|
|
554
|
+
console.log('[enter] sending \\r to terminal');
|
|
555
|
+
ws.send(JSON.stringify({ type: 'input', data: '\r' }));
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 其他按键直接发送
|
|
561
|
+
var key = KEY_MAP[keyName];
|
|
562
|
+
if (key && ws && ws.readyState === 1 && !isTransitioning) {
|
|
563
|
+
console.log('[sendKey] sending:', keyName, '->', key);
|
|
564
|
+
ws.send(JSON.stringify({ type: 'input', data: key }));
|
|
565
|
+
} else {
|
|
566
|
+
console.log('[sendKey] skipped:', keyName, 'ws:', ws?.readyState, 'transitioning:', isTransitioning);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 虚拟按键处理:PC 端用 click,移动端用 touch
|
|
571
|
+
if (isMobile) {
|
|
572
|
+
// 移动端:使用触摸事件,区分点击和拖动(滑动)
|
|
573
|
+
var vkStartX = 0, vkStartY = 0, vkMoved = false;
|
|
574
|
+
|
|
575
|
+
keybar.addEventListener('touchstart', function(e) {
|
|
576
|
+
if (e.target.tagName !== 'BUTTON') return;
|
|
577
|
+
var touch = e.touches[0];
|
|
578
|
+
vkStartX = touch.clientX;
|
|
579
|
+
vkStartY = touch.clientY;
|
|
580
|
+
vkMoved = false;
|
|
581
|
+
e.target.style.background = '#333';
|
|
582
|
+
}, { passive: true });
|
|
583
|
+
|
|
584
|
+
keybar.addEventListener('touchmove', function(e) {
|
|
585
|
+
if (e.target.tagName !== 'BUTTON') return;
|
|
586
|
+
if (vkMoved) return;
|
|
587
|
+
var touch = e.touches[0];
|
|
588
|
+
var dx = touch.clientX - vkStartX;
|
|
589
|
+
var dy = touch.clientY - vkStartY;
|
|
590
|
+
if (dx * dx + dy * dy > 100) { // 10px 阈值,允许横向滑动
|
|
591
|
+
vkMoved = true;
|
|
592
|
+
}
|
|
593
|
+
}, { passive: true });
|
|
594
|
+
|
|
595
|
+
keybar.addEventListener('touchend', function(e) {
|
|
596
|
+
if (e.target.tagName !== 'BUTTON') return;
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
e.target.style.background = '';
|
|
599
|
+
if (!vkMoved) {
|
|
600
|
+
var keyName = e.target.getAttribute('data-key');
|
|
601
|
+
sendKey(keyName);
|
|
602
|
+
}
|
|
603
|
+
}, { passive: false });
|
|
604
|
+
} else {
|
|
605
|
+
// PC 端:直接用 click
|
|
606
|
+
keybar.addEventListener('click', function(e) {
|
|
607
|
+
if (e.target.tagName === 'BUTTON') {
|
|
608
|
+
var keyName = e.target.getAttribute('data-key');
|
|
609
|
+
sendKey(keyName);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
453
613
|
|
|
454
614
|
connect();
|
|
455
615
|
setTimeout(resize, 100);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -8,6 +8,9 @@ import { chmodSync, statSync } from 'node:fs';
|
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { WebSocketServer } from 'ws';
|
|
10
10
|
|
|
11
|
+
// 设置进程名为 claude-opencode-viewer
|
|
12
|
+
process.title = 'claude-opencode-viewer';
|
|
13
|
+
|
|
11
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
15
|
const PORT = 7008;
|
|
13
16
|
|