claude-opencode-viewer 2.3.0 → 2.3.1

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 (2) hide show
  1. package/index.html +392 -131
  2. package/package.json +1 -1
package/index.html CHANGED
@@ -116,6 +116,10 @@
116
116
  align-items: center;
117
117
  justify-content: center;
118
118
  border: none;
119
+ outline: none;
120
+ -webkit-user-select: none;
121
+ -moz-user-select: none;
122
+ -ms-user-select: none;
119
123
  }
120
124
 
121
125
  .virtual-key:active {
@@ -143,14 +147,16 @@
143
147
  <div id="terminal-container">
144
148
  <div id="terminal"></div>
145
149
  <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>
150
+ <div class="virtual-key" data-key="up">↑</div>
151
+ <div class="virtual-key" data-key="down">↓</div>
152
+ <div class="virtual-key" data-key="left">←</div>
153
+ <div class="virtual-key" data-key="right">→</div>
154
+ <div class="virtual-key" data-key="enter">Enter</div>
155
+ <div class="virtual-key" data-key="tab">Tab</div>
156
+ <div class="virtual-key" data-key="esc">Esc</div>
157
+ <div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
158
+ <div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
159
+ <div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
154
160
  </div>
155
161
  </div>
156
162
  </div>
@@ -197,6 +203,53 @@
197
203
  var momentumRaf = null;
198
204
  var velocitySamples = [];
199
205
 
206
+ // 未发送消息缓存
207
+ var currentInputBuffer = '';
208
+ var CACHE_KEY = 'claude_opencode_input_cache';
209
+ var cacheRestored = false; // 防止重复恢复
210
+
211
+ function saveInputCache() {
212
+ if (currentInputBuffer) {
213
+ localStorage.setItem(CACHE_KEY, currentInputBuffer);
214
+ console.log('[cache] saved:', currentInputBuffer);
215
+ } else {
216
+ localStorage.removeItem(CACHE_KEY);
217
+ }
218
+ }
219
+
220
+ function loadInputCache() {
221
+ // 确保只恢复一次
222
+ if (cacheRestored) {
223
+ console.log('[cache] already restored, skipping');
224
+ return;
225
+ }
226
+
227
+ var cached = localStorage.getItem(CACHE_KEY);
228
+ if (cached && ws && ws.readyState === 1) {
229
+ console.log('[cache] restoring:', cached);
230
+ // 立即清除缓存,防止重复调用
231
+ localStorage.removeItem(CACHE_KEY);
232
+ cacheRestored = true;
233
+
234
+ // 将缓存的输入发送到终端
235
+ ws.send(JSON.stringify({ type: 'input', data: cached }));
236
+ // 重要:恢复后清空 currentInputBuffer,防止再次保存
237
+ currentInputBuffer = '';
238
+ } else {
239
+ console.log('[cache] no cache to restore');
240
+ }
241
+ }
242
+
243
+ function clearInputCache() {
244
+ currentInputBuffer = '';
245
+ localStorage.removeItem(CACHE_KEY);
246
+ console.log('[cache] cleared');
247
+ }
248
+
249
+ function getCellDims() {
250
+ return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
251
+ }
252
+
200
253
  // 参考 cc-viewer 的 App.jsx 行 76-94: iOS visualViewport 处理
201
254
  // iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
202
255
  // 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
@@ -218,10 +271,6 @@
218
271
  onVVChange();
219
272
  }
220
273
 
221
- function getCellDims() {
222
- return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
223
- }
224
-
225
274
  // 参考 cc-viewer 的 TerminalPanel.jsx 行 410-447: 移动端固定尺寸计算
226
275
  function mobileFixedResize() {
227
276
  if (!term) return;
@@ -302,109 +351,187 @@
302
351
  pixelAccum = 0;
303
352
  }
304
353
 
