claude-opencode-viewer 2.2.1 → 2.3.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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm start:*)",
5
+ "Bash(kill:*)",
6
+ "Bash(pkill:*)",
7
+ "Bash(node:*)",
8
+ "Bash(lsof -ti:7008)"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="JAVA_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$" />
6
+ <orderEntry type="inheritedJdk" />
7
+ <orderEntry type="sourceFolder" forTests="false" />
8
+ </component>
9
+ </module>
@@ -0,0 +1,5 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ </profile>
5
+ </component>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/claude_opencode_viewer.iml" filepath="$PROJECT_DIR$/.idea/claude_opencode_viewer.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="SmartFoxWorkToolsProjectState">
4
+ <option name="branch" value="fix/v2.1.0-keyboard" />
5
+ <option name="gitUrl" value="https://github.com/ChrisJason121238/claude-opencode-viewer.git" />
6
+ <option name="localBranch" value="fix/v2.1.0-keyboard" />
7
+ <option name="revision" value="e81a3afd4423f60c1d4f185dd4b16cf7ad1848cc" />
8
+ </component>
9
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="tdkConfig">
4
+ <option name="projectKey" value="0c137ba0-d5df-44aa-97f8-3eee32813208" />
5
+ </component>
6
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
package/index.html CHANGED
@@ -2,139 +2,157 @@
2
2
  <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
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; }
10
- html, body { height: 100%; background: #0a0a0a; overflow: hidden; }
11
- body {
10
+ html, body { margin: 0; padding: 0; overflow: hidden; }
11
+
12
+ /* 参考 cc-viewer 的 App.jsx 行 1319: 移动端容器使用 100vw/100vh */
13
+ #layout {
14
+ width: 100vw;
15
+ height: 100vh;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background: #000;
19
+ overflow: hidden;
20
+ }
21
+
22
+ /* 参考 cc-viewer 的 App.jsx 行 1320-1335: 顶部工具栏 */
23
+ #header {
24
+ padding: 10px 12px;
25
+ background: #111;
26
+ border-bottom: 1px solid #222;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ flex-shrink: 0;
31
+ height: 40px;
32
+ }
33
+
34
+ #mode-switcher {
35
+ display: flex;
36
+ gap: 4px;
37
+ align-items: center;
38
+ }
39
+
40
+ #mode-label {
41
+ font-size: 12px;
42
+ color: #666;
43
+ }
44
+
45
+ #mode-select {
46
+ background: #1a1a1a;
47
+ color: #d4d4d4;
48
+ border: 1px solid #333;
49
+ border-radius: 4px;
50
+ padding: 4px 24px 4px 8px;
51
+ font-size: 12px;
52
+ cursor: pointer;
53
+ -webkit-appearance: none;
54
+ appearance: none;
55
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 10 10'%3E%3Cpath fill='%23888' d='M5 7L1 3h8z'/%3E%3C/svg%3E");
56
+ background-repeat: no-repeat;
57
+ background-position: right 6px center;
58
+ }
59
+
60
+ /* 参考 cc-viewer 的 App.jsx 行 1425: 内容区使用 flex: 1 + relative + overflow: hidden */
61
+ #content {
62
+ flex: 1;
63
+ position: relative;
64
+ overflow: hidden;
65
+ }
66
+
67
+ /* 参考 cc-viewer 的 TerminalPanel.jsx: 终端容器样式 */
68
+ #terminal-container {
69
+ height: 100%;
12
70
  display: flex;
13
71
  flex-direction: column;
14
72
  background: #0a0a0a;
15
73
  }
74
+
16
75
  #terminal {
17
76
  flex: 1;
18
- min-height: 0;
19
77
  overflow: hidden;
20
78
  padding: 4px 8px;
21
79
  touch-action: none;
22
80
  overscroll-behavior: contain;
23
81
  }
82
+
24
83
  #terminal.transitioning {
25
84
  opacity: 0.3;
26
85
  transition: opacity 0.3s ease;
27
86
  }
87
+
88
+ /* 参考 cc-viewer 的 TerminalPanel.module.css: 虚拟键盘栏 */
28
89
  #virtual-keybar {
29
- display: none;
30
- background: #111;
31
- border-top: 1px solid #222;
32
- flex-shrink: 0;
33
- }
34
- #virtual-keybar-row {
35
90
  display: flex;
36
91
  gap: 6px;
37
92
  padding: 8px 10px;