305
- // 参考 cc-viewer TerminalPanel.jsx 172-309: 移动端触摸滚动完整实现
354
+ // OpenCode 模式触摸滚动实现 - 使用 xterm.js scrollLines API
355
+ var touchScreen = null;
356
+ var touchEventsBound = false;
357
+
358
+ function getLineHeight() {
359
+ var cellDims = getCellDims();
360
+ var height = (cellDims && cellDims.height) || 15;
361
+ console.log('[scroll] lineHeight:', height);
362
+ return height;
363
+ }
364
+
365
+ function stopMomentum() {
366
+ if (momentumRaf) {
367
+ cancelAnimationFrame(momentumRaf);
368
+ momentumRaf = null;
369
+ }
370
+ if (scrollRaf) {
371
+ cancelAnimationFrame(scrollRaf);
372
+ scrollRaf = null;
373
+ }
374
+ pendingDy = 0;
375
+ pixelAccum = 0;
376
+ }
377
+
306
378
  function flushScroll() {
307
379
  scrollRaf = null;
308
380
  if (pendingDy === 0) return;
381
+
309
382
  pixelAccum += pendingDy;
310
383
  pendingDy = 0;
311
- var cellDims = getCellDims();
312
- var lh = (cellDims && cellDims.height) || 15;
384
+
385
+ var lh = getLineHeight();
313
386
  var lines = Math.trunc(pixelAccum / lh);
387
+
314
388
  if (lines !== 0) {
389
+ console.log('[scroll] scrollLines:', lines, 'pixelAccum:', pixelAccum, 'lineHeight:', lh);
315
390
  term.scrollLines(lines);
316
391
  pixelAccum -= lines * lh;
317
392
  }
318
393
  }
319
394
 