93
+ background: #111;
94
+ border-top: 1px solid #222;
38
95
  overflow-x: auto;
96
+ flex-shrink: 0;
39
97
  -webkit-overflow-scrolling: touch;
40
- scrollbar-width: none;
41
- }
42
- #virtual-keybar-row::-webkit-scrollbar {
43
- display: none;
44
98
  }
45
- #virtual-keybar-row button {
99
+
100
+ .virtual-key {
46
101
  flex-shrink: 0;
47
- min-width: 56px;
48
- height: 40px;
102
+ padding: 12px 20px;
49
103
  border: 1px solid #333;
50
- border-radius: 6px;
104
+ border-radius: 8px;
51
105
  background: #1a1a1a;
52
106
  color: #ccc;
53
- font-size: 13px;
54
- touch-action: manipulation;
55
- white-space: nowrap;
56
- }
57
- #virtual-keybar-row button:active {
58
- background: #333;
59
- }
60
- #input-bar {
61
- display: none;
62
- background: #111;
63
- border-top: 1px solid #222;
64
- flex-shrink: 0;
65
- padding: 8px 10px;
66
- display: flex;
67
- gap: 8px;
68
- align-items: center;
69
- }
70
- #input-bar input {
71
- flex: 1;
72
- min-width: 0;
73
- padding: 10px 12px;
74
- background: #1a1a1a;
75
- border: 1px solid #333;
76
- border-radius: 6px;
77
- color: #d4d4d4;
78
- font-size: 14px;
107
+ font-size: 15px;
79
108
  font-family: Menlo, Monaco, monospace;
80
- outline: none;
81
- }
82
- #input-bar input:focus {
83
- border-color: #4ade80;
84
- }
85
- #mode-switcher {
86
- display: none;
87
- flex-shrink: 0;
88
- }
89
- #mode-switcher select {
90
- background: #1a1a1a;
91
- color: #d4d4d4;
92
- border: 1px solid #333;
93
- border-radius: 4px;
94
- padding: 10px 28px 10px 12px;
95
- font-size: 13px;
96
109
  cursor: pointer;
97
- -webkit-appearance: none;
98
- -moz-appearance: none;
99
- appearance: none;
100
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%23888' d='M5 7L1 3h8z'/%3E%3C/svg%3E");
101
- background-repeat: no-repeat;
102
- background-position: right 8px center;
103
- }
104
- #mode-switcher select:focus {
105
- outline: none;
106
- border-color: #4ade80;
110
+ user-select: none;
111
+ -webkit-tap-highlight-color: transparent;
112
+ touch-action: pan-x;
113
+ min-width: 44px;
114
+ min-height: 44px;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ border: none;
107
119
  }
108
- @media (max-width: 768px) {
109
- #mode-switcher { display: block; }
110
- #virtual-keybar { display: block; }
111
- #input-bar { display: flex; }
120
+
121
+ .virtual-key:active {
122
+ background: #333;
123
+ border-color: #555;
124
+ color: #fff;
112
125
  }
113
126
  </style>
114
127
  </head>
115
128
  <body>
116
- <div id="terminal"></div>
117
-
118
- <div id="input-bar">
119
- <input type="text" id="input-field" placeholder="输入命令..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
120
- <div id="mode-switcher">
121
- <select id="mode-select">
122
- <option value="claude">Claude Code</option>
123
- <option value="opencode">OpenCode</option>
124
- </select>
129
+ <!-- 参考 cc-viewer 的 App.jsx 行 1315-1607: 完整的移动端布局结构 -->
130
+ <div id="layout">
131
+ <div id="header">
132
+ <div style="font-size: 12px; color: #aaa;">Claude OpenCode Viewer</div>
133
+ <div id="mode-switcher">
134
+ <span id="mode-label">Mode:</span>
135
+ <select id="mode-select">
136
+ <option value="opencode">OpenCode</option>
137
+ <option value="claude">Claude</option>
138
+ </select>
139
+ </div>
125
140
  </div>
126
- </div>
127
141
 
128
- <div id="virtual-keybar">
129
- <div id="virtual-keybar-row">
130
- <button data-key="up">↑</button>
131
- <button data-key="down">↓</button>
132
- <button data-key="left">←</button>
133
- <button data-key="right">→</button>
134
- <button data-key="enter">Enter</button>
135
- <button data-key="esc">Esc</button>
136
- <button data-key="ctrlc">^C</button>
137
- <button data-key="tab">Tab</button>
142
+ <div id="content">
143
+ <div id="terminal-container">
144
+ <div id="terminal"></div>
145
+ <div id="virtual-keybar">
146
+ <button class="virtual-key" data-key="up">↑</button>
147
+ <button class="virtual-key" data-key="down">↓</button>
148
+ <button class="virtual-key" data-key="left">←</button>
149
+ <button class="virtual-key" data-key="right">→</button>
150
+ <button class="virtual-key" data-key="enter">Enter</button>
151
+ <button class="virtual-key" data-key="tab">Tab</button>
152
+ <button class="virtual-key" data-key="esc">Esc</button>
153
+ <button class="virtual-key" data-key="ctrlc">Ctrl+C</button>
154
+ </div>
155
+ </div>
138
156
  </div>
139
157
  </div>
140
158
 
@@ -166,7 +184,6 @@
166
184
 
167
185
  term.open(document.getElementById('terminal'));
168
186
 
169
- var keybar = document.getElementById('virtual-keybar');
170
187
  var modeSelect = document.getElementById('mode-select');
171
188
  var terminalEl = document.getElementById('terminal');
172
189
  var ws = null;
@@ -180,25 +197,34 @@
180
197
  var momentumRaf = null;
181
198
  var velocitySamples = [];
182
199
 
200
+ // 参考 cc-viewer 的 App.jsx 行 76-94: iOS visualViewport 处理
201
+ // iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
202
+ // 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
203
+ // 事件同步可见区域的高度和偏移,用 fixed 定位将布局锁定在可见区域内。
183
204
  if (isIOS && window.visualViewport) {
184
- var body = document.body;
205
+ var layoutEl = document.getElementById('layout');
185
206
  var onVVChange = function() {
207
+ if (!layoutEl) return;
186
208
  var vv = window.visualViewport;
187
- body.style.height = vv.height + 'px';
188
- body.style.position = 'fixed';
189
- body.style.top = '-' + vv.offsetTop + 'px';
190
- body.style.width = '100%';
209
+ layoutEl.style.position = 'fixed';
210
+ layoutEl.style.top = vv.offsetTop + 'px';
211
+ layoutEl.style.height = vv.height + 'px';
212
+ layoutEl.style.width = '100%';
213
+ layoutEl.style.left = '0';
191
214
  setTimeout(mobileFixedResize, 50);
192
215
  };
193
216
  window.visualViewport.addEventListener('resize', onVVChange);
194
217
  window.visualViewport.addEventListener('scroll', onVVChange);
218
+ onVVChange();
195
219
  }
196
220
 
197
221
  function getCellDims() {
198
222
  return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
199
223
  }
200
224
 
225
+ // 参考 cc-viewer 的 TerminalPanel.jsx 行 410-447: 移动端固定尺寸计算
201
226
  function mobileFixedResize() {
227
+ if (!term) return;
202
228
  var cellDims = getCellDims();
203
229
  if (!cellDims || !cellDims.width || !cellDims.height) {
204
230
  setTimeout(mobileFixedResize, 50);
@@ -207,17 +233,15 @@
207
233
 
208
234
  var padX = 16;
209
235
  var padY = 8;
210
- var topBarHeight = 32;
211
- var switcherHeight = isMobile ? 48 : 0;
212
- var keybarHeight = isMobile ? 96 : 0;
236
+ var topBarHeight = 40;
237
+ var keybarHeight = 52;
213
238
 
214
239
  var availW = window.innerWidth - padX;
215
- var availH = window.innerHeight - topBarHeight - switcherHeight - keybarHeight - padY;
240
+ var availH = window.innerHeight - topBarHeight - keybarHeight - padY;
216
241
 
217
242
  var currentFontSize = term.options.fontSize;
218
243
  var currentCharWidth = cellDims.width;
219
244
  var targetFontSize = Math.floor(currentFontSize * availW / (MOBILE_COLS * currentCharWidth) * 10) / 10;
220
- targetFontSize = Math.max(8, targetFontSize);
221
245
 
222
246
  term.options.fontSize = targetFontSize;
223
247
 
@@ -278,20 +302,26 @@
278
302
  pixelAccum = 0;
279
303
  }
280
304
 
305
+ // 参考 cc-viewer 的 TerminalPanel.jsx 行 172-309: 移动端触摸滚动完整实现
281
306
  function flushScroll() {
282
307
  scrollRaf = null;
283
308
  if (pendingDy === 0) return;
284
309
  pixelAccum += pendingDy;
285
310
  pendingDy = 0;
286
311
  var cellDims = getCellDims();
287
- var lh = (cellDims && cellDims.height) || 16;
312
+ var lh = (cellDims && cellDims.height) || 15;
288
313
  var lines = Math.trunc(pixelAccum / lh);
289
- if (lines !== 0) { term.scrollLines(lines); pixelAccum -= lines * lh; }
314
+ if (lines !== 0) {
315
+ term.scrollLines(lines);
316
+ pixelAccum -= lines * lh;
317
+ }
290
318
  }
291
319
 
292
320
  if (isMobile) {
293
- var screen = document.querySelector('.xterm-screen');
294
- if (screen) {
321
+ setTimeout(function() {
322
+ var screen = document.querySelector('.xterm-screen');
323
+ if (!screen) return;
324
+
295
325
  screen.addEventListener('touchstart', function(e) {
296
326
  stopMomentum();
297
327
  if (e.touches.length !== 1) return;
@@ -309,7 +339,9 @@
309
339
  if (dt > 0) {
310
340
  var v = dy / dt * 16;
311
341
  velocitySamples.push({ v: v, t: now });
312
- while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) velocitySamples.shift();
342
+ while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
343
+ velocitySamples.shift();
344
+ }
313
345
  }
314
346
  pendingDy += dy;
315
347
  if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
@@ -318,19 +350,24 @@
318
350
  }, { passive: true });
319
351
 
320
352
  screen.addEventListener('touchend', function() {
321
- if (scrollRaf) { cancelAnimationFrame(scrollRaf); scrollRaf = null; }
353
+ if (scrollRaf) {
354
+ cancelAnimationFrame(scrollRaf);
355
+ scrollRaf = null;
356
+ }
322
357
  if (pendingDy !== 0) {
323
358
  pixelAccum += pendingDy;
324
359
  pendingDy = 0;
325
360
  var cellDims = getCellDims();
326
- var lh = (cellDims && cellDims.height) || 16;
361
+ var lh = (cellDims && cellDims.height) || 15;
327
362
  var lines = Math.trunc(pixelAccum / lh);
328
363
  if (lines !== 0) term.scrollLines(lines);
329
364
  pixelAccum = 0;
330
365
  }
366
+
331
367
  var velocity = 0;
332
368
  if (velocitySamples.length >= 2) {
333
- var totalWeight = 0, weightedV = 0;
369
+ var totalWeight = 0;
370
+ var weightedV = 0;
334
371
  var latest = velocitySamples[velocitySamples.length - 1].t;
335
372
  for (var i = 0; i < velocitySamples.length; i++) {
336
373
  var s = velocitySamples[i];
@@ -341,13 +378,14 @@
341
378
  velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
342
379
  }
343
380
  velocitySamples = [];
381
+
344
382
  if (Math.abs(velocity) < 0.5) return;
345
383
  var friction = 0.95;
346
384
  var mAccum = 0;
347
385
  var tick = function() {
348
386
  if (Math.abs(velocity) < 0.3) {
349
387
  var cellDims = getCellDims();
350
- var lh = (cellDims && cellDims.height) || 16;
388
+ var lh = (cellDims && cellDims.height) || 15;
351
389
  var rest = Math.round(mAccum / lh);
352
390
  if (rest !== 0) term.scrollLines(rest);
353
391
  momentumRaf = null;
@@ -355,15 +393,18 @@
355
393
  }
356
394
  mAccum += velocity;
357
395
  var cellDims = getCellDims();
358
- var lh = (cellDims && cellDims.height) || 16;
396
+ var lh = (cellDims && cellDims.height) || 15;
359
397
  var lines = Math.trunc(mAccum / lh);
360
- if (lines !== 0) { term.scrollLines(lines); mAccum -= lines * lh; }
398
+ if (lines !== 0) {
399
+ term.scrollLines(lines);
400
+ mAccum -= lines * lh;
401
+ }
361
402
  velocity *= friction;
362
403
  momentumRaf = requestAnimationFrame(tick);
363
404
  };
364
405
  momentumRaf = requestAnimationFrame(tick);
365
406
  }, { passive: true });
366
- }
407
+ }, 100);
367
408
  }
368
409
 
369
410
  function startTransition() {
@@ -438,56 +479,9 @@
438
479
 
439
480
  window.addEventListener('resize', resize);
440
481
  if (isMobile) {
441
- window.addEventListener('orientationchange', function() { setTimeout(resize, 200); });
442
- }
443
-
444
- // 输入框状态
445
- var inputField = document.getElementById('input-field');
446
- var inputFocused = false;
447
-
448
- if (inputField) {
449
- // 跟踪输入框焦点状态
450
- inputField.addEventListener('focus', function() {
451
- inputFocused = true;
452
- // iOS 上需要临时恢复 body 滚动能力
453
- if (isIOS && document.body.style.position === 'fixed') {
454
- var scrollTop = -parseFloat(document.body.style.top || '0');
455
- document.body.style.position = '';
456
- document.body.style.top = '';
457
- window.scrollTo(0, document.body.scrollHeight);
458
- // 恢复 fixed 定位
459
- setTimeout(function() {
460
- document.body.style.position = 'fixed';
461
- document.body.style.top = '-' + scrollTop + 'px';
462
- }, 300);
463
- } else {
464
- window.scrollTo(0, document.body.scrollHeight);
465
- }
466
- });
467
-
468
- inputField.addEventListener('blur', function() {
469
- inputFocused = false;
470
- });
471
-
472
- // 输入框回车处理
473
- inputField.addEventListener('keydown', function(e) {
474
- if (e.key === 'Enter' && ws && ws.readyState === 1 && !isTransitioning) {
475
- var value = inputField.value;
476
- if (value) {
477
- console.log('[input enter] sending:', value);
478
- ws.send(JSON.stringify({ type: 'input', data: value + '\r\n' }));
479
- inputField.value = '';
480
- }
481
- e.preventDefault();
482
- }
482
+ window.addEventListener('orientationchange', function() {
483
+ setTimeout(resize, 200);
483
484
  });
484
-
485
- // 移动端触摸输入框时阻止终端获焦
486
- if (isMobile) {
487
- inputField.addEventListener('touchstart', function(e) {
488
- e.stopPropagation();
489
- }, { passive: true });
490
- }
491
485
  }
492
486
 
493
487
  // 虚拟按键映射表
@@ -498,123 +492,53 @@
498
492
  'right': '\x1b[C',
499
493
  'esc': '\x1b',
500
494
  'ctrlc': '\x03',
501
- 'tab': '\t'
495
+ 'tab': '\t',
496
+ 'enter': '\r'
502
497
  };
503
498
 
504
- // 方向键列表
505
- var ARROW_KEYS = ['up', 'down', 'left', 'right'];
506
-
507
499
  function sendKey(keyName) {
508
- // 重新获取焦点状态(确保使用最新状态)
509
- var isInputFocused = (inputField && document.activeElement === inputField);
510
-
511
- // 方向键特殊处理:根据焦点位置决定行为
512
- if (ARROW_KEYS.indexOf(keyName) !== -1) {
513
- if (isInputFocused) {
514
- // 焦点在输入框:模拟方向键操作输入框光标
515
- var cursorPos = inputField.selectionStart;
516
- var textLen = inputField.value.length;
517
-
518
- if (keyName === 'left' && cursorPos > 0) {
519
- inputField.setSelectionRange(cursorPos - 1, cursorPos - 1);
520
- } else if (keyName === 'right' && cursorPos < textLen) {
521
- inputField.setSelectionRange(cursorPos + 1, cursorPos + 1);
522
- }
523
- // 上下方向键在单行输入框中无操作
524
- return;
525
- } else {
526
- // 焦点在终端:发送方向键到终端,并收起键盘
527
- if (isMobile) {
528
- inputField.blur();
529
- }
530
- }
531
- } else {
532
- // 非方向键:点击时收起键盘
533
- if (isMobile) {
534
- inputField.blur();
535
- }
500
+ var seq = KEY_MAP[keyName];
501
+ if (seq && ws && ws.readyState === 1 && !isTransitioning) {
502
+ ws.send(JSON.stringify({ type: 'input', data: seq }));
536
503
  }
504
+ }
537
505
 
538
- // Enter 键特殊处理:根据焦点位置决定行为
539
- if (keyName === 'enter') {
540
- if (isInputFocused && inputField && inputField.value) {
541
- // 焦点在输入框且有内容:只发送输入框内容,不额外发送 \r
542
- var value = inputField.value;
543
- console.log('[enter] from input:', value);
544
- ws.send(JSON.stringify({ type: 'input', data: value + '\r\n' }));
545
- inputField.value = '';
546
- inputField.blur();
547
- } else if (isInputFocused && inputField) {
548
- // 焦点在输入框但内容为空:不发送任何内容,避免误触
549
- console.log('[enter] input focused but empty, ignored');
550
- } else {
551
- // 焦点在终端:直接发送回车
552
- console.log('[enter] sending \\r to terminal');
553
- ws.send(JSON.stringify({ type: 'input', data: '\r\n' }));
554
- }
555
- return;
556
- }
506
+ // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
507
+ var keybar = document.getElementById('virtual-keybar');
508
+ var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
509
+
510
+ keybar.addEventListener('touchstart', function(e) {
511
+ if (!e.target.classList.contains('virtual-key')) return;
512
+ e.preventDefault();
513
+ var touch = e.touches[0];
514
+ vkStartX = touch.clientX;
515
+ vkStartY = touch.clientY;
516
+ vkMoved = false;
517
+ vkTarget = e.target;
518
+ vkTarget.style.background = '#333';
519
+ });
557
520
 
558
- // Esc 键:中断终端进程(发送 ESC 字符)
559
- if (keyName === 'esc') {
560
- console.log('[esc] sending escape to terminal');
561
- ws.send(JSON.stringify({ type: 'input', data: '\x1b' }));
562
- return;
521
+ keybar.addEventListener('touchmove', function(e) {
522
+ if (vkMoved) return;
523
+ var touch = e.touches[0];
524
+ var dx = touch.clientX - vkStartX;
525
+ var dy = touch.clientY - vkStartY;
526
+ if (dx * dx + dy * dy > 64) {
527
+ vkMoved = true;
563
528
  }
529
+ });
564
530
 
565
- // 其他按键直接发送到终端
566
- var key = KEY_MAP[keyName];
567
- if (key && ws && ws.readyState === 1 && !isTransitioning) {
568
- console.log('[sendKey] sending:', keyName, '->', key);
569
- ws.send(JSON.stringify({ type: 'input', data: key }));
570
- } else {
571
- console.log('[sendKey] skipped:', keyName, 'ws:', ws?.readyState, 'transitioning:', isTransitioning);
531
+ keybar.addEventListener('touchend', function(e) {
532
+ e.preventDefault();
533
+ if (vkTarget) {
534
+ vkTarget.style.background = '';
535
+ vkTarget = null;
572
536
  }
573
- }
574
-
575
- // 虚拟按键处理:PC 端用 click,移动端用 touch
576
- if (isMobile) {
577
- // 移动端:使用触摸事件,区分点击和拖动(滑动)
578
- var vkStartX = 0, vkStartY = 0, vkMoved = false;
579
-
580
- keybar.addEventListener('touchstart', function(e) {
581
- if (e.target.tagName !== 'BUTTON') return;
582
- var touch = e.touches[0];
583
- vkStartX = touch.clientX;
584
- vkStartY = touch.clientY;
585
- vkMoved = false;
586
- e.target.style.background = '#333';
587
- }, { passive: true });
588
-
589
- keybar.addEventListener('touchmove', function(e) {
590
- if (e.target.tagName !== 'BUTTON') return;
591
- if (vkMoved) return;
592
- var touch = e.touches[0];
593
- var dx = touch.clientX - vkStartX;
594
- var dy = touch.clientY - vkStartY;
595
- if (dx * dx + dy * dy > 100) { // 10px 阈值,允许横向滑动
596
- vkMoved = true;
597
- }
598
- }, { passive: true });
599
-
600
- keybar.addEventListener('touchend', function(e) {
601
- if (e.target.tagName !== 'BUTTON') return;
602
- e.preventDefault();
603
- e.target.style.background = '';
604
- if (!vkMoved) {
605
- var keyName = e.target.getAttribute('data-key');
606
- sendKey(keyName);
607
- }
608
- }, { passive: false });
609
- } else {
610
- // PC 端:直接用 click
611
- keybar.addEventListener('click', function(e) {
612
- if (e.target.tagName === 'BUTTON') {
613
- var keyName = e.target.getAttribute('data-key');
614
- sendKey(keyName);
615
- }
616
- });
617
- }
537
+ if (!vkMoved && e.target.classList.contains('virtual-key')) {
538
+ var keyName = e.target.getAttribute('data-key');
539
+ sendKey(keyName);
540
+ }
541
+ });
618
542
 
619
543
  connect();
620
544
  setTimeout(resize, 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",