320
- if (isMobile) {
321
- setTimeout(function() {
322
- var screen = document.querySelector('.xterm-screen');
323
- if (!screen) return;
324
-
325
- screen.addEventListener('touchstart', function(e) {
326
- stopMomentum();
327
- if (e.touches.length !== 1) return;
328
- lastY = e.touches[0].clientY;
329
- lastTime = performance.now();
330
- velocitySamples = [];
331
- }, { passive: true });
395
+ function handleTouchStart(e) {
396
+ console.log('[scroll] touchstart');
397
+ stopMomentum();
398
+ if (e.touches.length !== 1) return;
399
+ lastY = e.touches[0].clientY;
400
+ lastTime = performance.now();
401
+ velocitySamples = [];
402
+ }
332
403
 
333
- screen.addEventListener('touchmove', function(e) {
334
- if (e.touches.length !== 1) return;
335
- var y = e.touches[0].clientY;
336
- var now = performance.now();
337
- var dt = now - lastTime;
338
- var dy = lastY - y;
339
- if (dt > 0) {
340
- var v = dy / dt * 16;
341
- velocitySamples.push({ v: v, t: now });
342
- while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
343
- velocitySamples.shift();
344
- }
345
- }
346
- pendingDy += dy;
347
- if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
348
- lastY = y;
349
- lastTime = now;
350
- }, { passive: true });
404
+ function handleTouchMove(e) {
405
+ if (e.touches.length !== 1) return;
406
+ var y = e.touches[0].clientY;
407
+ var now = performance.now();
408
+ var dt = now - lastTime;
409
+ var dy = lastY - y;
410
+
411
+ if (dt > 0) {
412
+ var v = dy / dt * 16;
413
+ velocitySamples.push({ v: v, t: now });
414
+ while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
415
+ velocitySamples.shift();
416
+ }
417
+ }
351
418
 
352
- screen.addEventListener('touchend', function() {
353
- if (scrollRaf) {
354
- cancelAnimationFrame(scrollRaf);
355
- scrollRaf = null;
356
- }
357
- if (pendingDy !== 0) {
358
- pixelAccum += pendingDy;
359
- pendingDy = 0;
360
- var cellDims = getCellDims();
361
- var lh = (cellDims && cellDims.height) || 15;
362
- var lines = Math.trunc(pixelAccum / lh);
363
- if (lines !== 0) term.scrollLines(lines);
364
- pixelAccum = 0;
365
- }
419
+ pendingDy += dy;
420
+ console.log('[scroll] touchmove dy:', dy, 'pendingDy:', pendingDy);
366
421
 
367
- var velocity = 0;
368
- if (velocitySamples.length >= 2) {
369
- var totalWeight = 0;
370
- var weightedV = 0;
371
- var latest = velocitySamples[velocitySamples.length - 1].t;
372
- for (var i = 0; i < velocitySamples.length; i++) {
373
- var s = velocitySamples[i];
374
- var w = Math.max(0, 1 - (latest - s.t) / 100);
375
- weightedV += s.v * w;
376
- totalWeight += w;
377
- }
378
- velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
422
+ if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
423
+ lastY = y;
424
+ lastTime = now;
425
+ }
426
+
427
+ function handleTouchEnd() {
428
+ console.log('[scroll] touchend');
429
+
430
+ if (scrollRaf) {
431
+ cancelAnimationFrame(scrollRaf);
432
+ scrollRaf = null;
433
+ }
434
+
435
+ if (pendingDy !== 0) {
436
+ pixelAccum += pendingDy;
437
+ pendingDy = 0;
438
+ var lh = getLineHeight();
439
+ var lines = Math.trunc(pixelAccum / lh);
440
+ if (lines !== 0) {
441
+ console.log('[scroll] final scrollLines:', lines);
442
+ term.scrollLines(lines);
443
+ }
444
+ pixelAccum = 0;
445
+ }
446
+
447
+ var velocity = 0;
448
+ if (velocitySamples.length >= 2) {
449
+ var totalWeight = 0;
450
+ var weightedV = 0;
451
+ var latest = velocitySamples[velocitySamples.length - 1].t;
452
+ for (var i = 0; i < velocitySamples.length; i++) {
453
+ var s = velocitySamples[i];
454
+ var w = Math.max(0, 1 - (latest - s.t) / 100);
455
+ weightedV += s.v * w;
456
+ totalWeight += w;
457
+ }
458
+ velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
459
+ }
460
+ velocitySamples = [];
461
+
462
+ console.log('[scroll] velocity:', velocity);
463
+ if (Math.abs(velocity) < 0.5) return;
464
+
465
+ var friction = 0.95;
466
+ var mAccum = 0;
467
+ var tick = function() {
468
+ if (Math.abs(velocity) < 0.3) {
469
+ var lh = getLineHeight();
470
+ var rest = Math.round(mAccum / lh);
471
+ if (rest !== 0) {
472
+ console.log('[scroll] momentum final:', rest);
473
+ term.scrollLines(rest);
379
474
  }
380
- velocitySamples = [];
381
-
382
- if (Math.abs(velocity) < 0.5) return;
383
- var friction = 0.95;
384
- var mAccum = 0;
385
- var tick = function() {
386
- if (Math.abs(velocity) < 0.3) {
387
- var cellDims = getCellDims();
388
- var lh = (cellDims && cellDims.height) || 15;
389
- var rest = Math.round(mAccum / lh);
390
- if (rest !== 0) term.scrollLines(rest);
391
- momentumRaf = null;
392
- return;
393
- }
394
- mAccum += velocity;
395
- var cellDims = getCellDims();
396
- var lh = (cellDims && cellDims.height) || 15;
397
- var lines = Math.trunc(mAccum / lh);
398
- if (lines !== 0) {
399
- term.scrollLines(lines);
400
- mAccum -= lines * lh;
401
- }
402
- velocity *= friction;
403
- momentumRaf = requestAnimationFrame(tick);
404
- };
405
- momentumRaf = requestAnimationFrame(tick);
406
- }, { passive: true });
407
- }, 100);
475
+ momentumRaf = null;
476
+ return;
477
+ }
478
+ mAccum += velocity;
479
+ var lh = getLineHeight();
480
+ var lines = Math.trunc(mAccum / lh);
481
+ if (lines !== 0) {
482
+ term.scrollLines(lines);
483
+ mAccum -= lines * lh;
484
+ }
485
+ velocity *= friction;
486
+ momentumRaf = requestAnimationFrame(tick);
487
+ };
488
+ momentumRaf = requestAnimationFrame(tick);
489
+ }
490
+
491
+ function unbindTouchScroll() {
492
+ if (touchScreen && touchEventsBound) {
493
+ console.log('[scroll] unbinding touch events');
494
+ touchScreen.removeEventListener('touchstart', handleTouchStart);
495
+ touchScreen.removeEventListener('touchmove', handleTouchMove);
496
+ touchScreen.removeEventListener('touchend', handleTouchEnd);
497
+ touchEventsBound = false;
498
+ touchScreen = null;
499
+ }
500
+ }
501
+
502
+ function setupMobileTouchScroll() {
503
+ // 先解绑旧的
504
+ unbindTouchScroll();
505
+
506
+ // 只在 opencode 模式下绑定
507
+ if (currentMode !== 'opencode') {
508
+ console.log('[scroll] Not opencode mode, skip binding');
509
+ return;
510
+ }
511
+
512
+ var screen = terminalEl.querySelector('.xterm-screen');
513
+ if (!screen) {
514
+ console.log('[scroll] .xterm-screen not found, retrying...');
515
+ setTimeout(setupMobileTouchScroll, 50);
516
+ return;
517
+ }
518
+
519
+ touchScreen = screen;
520
+ screen.addEventListener('touchstart', handleTouchStart, { passive: true });
521
+ screen.addEventListener('touchmove', handleTouchMove, { passive: true });
522
+ screen.addEventListener('touchend', handleTouchEnd, { passive: true });
523
+ touchEventsBound = true;
524
+ console.log('[scroll] Touch events bound to .xterm-screen (opencode mode)');
525
+ }
526
+
527
+ function rebindTouchScroll() {
528
+ if (isMobile) {
529
+ setTimeout(setupMobileTouchScroll, 100);
530
+ }
531
+ }
532
+
533
+ if (isMobile) {
534
+ setupMobileTouchScroll();
408
535
  }
409
536
 
410
537
  function startTransition() {
@@ -452,6 +579,8 @@
452
579
  else if (msg.type === 'exit') throttledWrite('\r\n[进程已退出: ' + msg.exitCode + ']\r\n');
453
580
  else if (msg.type === 'mode') {
454
581
  endTransition(msg.mode);
582
+ // 模式切换完成后,重新绑定触摸事件
583
+ rebindTouchScroll();
455
584
  }
456
585
  else if (msg.type === 'switching') {
457
586
  // 服务端开始切换,前端清屏
@@ -469,12 +598,49 @@
469
598
  };
470
599
 
471
600
  term.onData(function(d) {
472
- if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'input', data: d }));
601
+ if (ws && ws.readyState === 1) {
602
+ ws.send(JSON.stringify({ type: 'input', data: d }));
603
+
604
+ // 缓存管理:跟踪当前行的输入(仅在 opencode 模式且未在恢复中)
605
+ if (currentMode === 'opencode' && cacheRestored) {
606
+ if (d === '\r' || d === '\n' || d === '\r\n') {
607
+ // 回车:命令已发送,清除缓存
608
+ clearInputCache();
609
+ } else if (d === '\x7f' || d === '\b') {
610
+ // 退格:删除最后一个字符
611
+ if (currentInputBuffer.length > 0) {
612
+ currentInputBuffer = currentInputBuffer.slice(0, -1);
613
+ saveInputCache();
614
+ }
615
+ } else if (d === '\x03') {
616
+ // Ctrl+C:中断,清除缓存
617
+ clearInputCache();
618
+ } else if (d === '\x15') {
619
+ // Ctrl+U:清空整行
620
+ clearInputCache();
621
+ } else if (d.length === 1 && d.charCodeAt(0) >= 32 && d.charCodeAt(0) < 127) {
622
+ // 可打印 ASCII 字符:添加到缓冲区
623
+ currentInputBuffer += d;
624
+ saveInputCache();
625
+ }
626
+ }
627
+ }
628
+
473
629
  // 点击终端输入时,滚动到底部
474
630
  setTimeout(function() {
475
631
  window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
476
632
  }, 50);
477
633
  });
634
+
635
+ // 页面加载后尝试恢复缓存的输入(仅在 opencode 模式)
636
+ setTimeout(function() {
637
+ if (currentMode === 'opencode') {
638
+ loadInputCache();
639
+ } else {
640
+ // 非 opencode 模式,标记为已恢复,避免后续缓存逻辑干扰
641
+ cacheRestored = true;
642
+ }
643
+ }, 800);
478
644
  }
479
645
 
480
646
  window.addEventListener('resize', resize);
@@ -484,6 +650,20 @@
484
650
  });
485
651
  }
486
652
 
653
+ // 页面卸载前保存输入缓存
654
+ window.addEventListener('beforeunload', function() {
655
+ if (currentInputBuffer) {
656
+ saveInputCache();
657
+ }
658
+ });
659
+
660
+ // 页面可见性变化时保存缓存
661
+ document.addEventListener('visibilitychange', function() {
662
+ if (document.hidden && currentInputBuffer) {
663
+ saveInputCache();
664
+ }
665
+ });
666
+
487
667
  // 虚拟按键映射表
488
668
  var KEY_MAP = {
489
669
  'up': '\x1b[A',
@@ -503,42 +683,123 @@
503
683
  }
504
684
  }
505
685
 
686
+ function scrollTerminal(lines) {
687
+ if (term) {
688
+ term.scrollLines(lines);
689
+ console.log('[scroll] scrolled by:', lines);
690
+ }
691
+ }
692
+
506
693
  // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
507
- var keybar = document.getElementById('virtual-keybar');
694
+ // 每个按键独立绑定事件,不在容器上绑定
508
695
  var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
696
+ var scrollInterval = null; // 长按滚动定时器
697
+
698
+ function setupVirtualKeyEvents() {
699
+ var keys = document.querySelectorAll('.virtual-key');
700
+ keys.forEach(function(key) {
701
+ // 防止元素获得焦点
702
+ key.setAttribute('tabindex', '-1');
703
+
704
+ // 移动端触摸事件
705
+ key.addEventListener('touchstart', function(e) {
706
+ var touch = e.touches[0];
707
+ vkStartX = touch.clientX;
708
+ vkStartY = touch.clientY;
709
+ vkMoved = false;
710
+ vkTarget = e.currentTarget;
711
+ vkTarget.style.background = '#333';
712
+
713
+ // 如果是滚动按钮,阻止默认行为(防止键盘弹出)并启动持续滚动
714
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
715
+ if (scrollLines) {
716
+ e.preventDefault(); // 关键:阻止默认行为,防止键盘弹出
717
+ scrollTerminal(parseInt(scrollLines, 10));
718
+ scrollInterval = setInterval(function() {
719
+ scrollTerminal(parseInt(scrollLines, 10));
720
+ }, 100);
721
+ }
722
+ }, { passive: false }); // 必须是 false 才能调用 preventDefault()
723
+
724
+ key.addEventListener('touchmove', function(e) {
725
+ if (vkMoved) return;
726
+ var touch = e.touches[0];
727
+ var dx = touch.clientX - vkStartX;
728
+ var dy = touch.clientY - vkStartY;
729
+ if (dx * dx + dy * dy > 64) { // 8px 阈值
730
+ vkMoved = true;
731
+ if (vkTarget) {
732
+ vkTarget.style.background = '';
733
+ }
734
+ }
735
+ }, { passive: true });
509
736
 
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
- });
737
+ key.addEventListener('touchend', function(e) {
738
+ // 清除滚动定时器
739
+ if (scrollInterval) {
740
+ clearInterval(scrollInterval);
741
+ scrollInterval = null;
742
+ }
520
743
 
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;
528
- }
529
- });
744
+ if (vkTarget) {
745
+ vkTarget.style.background = '';
746
+ vkTarget = null;
747
+ }
530
748
 
531
- keybar.addEventListener('touchend', function(e) {
532
- e.preventDefault();
533
- if (vkTarget) {
534
- vkTarget.style.background = '';
535
- vkTarget = null;
536
- }
537
- if (!vkMoved && e.target.classList.contains('virtual-key')) {
538
- var keyName = e.target.getAttribute('data-key');
539
- sendKey(keyName);
540
- }
541
- });
749
+ // 如果没有移动,触发按键功能并阻止默认行为
750
+ if (!vkMoved) {
751
+ e.preventDefault(); // 阻止默认行为
752
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
753
+ if (!scrollLines) {
754
+ // 非滚动按钮才触发按键
755
+ var keyName = e.currentTarget.getAttribute('data-key');
756
+ sendKey(keyName);
757
+ }
758
+ }
759
+ }, { passive: false });
760
+
761
+ // PC端点击支持
762
+ key.addEventListener('click', function(e) {
763
+ e.preventDefault();
764
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
765
+ if (scrollLines) {
766
+ scrollTerminal(parseInt(scrollLines, 10));
767
+ } else {
768
+ var keyName = e.currentTarget.getAttribute('data-key');
769
+ sendKey(keyName);
770
+ }
771
+ });
772
+
773
+ // PC端鼠标按下支持(长按滚动)
774
+ key.addEventListener('mousedown', function(e) {
775
+ e.preventDefault();
776
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
777
+ if (scrollLines) {
778
+ scrollTerminal(parseInt(scrollLines, 10));
779
+ scrollInterval = setInterval(function() {
780
+ scrollTerminal(parseInt(scrollLines, 10));
781
+ }, 100);
782
+ }
783
+ });
784
+
785
+ key.addEventListener('mouseup', function(e) {
786
+ if (scrollInterval) {
787
+ clearInterval(scrollInterval);
788
+ scrollInterval = null;
789
+ }
790
+ });
791
+
792
+ key.addEventListener('mouseleave', function(e) {
793
+ if (scrollInterval) {
794
+ clearInterval(scrollInterval);
795
+ scrollInterval = null;
796
+ }
797
+ });
798
+ });
799
+ }
800
+
801
+ // 初始化虚拟按键事件
802
+ setupVirtualKeyEvents();
542
803
 
543
804
  connect();
544
805
  setTimeout(resize, 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",