claude-opencode-viewer 2.3.0 → 2.4.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 CHANGED
@@ -29,6 +29,366 @@
29
29
  justify-content: space-between;
30
30
  flex-shrink: 0;
31
31
  height: 40px;
32
+ gap: 12px;
33
+ }
34
+
35
+ #session-history-bar {
36
+ display: none;
37
+ position: absolute;
38
+ top: 0;
39
+ left: 0;
40
+ right: 0;
41
+ bottom: 0;
42
+ background: #0a0a0a;
43
+ z-index: 1000;
44
+ flex-direction: column;
45
+ }
46
+
47
+ #session-history-bar.visible {
48
+ display: flex;
49
+ }
50
+
51
+ #session-list-view {
52
+ display: flex;
53
+ flex-direction: column;
54
+ flex: 1;
55
+ min-height: 0;
56
+ }
57
+
58
+ #session-list-view.hidden {
59
+ display: none;
60
+ }
61
+
62
+ #session-history-header {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ padding: 12px 16px;
67
+ background: #111;
68
+ border-bottom: 1px solid #222;
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ #session-history-title {
73
+ font-size: 14px;
74
+ color: #ddd;
75
+ font-weight: 600;
76
+ }
77
+
78
+ #session-history-actions {
79
+ display: flex;
80
+ gap: 8px;
81
+ }
82
+
83
+ #session-list-container {
84
+ flex: 1;
85
+ overflow-y: auto;
86
+ padding: 12px;
87
+ -webkit-overflow-scrolling: touch;
88
+ }
89
+
90
+ #session-detail-view {
91
+ display: none;
92
+ flex-direction: column;
93
+ flex: 1;
94
+ min-height: 0;
95
+ }
96
+
97
+ #session-detail-view.visible {
98
+ display: flex;
99
+ }
100
+
101
+ #session-detail-header {
102
+ padding: 12px 16px;
103
+ background: #0d0d0d;
104
+ border-bottom: 1px solid #222;
105
+ flex-shrink: 0;
106
+ }
107
+
108
+ #session-detail-title {
109
+ font-size: 14px;
110
+ color: #ddd;
111
+ font-weight: 500;
112
+ margin-bottom: 4px;
113
+ }
114
+
115
+ #session-detail-meta {
116
+ font-size: 11px;
117
+ color: #888;
118
+ display: flex;
119
+ gap: 12px;
120
+ }
121
+
122
+ #session-detail-content {
123
+ flex: 1;
124
+ overflow-y: auto;
125
+ padding: 16px;
126
+ -webkit-overflow-scrolling: touch;
127
+ }
128
+
129
+ .message-item {
130
+ margin-bottom: 20px;
131
+ display: flex;
132
+ gap: 12px;
133
+ align-items: flex-start;
134
+ }
135
+
136
+ /* User 消息:左对齐 */
137
+ .message-user {
138
+ justify-content: flex-start;
139
+ }
140
+
141
+ /* Assistant 消息:右对齐 */
142
+ .message-assistant {
143
+ justify-content: flex-end;
144
+ flex-direction: row-reverse;
145
+ }
146
+
147
+ .message-avatar {
148
+ flex-shrink: 0;
149
+ width: 32px;
150
+ height: 32px;
151
+ border-radius: 50%;
152
+ background: #2a4a7c;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ font-size: 14px;
157
+ }
158
+
159
+ .message-assistant .message-avatar {
160
+ background: #1a5a3a;
161
+ }
162
+
163
+ .message-content {
164
+ flex: 1;
165
+ min-width: 0;
166
+ max-width: 80%;
167
+ }
168
+
169
+ .message-header {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 8px;
173
+ margin-bottom: 6px;
174
+ }
175
+
176
+ /* User 的标题左对齐 */
177
+ .message-user .message-header {
178
+ justify-content: flex-start;
179
+ }
180
+
181
+ /* Assistant 的标题右对齐 */
182
+ .message-assistant .message-header {
183
+ justify-content: flex-end;
184
+ flex-direction: row-reverse;
185
+ }
186
+
187
+ .message-role {
188
+ font-size: 11px;
189
+ color: #888;
190
+ font-weight: 600;
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.5px;
193
+ }
194
+
195
+ .message-text {
196
+ color: #ddd;
197
+ font-size: 13px;
198
+ line-height: 1.6;
199
+ white-space: pre-wrap;
200
+ word-break: break-word;
201
+ background: #141414;
202
+ padding: 12px;
203
+ border-radius: 8px;
204
+ border: 1px solid #222;
205
+ }
206
+
207
+ /* User 消息气泡 */
208
+ .message-user .message-text {
209
+ background: #1a2332;
210
+ border-color: #2a4a7c;
211
+ }
212
+
213
+ /* Assistant 消息气泡 */
214
+ .message-assistant .message-text {
215
+ background: #1a2e1a;
216
+ border-color: #2a5a3a;
217
+ }
218
+
219
+ .message-tool-call {
220
+ margin-top: 8px;
221
+ padding: 8px 12px;
222
+ background: #1a1a0a;
223
+ border: 1px solid #333;
224
+ border-radius: 6px;
225
+ font-size: 12px;
226
+ }
227
+
228
+ .message-tool-name {
229
+ color: #f0ad4e;
230
+ font-weight: 600;
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 6px;
234
+ }
235
+
236
+ .message-tool-result {
237
+ margin-top: 6px;
238
+ padding: 8px;
239
+ background: #0a0a0a;
240
+ border-radius: 4px;
241
+ font-size: 11px;
242
+ color: #999;
243
+ max-height: 100px;
244
+ overflow-y: auto;
245
+ }
246
+
247
+ .message-empty {
248
+ text-align: center;
249
+ padding: 40px 20px;
250
+ color: #666;
251
+ font-size: 13px;
252
+ }
253
+
254
+ #session-list {
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: 6px;
258
+ }
259
+
260
+ .session-item {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 8px;
264
+ padding: 8px 12px;
265
+ background: #1a1a1a;
266
+ border: 1px solid #333;
267
+ border-radius: 6px;
268
+ cursor: pointer;
269
+ transition: all 0.15s;
270
+ min-width: 0;
271
+ }
272
+
273
+ .session-item:hover {
274
+ background: #252525;
275
+ border-color: #555;
276
+ }
277
+
278
+ .session-item.active {
279
+ background: #2a4a7c;
280
+ border-color: #4a8cff;
281
+ }
282
+
283
+ .session-icon {
284
+ flex-shrink: 0;
285
+ width: 8px;
286
+ height: 8px;
287
+ border-radius: 50%;
288
+ background: #666;
289
+ }
290
+
291
+ .session-item.active .session-icon {
292
+ background: #4a8cff;
293
+ }
294
+
295
+ .session-info {
296
+ flex: 1;
297
+ min-width: 0;
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: 2px;
301
+ }
302
+
303
+ .session-title {
304
+ font-size: 13px;
305
+ color: #ddd;
306
+ font-weight: 500;
307
+ line-height: 1.4;
308
+ display: -webkit-box;
309
+ -webkit-line-clamp: 2;
310
+ -webkit-box-orient: vertical;
311
+ overflow: hidden;
312
+ text-overflow: ellipsis;
313
+ word-break: break-word;
314
+ }
315
+
316
+ .session-item.active .session-title {
317
+ color: #fff;
318
+ }
319
+
320
+ .session-meta {
321
+ font-size: 11px;
322
+ color: #888;
323
+ display: flex;
324
+ gap: 8px;
325
+ }
326
+
327
+ .session-time {
328
+ flex-shrink: 0;
329
+ }
330
+
331
+ .session-dir {
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ .history-toggle-btn {
338
+ background: none;
339
+ border: 1px solid #333;
340
+ color: #aaa;
341
+ padding: 4px 10px;
342
+ font-size: 12px;
343
+ cursor: pointer;
344
+ border-radius: 4px;
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 4px;
348
+ flex-shrink: 0;
349
+ }
350
+
351
+ .history-toggle-btn:hover {
352
+ background: #2a2a2a;
353
+ color: #ddd;
354
+ border-color: #555;
355
+ }
356
+
357
+ .history-toggle-btn svg {
358
+ width: 14px;
359
+ height: 14px;
360
+ }
361
+
362
+ .session-loading {
363
+ text-align: center;
364
+ padding: 16px;
365
+ color: #888;
366
+ font-size: 12px;
367
+ }
368
+
369
+ .session-empty {
370
+ text-align: center;
371
+ padding: 16px;
372
+ color: #666;
373
+ font-size: 12px;
374
+ }
375
+
376
+ .session-restore-hint {
377
+ margin-top: 8px;
378
+ padding: 8px 12px;
379
+ background: #1a1a1a;
380
+ border: 1px solid #333;
381
+ border-radius: 6px;
382
+ font-size: 11px;
383
+ color: #888;
384
+ text-align: center;
385
+ }
386
+
387
+ .session-restore-hint code {
388
+ color: #4a8cff;
389
+ background: #0a0a0a;
390
+ padding: 2px 6px;
391
+ border-radius: 3px;
32
392
  }
33
393
 
34
394
  #mode-switcher {
@@ -116,6 +476,10 @@
116
476
  align-items: center;
117
477
  justify-content: center;
118
478
  border: none;
479
+ outline: none;
480
+ -webkit-user-select: none;
481
+ -moz-user-select: none;
482
+ -ms-user-select: none;
119
483
  }
120
484
 
121
485
  .virtual-key:active {
@@ -130,6 +494,13 @@
130
494
  <div id="layout">
131
495
  <div id="header">
132
496
  <div style="font-size: 12px; color: #aaa;">Claude OpenCode Viewer</div>
497
+ <button class="history-toggle-btn" id="history-toggle">
498
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
499
+ <circle cx="12" cy="12" r="10"></circle>
500
+ <polyline points="12 6 12 12 16 14"></polyline>
501
+ </svg>
502
+ <span>历史</span>
503
+ </button>
133
504
  <div id="mode-switcher">
134
505
  <span id="mode-label">Mode:</span>
135
506
  <select id="mode-select">
@@ -139,18 +510,84 @@
139
510
  </div>
140
511
  </div>
141
512
 
513
+ <!-- 会话历史栏 -->
514
+ <div id="session-history-bar">
515
+ <!-- 会话列表视图 -->
516
+ <div id="session-list-view">
517
+ <div id="session-history-header">
518
+ <div id="session-history-title">历史会话</div>
519
+ <div id="session-history-actions">
520
+ <button class="history-toggle-btn" id="refresh-sessions">
521
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
522
+ <polyline points="23 4 23 10 17 10"></polyline>
523
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
524
+ </svg>
525
+ <span>刷新</span>
526
+ </button>
527
+ <button class="history-toggle-btn" id="close-history">
528
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
529
+ <line x1="18" y1="6" x2="6" y2="18"></line>
530
+ <line x1="6" y1="6" x2="18" y2="18"></line>
531
+ </svg>
532
+ <span>返回</span>
533
+ </button>
534
+ </div>
535
+ </div>
536
+ <div id="session-list-container">
537
+ <div id="session-list">
538
+ <div class="session-loading">加载历史会话...</div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <!-- 会话详情视图 -->
544
+ <div id="session-detail-view">
545
+ <div id="session-history-header">
546
+ <div id="session-history-title">会话详情</div>
547
+ <div id="session-history-actions">
548
+ <button class="history-toggle-btn" id="restore-session" style="background: #1a5a3a; border-color: #2a7a4a;">
549
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
550
+ <polyline points="1 4 1 10 7 10"></polyline>
551
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
552
+ </svg>
553
+ <span>恢复会话</span>
554
+ </button>
555
+ <button class="history-toggle-btn" id="back-to-list">
556
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
557
+ <line x1="19" y1="12" x2="5" y2="12"></line>
558
+ <polyline points="12 19 5 12 12 5"></polyline>
559
+ </svg>
560
+ <span>返回列表</span>
561
+ </button>
562
+ </div>
563
+ </div>
564
+ <div id="session-detail-header">
565
+ <div id="session-detail-title">会话标题</div>
566
+ <div id="session-detail-meta">
567
+ <span id="session-detail-time">时间</span>
568
+ <span id="session-detail-dir">目录</span>
569
+ </div>
570
+ </div>
571
+ <div id="session-detail-content">
572
+ <div class="message-empty">加载中...</div>
573
+ </div>
574
+ </div>
575
+ </div>
576
+
142
577
  <div id="content">
143
578
  <div id="terminal-container">
144
579
  <div id="terminal"></div>
145
580
  <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>
581
+ <div class="virtual-key" data-key="up">↑</div>
582
+ <div class="virtual-key" data-key="down">↓</div>
583
+ <div class="virtual-key" data-key="left">←</div>
584
+ <div class="virtual-key" data-key="right">→</div>
585
+ <div class="virtual-key" data-key="enter">Enter</div>
586
+ <div class="virtual-key" data-key="tab">Tab</div>
587
+ <div class="virtual-key" data-key="esc">Esc</div>
588
+ <div class="virtual-key" data-key="ctrlc">Ctrl+C</div>
589
+ <div class="virtual-key scroll-key" data-scroll="-5">⇡ 滚动</div>
590
+ <div class="virtual-key scroll-key" data-scroll="5">⇣ 滚动</div>
154
591
  </div>
155
592
  </div>
156
593
  </div>
@@ -197,6 +634,59 @@
197
634
  var momentumRaf = null;
198
635
  var velocitySamples = [];
199
636
 
637
+ // 未发送消息缓存
638
+ var currentInputBuffer = '';
639
+ var CACHE_KEY = 'claude_opencode_input_cache';
640
+ var cacheRestored = false; // 防止重复恢复
641
+
642
+ // 会话历史相关
643
+ var sessions = [];
644
+ var currentSessionId = null;
645
+ var currentSessionData = null;
646
+ var historyBarVisible = false;
647
+
648
+ function saveInputCache() {
649
+ if (currentInputBuffer) {
650
+ localStorage.setItem(CACHE_KEY, currentInputBuffer);
651
+ console.log('[cache] saved:', currentInputBuffer);
652
+ } else {
653
+ localStorage.removeItem(CACHE_KEY);
654
+ }
655
+ }
656
+
657
+ function loadInputCache() {
658
+ // 确保只恢复一次
659
+ if (cacheRestored) {
660
+ console.log('[cache] already restored, skipping');
661
+ return;
662
+ }
663
+
664
+ var cached = localStorage.getItem(CACHE_KEY);
665
+ if (cached && ws && ws.readyState === 1) {
666
+ console.log('[cache] restoring:', cached);
667
+ // 立即清除缓存,防止重复调用
668
+ localStorage.removeItem(CACHE_KEY);
669
+ cacheRestored = true;
670
+
671
+ // 将缓存的输入发送到终端
672
+ ws.send(JSON.stringify({ type: 'input', data: cached }));
673
+ // 重要:恢复后清空 currentInputBuffer,防止再次保存
674
+ currentInputBuffer = '';
675
+ } else {
676
+ console.log('[cache] no cache to restore');
677
+ }
678
+ }
679
+
680
+ function clearInputCache() {
681
+ currentInputBuffer = '';
682
+ localStorage.removeItem(CACHE_KEY);
683
+ console.log('[cache] cleared');
684
+ }
685
+
686
+ function getCellDims() {
687
+ return term._core && term._core._renderService && term._core._renderService.dimensions && term._core._renderService.dimensions.css && term._core._renderService.dimensions.css.cell;
688
+ }
689
+
200
690
  // 参考 cc-viewer 的 App.jsx 行 76-94: iOS visualViewport 处理
201
691
  // iOS 虚拟键盘弹出时,Safari 会滚动整个文档将页面上推,
202
692
  // 导致导航栏消失在视口之外。通过 visualViewport 的 resize + scroll
@@ -218,10 +708,6 @@
218
708
  onVVChange();
219
709
  }
220
710
 
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
711
  // 参考 cc-viewer 的 TerminalPanel.jsx 行 410-447: 移动端固定尺寸计算
226
712
  function mobileFixedResize() {
227
713
  if (!term) return;
@@ -302,109 +788,187 @@
302
788
  pixelAccum = 0;
303
789
  }
304
790
 
305
- // 参考 cc-viewer TerminalPanel.jsx 172-309: 移动端触摸滚动完整实现
791
+ // OpenCode 模式触摸滚动实现 - 使用 xterm.js scrollLines API
792
+ var touchScreen = null;
793
+ var touchEventsBound = false;
794
+
795
+ function getLineHeight() {
796
+ var cellDims = getCellDims();
797
+ var height = (cellDims && cellDims.height) || 15;
798
+ console.log('[scroll] lineHeight:', height);
799
+ return height;
800
+ }
801
+
802
+ function stopMomentum() {
803
+ if (momentumRaf) {
804
+ cancelAnimationFrame(momentumRaf);
805
+ momentumRaf = null;
806
+ }
807
+ if (scrollRaf) {
808
+ cancelAnimationFrame(scrollRaf);
809
+ scrollRaf = null;
810
+ }
811
+ pendingDy = 0;
812
+ pixelAccum = 0;
813
+ }
814
+
306
815
  function flushScroll() {
307
816
  scrollRaf = null;
308
817
  if (pendingDy === 0) return;
818
+
309
819
  pixelAccum += pendingDy;
310
820
  pendingDy = 0;
311
- var cellDims = getCellDims();
312
- var lh = (cellDims && cellDims.height) || 15;
821
+
822
+ var lh = getLineHeight();
313
823
  var lines = Math.trunc(pixelAccum / lh);
824
+
314
825
  if (lines !== 0) {
826
+ console.log('[scroll] scrollLines:', lines, 'pixelAccum:', pixelAccum, 'lineHeight:', lh);
315
827
  term.scrollLines(lines);
316
828
  pixelAccum -= lines * lh;
317
829
  }
318
830
  }
319
831
 
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 });
832
+ function handleTouchStart(e) {
833
+ console.log('[scroll] touchstart');
834
+ stopMomentum();
835
+ if (e.touches.length !== 1) return;
836
+ lastY = e.touches[0].clientY;
837
+ lastTime = performance.now();
838
+ velocitySamples = [];
839
+ }
332
840
 
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 });
841
+ function handleTouchMove(e) {
842
+ if (e.touches.length !== 1) return;
843
+ var y = e.touches[0].clientY;
844
+ var now = performance.now();
845
+ var dt = now - lastTime;
846
+ var dy = lastY - y;
351
847
 
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
- }
848
+ if (dt > 0) {
849
+ var v = dy / dt * 16;
850
+ velocitySamples.push({ v: v, t: now });
851
+ while (velocitySamples.length > 0 && now - velocitySamples[0].t > 100) {
852
+ velocitySamples.shift();
853
+ }
854
+ }
366
855
 
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;
856
+ pendingDy += dy;
857
+ console.log('[scroll] touchmove dy:', dy, 'pendingDy:', pendingDy);
858
+
859
+ if (!scrollRaf) scrollRaf = requestAnimationFrame(flushScroll);
860
+ lastY = y;
861
+ lastTime = now;
862
+ }
863
+
864
+ function handleTouchEnd() {
865
+ console.log('[scroll] touchend');
866
+
867
+ if (scrollRaf) {
868
+ cancelAnimationFrame(scrollRaf);
869
+ scrollRaf = null;
870
+ }
871
+
872
+ if (pendingDy !== 0) {
873
+ pixelAccum += pendingDy;
874
+ pendingDy = 0;
875
+ var lh = getLineHeight();
876
+ var lines = Math.trunc(pixelAccum / lh);
877
+ if (lines !== 0) {
878
+ console.log('[scroll] final scrollLines:', lines);
879
+ term.scrollLines(lines);
880
+ }
881
+ pixelAccum = 0;
882
+ }
883
+
884
+ var velocity = 0;
885
+ if (velocitySamples.length >= 2) {
886
+ var totalWeight = 0;
887
+ var weightedV = 0;
888
+ var latest = velocitySamples[velocitySamples.length - 1].t;
889
+ for (var i = 0; i < velocitySamples.length; i++) {
890
+ var s = velocitySamples[i];
891
+ var w = Math.max(0, 1 - (latest - s.t) / 100);
892
+ weightedV += s.v * w;
893
+ totalWeight += w;
894
+ }
895
+ velocity = totalWeight > 0 ? weightedV / totalWeight : 0;
896
+ }
897
+ velocitySamples = [];
898
+
899
+ console.log('[scroll] velocity:', velocity);
900
+ if (Math.abs(velocity) < 0.5) return;
901
+
902
+ var friction = 0.95;
903
+ var mAccum = 0;
904
+ var tick = function() {
905
+ if (Math.abs(velocity) < 0.3) {
906
+ var lh = getLineHeight();
907
+ var rest = Math.round(mAccum / lh);
908
+ if (rest !== 0) {
909
+ console.log('[scroll] momentum final:', rest);
910
+ term.scrollLines(rest);
379
911
  }
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);
912
+ momentumRaf = null;
913
+ return;
914
+ }
915
+ mAccum += velocity;
916
+ var lh = getLineHeight();
917
+ var lines = Math.trunc(mAccum / lh);
918
+ if (lines !== 0) {
919
+ term.scrollLines(lines);
920
+ mAccum -= lines * lh;
921
+ }
922
+ velocity *= friction;
923
+ momentumRaf = requestAnimationFrame(tick);
924
+ };
925
+ momentumRaf = requestAnimationFrame(tick);
926
+ }
927
+
928
+ function unbindTouchScroll() {
929
+ if (touchScreen && touchEventsBound) {
930
+ console.log('[scroll] unbinding touch events');
931
+ touchScreen.removeEventListener('touchstart', handleTouchStart);
932
+ touchScreen.removeEventListener('touchmove', handleTouchMove);
933
+ touchScreen.removeEventListener('touchend', handleTouchEnd);
934
+ touchEventsBound = false;
935
+ touchScreen = null;
936
+ }
937
+ }
938
+
939
+ function setupMobileTouchScroll() {
940
+ // 先解绑旧的
941
+ unbindTouchScroll();
942
+
943
+ // 只在 opencode 模式下绑定
944
+ if (currentMode !== 'opencode') {
945
+ console.log('[scroll] Not opencode mode, skip binding');
946
+ return;
947
+ }
948
+
949
+ var screen = terminalEl.querySelector('.xterm-screen');
950
+ if (!screen) {
951
+ console.log('[scroll] .xterm-screen not found, retrying...');
952
+ setTimeout(setupMobileTouchScroll, 50);
953
+ return;
954
+ }
955
+
956
+ touchScreen = screen;
957
+ screen.addEventListener('touchstart', handleTouchStart, { passive: true });
958
+ screen.addEventListener('touchmove', handleTouchMove, { passive: true });
959
+ screen.addEventListener('touchend', handleTouchEnd, { passive: true });
960
+ touchEventsBound = true;
961
+ console.log('[scroll] Touch events bound to .xterm-screen (opencode mode)');
962
+ }
963
+
964
+ function rebindTouchScroll() {
965
+ if (isMobile) {
966
+ setTimeout(setupMobileTouchScroll, 100);
967
+ }
968
+ }
969
+
970
+ if (isMobile) {
971
+ setupMobileTouchScroll();
408
972
  }
409
973
 
410
974
  function startTransition() {
@@ -432,6 +996,42 @@
432
996
  // 设置初始选中项
433
997
  modeSelect.value = currentMode;
434
998
 
999
+ // 只绑定一次 term.onData,避免重连时重复绑定
1000
+ term.onData(function(d) {
1001
+ if (ws && ws.readyState === 1) {
1002
+ ws.send(JSON.stringify({ type: 'input', data: d }));
1003
+
1004
+ // 缓存管理:跟踪当前行的输入(仅在 opencode 模式且未在恢复中)
1005
+ if (currentMode === 'opencode' && cacheRestored) {
1006
+ if (d === '\r' || d === '\n' || d === '\r\n') {
1007
+ // 回车:命令已发送,清除缓存
1008
+ clearInputCache();
1009
+ } else if (d === '\x7f' || d === '\b') {
1010
+ // 退格:删除最后一个字符
1011
+ if (currentInputBuffer.length > 0) {
1012
+ currentInputBuffer = currentInputBuffer.slice(0, -1);
1013
+ saveInputCache();
1014
+ }
1015
+ } else if (d === '\x03') {
1016
+ // Ctrl+C:中断,清除缓存
1017
+ clearInputCache();
1018
+ } else if (d === '\x15') {
1019
+ // Ctrl+U:清空整行
1020
+ clearInputCache();
1021
+ } else if (d.length === 1 && d.charCodeAt(0) >= 32 && d.charCodeAt(0) < 127) {
1022
+ // 可打印 ASCII 字符:添加到缓冲区
1023
+ currentInputBuffer += d;
1024
+ saveInputCache();
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ // 点击终端输入时,滚动到底部
1030
+ setTimeout(function() {
1031
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
1032
+ }, 50);
1033
+ });
1034
+
435
1035
  function connect() {
436
1036
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
437
1037
  ws = new WebSocket(proto + '//' + location.host + '/ws');
@@ -449,9 +1049,14 @@
449
1049
  try {
450
1050
  var msg = JSON.parse(e.data);
451
1051
  if (msg.type === 'data') throttledWrite(msg.data);
452
- else if (msg.type === 'exit') throttledWrite('\r\n[进程已退出: ' + msg.exitCode + ']\r\n');
1052
+ else if (msg.type === 'exit') {
1053
+ throttledWrite('\r\n\x1b[33m[进程已退出: ' + msg.exitCode + ']\x1b[0m\r\n');
1054
+ throttledWrite('\x1b[90m按 Enter 键重新启动 ' + currentMode + '...\x1b[0m\r\n');
1055
+ }
453
1056
  else if (msg.type === 'mode') {
454
1057
  endTransition(msg.mode);
1058
+ // 模式切换完成后,重新绑定触摸事件
1059
+ rebindTouchScroll();
455
1060
  }
456
1061
  else if (msg.type === 'switching') {
457
1062
  // 服务端开始切换,前端清屏
@@ -465,16 +1070,28 @@
465
1070
  modeIndicator.textContent = msg.mode === 'claude' ? 'Claude' : 'OpenCode';
466
1071
  }
467
1072
  }
1073
+ else if (msg.type === 'restored') {
1074
+ // 会话恢复成功
1075
+ term.write('\x1b[32m✓ 会话已恢复: ' + msg.sessionId + '\x1b[0m\r\n');
1076
+ term.write('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\r\n');
1077
+ term.write('\r\n');
1078
+ }
1079
+ else if (msg.type === 'restore-error') {
1080
+ // 恢复失败
1081
+ term.write('\x1b[31m✗ 恢复失败: ' + msg.error + '\x1b[0m\r\n');
1082
+ }
468
1083
  } catch(err) {}
469
1084
  };
470
1085
 
471
- term.onData(function(d) {
472
- if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'input', data: d }));
473
- // 点击终端输入时,滚动到底部
474
- setTimeout(function() {
475
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
476
- }, 50);
477
- });
1086
+ // 页面加载后尝试恢复缓存的输入(仅在 opencode 模式)
1087
+ setTimeout(function() {
1088
+ if (currentMode === 'opencode') {
1089
+ loadInputCache();
1090
+ } else {
1091
+ // 非 opencode 模式,标记为已恢复,避免后续缓存逻辑干扰
1092
+ cacheRestored = true;
1093
+ }
1094
+ }, 800);
478
1095
  }
479
1096
 
480
1097
  window.addEventListener('resize', resize);
@@ -484,6 +1101,20 @@
484
1101
  });
485
1102
  }
486
1103
 
1104
+ // 页面卸载前保存输入缓存
1105
+ window.addEventListener('beforeunload', function() {
1106
+ if (currentInputBuffer) {
1107
+ saveInputCache();
1108
+ }
1109
+ });
1110
+
1111
+ // 页面可见性变化时保存缓存
1112
+ document.addEventListener('visibilitychange', function() {
1113
+ if (document.hidden && currentInputBuffer) {
1114
+ saveInputCache();
1115
+ }
1116
+ });
1117
+
487
1118
  // 虚拟按键映射表
488
1119
  var KEY_MAP = {
489
1120
  'up': '\x1b[A',
@@ -503,43 +1134,401 @@
503
1134
  }
504
1135
  }
505
1136
 
1137
+ function scrollTerminal(lines) {
1138
+ if (term) {
1139
+ term.scrollLines(lines);
1140
+ console.log('[scroll] scrolled by:', lines);
1141
+ }
1142
+ }
1143
+
506
1144
  // 参考 cc-viewer 的 TerminalPanel.jsx 行 519-546: 虚拟按键触摸处理
507
- var keybar = document.getElementById('virtual-keybar');
1145
+ // 每个按键独立绑定事件,不在容器上绑定
508
1146
  var vkStartX = 0, vkStartY = 0, vkMoved = false, vkTarget = null;
1147
+ var scrollInterval = null; // 长按滚动定时器
509
1148
 
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
- });
1149
+ function setupVirtualKeyEvents() {
1150
+ var keys = document.querySelectorAll('.virtual-key');
1151
+ keys.forEach(function(key) {
1152
+ // 防止元素获得焦点
1153
+ key.setAttribute('tabindex', '-1');
1154
+
1155
+ // 移动端触摸事件
1156
+ key.addEventListener('touchstart', function(e) {
1157
+ var touch = e.touches[0];
1158
+ vkStartX = touch.clientX;
1159
+ vkStartY = touch.clientY;
1160
+ vkMoved = false;
1161
+ vkTarget = e.currentTarget;
1162
+ vkTarget.style.background = '#333';
1163
+
1164
+ // 如果是滚动按钮,阻止默认行为(防止键盘弹出)并启动持续滚动
1165
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
1166
+ if (scrollLines) {
1167
+ e.preventDefault(); // 关键:阻止默认行为,防止键盘弹出
1168
+ scrollTerminal(parseInt(scrollLines, 10));
1169
+ scrollInterval = setInterval(function() {
1170
+ scrollTerminal(parseInt(scrollLines, 10));
1171
+ }, 100);
1172
+ }
1173
+ }, { passive: false }); // 必须是 false 才能调用 preventDefault()
1174
+
1175
+ key.addEventListener('touchmove', function(e) {
1176
+ if (vkMoved) return;
1177
+ var touch = e.touches[0];
1178
+ var dx = touch.clientX - vkStartX;
1179
+ var dy = touch.clientY - vkStartY;
1180
+ if (dx * dx + dy * dy > 64) { // 8px 阈值
1181
+ vkMoved = true;
1182
+ if (vkTarget) {
1183
+ vkTarget.style.background = '';
1184
+ }
1185
+ }
1186
+ }, { passive: true });
1187
+
1188
+ key.addEventListener('touchend', function(e) {
1189
+ // 清除滚动定时器
1190
+ if (scrollInterval) {
1191
+ clearInterval(scrollInterval);
1192
+ scrollInterval = null;
1193
+ }
1194
+
1195
+ if (vkTarget) {
1196
+ vkTarget.style.background = '';
1197
+ vkTarget = null;
1198
+ }
1199
+
1200
+ // 如果没有移动,触发按键功能并阻止默认行为
1201
+ if (!vkMoved) {
1202
+ e.preventDefault(); // 阻止默认行为
1203
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
1204
+ if (!scrollLines) {
1205
+ // 非滚动按钮才触发按键
1206
+ var keyName = e.currentTarget.getAttribute('data-key');
1207
+ sendKey(keyName);
1208
+ }
1209
+ }
1210
+ }, { passive: false });
1211
+
1212
+ // PC端点击支持
1213
+ key.addEventListener('click', function(e) {
1214
+ e.preventDefault();
1215
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
1216
+ if (scrollLines) {
1217
+ scrollTerminal(parseInt(scrollLines, 10));
1218
+ } else {
1219
+ var keyName = e.currentTarget.getAttribute('data-key');
1220
+ sendKey(keyName);
1221
+ }
1222
+ });
1223
+
1224
+ // PC端鼠标按下支持(长按滚动)
1225
+ key.addEventListener('mousedown', function(e) {
1226
+ e.preventDefault();
1227
+ var scrollLines = e.currentTarget.getAttribute('data-scroll');
1228
+ if (scrollLines) {
1229
+ scrollTerminal(parseInt(scrollLines, 10));
1230
+ scrollInterval = setInterval(function() {
1231
+ scrollTerminal(parseInt(scrollLines, 10));
1232
+ }, 100);
1233
+ }
1234
+ });
1235
+
1236
+ key.addEventListener('mouseup', function(e) {
1237
+ if (scrollInterval) {
1238
+ clearInterval(scrollInterval);
1239
+ scrollInterval = null;
1240
+ }
1241
+ });
1242
+
1243
+ key.addEventListener('mouseleave', function(e) {
1244
+ if (scrollInterval) {
1245
+ clearInterval(scrollInterval);
1246
+ scrollInterval = null;
1247
+ }
1248
+ });
1249
+ });
1250
+ }
1251
+
1252
+ // 会话历史功能
1253
+ function formatTime(timestamp) {
1254
+ var date = new Date(timestamp);
1255
+ var now = new Date();
1256
+ var diff = now - date;
1257
+ var minutes = Math.floor(diff / 60000);
1258
+ var hours = Math.floor(diff / 3600000);
1259
+ var days = Math.floor(diff / 86400000);
1260
+
1261
+ if (minutes < 1) return '刚刚';
1262
+ if (minutes < 60) return minutes + '分钟前';
1263
+ if (hours < 24) return hours + '小时前';
1264
+ if (days < 7) return days + '天前';
1265
+
1266
+ return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
1267
+ }
1268
+
1269
+ function loadSessions() {
1270
+ var sessionList = document.getElementById('session-list');
1271
+ sessionList.innerHTML = '<div class="session-loading">加载历史会话...</div>';
1272
+
1273
+ fetch('/api/sessions')
1274
+ .then(function(response) { return response.json(); })
1275
+ .then(function(data) {
1276
+ sessions = data;
1277
+ renderSessions();
1278
+ })
1279
+ .catch(function(err) {
1280
+ console.error('[sessions] 加载失败:', err);
1281
+ sessionList.innerHTML = '<div class="session-empty">无法加载历史会话</div>';
1282
+ });
1283
+ }
1284
+
1285
+ function renderSessions() {
1286
+ var sessionList = document.getElementById('session-list');
1287
+
1288
+ if (sessions.length === 0) {
1289
+ sessionList.innerHTML = '<div class="session-empty">暂无历史会话</div>';
1290
+ return;
1291
+ }
1292
+
1293
+ sessionList.innerHTML = '';
1294
+
1295
+ sessions.forEach(function(session) {
1296
+ var item = document.createElement('div');
1297
+ item.className = 'session-item';
1298
+ if (session.id === currentSessionId) {
1299
+ item.classList.add('active');
1300
+ }
1301
+
1302
+ var icon = document.createElement('div');
1303
+ icon.className = 'session-icon';
1304
+
1305
+ var info = document.createElement('div');
1306
+ info.className = 'session-info';
1307
+
1308
+ var title = document.createElement('div');
1309
+ title.className = 'session-title';
1310
+ // 使用预览文本,如果没有预览则使用标题
1311
+ title.textContent = session.preview || session.title;
1312
+
1313
+ var meta = document.createElement('div');
1314
+ meta.className = 'session-meta';
1315
+
1316
+ var time = document.createElement('span');
1317
+ time.className = 'session-time';
1318
+ time.textContent = formatTime(session.time_updated);
1319
+
1320
+ var dir = document.createElement('span');
1321
+ dir.className = 'session-dir';
1322
+ dir.textContent = session.directory.replace(/^\/Users\/[^\/]+/, '~');
1323
+
1324
+ meta.appendChild(time);
1325
+ meta.appendChild(dir);
1326
+ info.appendChild(title);
1327
+ info.appendChild(meta);
1328
+
1329
+ item.appendChild(icon);
1330
+ item.appendChild(info);
1331
+
1332
+ item.addEventListener('click', function() {
1333
+ loadSession(session);
1334
+ });
1335
+
1336
+ sessionList.appendChild(item);
1337
+ });
1338
+ }
1339
+
1340
+ function showSessionDetail() {
1341
+ document.getElementById('session-list-view').classList.add('hidden');
1342
+ document.getElementById('session-detail-view').classList.add('visible');
1343
+ }
1344
+
1345
+ function showSessionList() {
1346
+ document.getElementById('session-list-view').classList.remove('hidden');
1347
+ document.getElementById('session-detail-view').classList.remove('visible');
1348
+ }
1349
+
1350
+ function loadSession(session) {
1351
+ console.log('[session] 加载会话:', session.title);
1352
+
1353
+ // 保存当前会话数据
1354
+ currentSessionData = session;
1355
+
1356
+ // 切换到详情视图
1357
+ showSessionDetail();
1358
+
1359
+ // 更新详情页标题和元信息
1360
+ document.getElementById('session-detail-title').textContent = session.preview || session.title;
1361
+ document.getElementById('session-detail-time').textContent = formatTime(session.time_updated);
1362
+ document.getElementById('session-detail-dir').textContent = session.directory.replace(/^\/Users\/[^\/]+/, '~');
1363
+
1364
+ var contentDiv = document.getElementById('session-detail-content');
1365
+ contentDiv.innerHTML = '<div class="message-empty">加载消息中...</div>';
1366
+
1367
+ // 获取会话的所有消息
1368
+ fetch('/api/session/' + session.id)
1369
+ .then(function(response) { return response.json(); })
1370
+ .then(function(messages) {
1371
+ console.log('[session] 收到', messages.length, '条消息');
1372
+
1373
+ if (messages.length === 0) {
1374
+ contentDiv.innerHTML = '<div class="message-empty">该会话暂无消息</div>';
1375
+ return;
1376
+ }
1377
+
1378
+ // 渲染消息列表
1379
+ var html = '';
1380
+ messages.forEach(function(msg) {
1381
+ console.log('[render] 消息:', msg);
1382
+
1383
+ var roleClass = msg.role === 'assistant' ? 'message-assistant' : 'message-user';
1384
+ var avatar = msg.role === 'user' ? '👤' : '🤖';
1385
+ var roleName = msg.role === 'user' ? 'User' : 'Assistant';
520
1386
 
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;
1387
+ html += '<div class="message-item ' + roleClass + '">';
1388
+ html += '<div class="message-avatar">' + avatar + '</div>';
1389
+ html += '<div class="message-content">';
1390
+ html += '<div class="message-header">';
1391
+ html += '<div class="message-role">' + roleName + '</div>';
1392
+ html += '</div>';
1393
+
1394
+ // 显示推理过程(仅 assistant)
1395
+ if (msg.reasoning) {
1396
+ html += '<div class="message-text" style="background: #1a1a0a; border-color: #333;">';
1397
+ html += '<div style="color: #f0ad4e; font-size: 11px; font-weight: 600; margin-bottom: 6px;">💭 思考过程</div>';
1398
+ html += '<div style="color: #bbb;">' + escapeHtml(msg.reasoning) + '</div>';
1399
+ html += '</div>';
1400
+ }
1401
+
1402
+ // 显示文本内容
1403
+ if (msg.text) {
1404
+ html += '<div class="message-text">' + escapeHtml(msg.text) + '</div>';
1405
+ } else if (!msg.reasoning && !msg.toolCalls && !msg.toolResults) {
1406
+ html += '<div class="message-text" style="color: #666; font-style: italic;">(无文本内容)</div>';
1407
+ }
1408
+
1409
+ // 显示工具调用
1410
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
1411
+ msg.toolCalls.forEach(function(tool) {
1412
+ html += '<div class="message-tool-call">';
1413
+ html += '<div class="message-tool-name">🔧 工具调用: ' + escapeHtml(tool.name || '未知工具') + '</div>';
1414
+ html += '</div>';
1415
+ });
1416
+ }
1417
+
1418
+ // 显示工具结果
1419
+ if (msg.toolResults && msg.toolResults.length > 0) {
1420
+ msg.toolResults.forEach(function(result) {
1421
+ if (result.text) {
1422
+ var resultText = result.text.length > 200 ? result.text.substring(0, 200) + '...' : result.text;
1423
+ html += '<div class="message-tool-call">';
1424
+ html += '<div class="message-tool-name">📝 工具结果</div>';
1425
+ html += '<div class="message-tool-result">' + escapeHtml(resultText) + '</div>';
1426
+ html += '</div>';
1427
+ }
1428
+ });
1429
+ }
1430
+
1431
+ html += '</div>';
1432
+ html += '</div>';
1433
+ });
1434
+
1435
+ contentDiv.innerHTML = html;
1436
+
1437
+ // 滚动到底部
1438
+ contentDiv.scrollTop = contentDiv.scrollHeight;
1439
+ })
1440
+ .catch(function(err) {
1441
+ console.error('[session] 加载消息失败:', err);
1442
+ contentDiv.innerHTML = '<div class="message-empty">加载失败: ' + escapeHtml(err.message) + '</div>';
1443
+ });
1444
+ }
1445
+
1446
+ function escapeHtml(text) {
1447
+ var div = document.createElement('div');
1448
+ div.textContent = text;
1449
+ return div.innerHTML;
1450
+ }
1451
+
1452
+ function toggleHistoryBar() {
1453
+ historyBarVisible = !historyBarVisible;
1454
+ var historyBar = document.getElementById('session-history-bar');
1455
+
1456
+ if (historyBarVisible) {
1457
+ historyBar.classList.add('visible');
1458
+ loadSessions();
1459
+ } else {
1460
+ historyBar.classList.remove('visible');
528
1461
  }
1462
+ }
1463
+
1464
+ // 绑定历史按钮
1465
+ document.getElementById('history-toggle').addEventListener('click', function() {
1466
+ toggleHistoryBar();
1467
+ });
1468
+
1469
+ // 刷新会话列表
1470
+ document.getElementById('refresh-sessions').addEventListener('click', function(e) {
1471
+ e.stopPropagation();
1472
+ loadSessions();
1473
+ });
1474
+
1475
+ // 关闭历史栏
1476
+ document.getElementById('close-history').addEventListener('click', function(e) {
1477
+ e.stopPropagation();
1478
+ toggleHistoryBar();
1479
+ });
1480
+
1481
+ // 返回到会话列表
1482
+ document.getElementById('back-to-list').addEventListener('click', function(e) {
1483
+ e.stopPropagation();
1484
+ showSessionList();
529
1485
  });
530
1486
 
531
- keybar.addEventListener('touchend', function(e) {
532
- e.preventDefault();
533
- if (vkTarget) {
534
- vkTarget.style.background = '';
535
- vkTarget = null;
1487
+ // 恢复会话
1488
+ document.getElementById('restore-session').addEventListener('click', function(e) {
1489
+ e.stopPropagation();
1490
+ if (!currentSessionData) {
1491
+ console.error('[restore] 没有当前会话数据');
1492
+ return;
536
1493
  }
537
- if (!vkMoved && e.target.classList.contains('virtual-key')) {
538
- var keyName = e.target.getAttribute('data-key');
539
- sendKey(keyName);
1494
+
1495
+ console.log('[restore] 恢复会话:', currentSessionData.id);
1496
+
1497
+ // 关闭历史栏
1498
+ toggleHistoryBar();
1499
+
1500
+ // 清空终端
1501
+ term.clear();
1502
+
1503
+ // 显示正在恢复的提示
1504
+ term.write('\x1b[1;36m╔══════════════════════════════════════════════════════════════╗\x1b[0m\r\n');
1505
+ term.write('\x1b[1;36m║ \x1b[1;37m正在恢复会话... \x1b[1;36m║\x1b[0m\r\n');
1506
+ term.write('\x1b[1;36m╚══════════════════════════════════════════════════════════════╝\x1b[0m\r\n');
1507
+ term.write('\r\n');
1508
+
1509
+ // 发送恢复会话的请求到服务端
1510
+ if (ws && ws.readyState === 1) {
1511
+ if (currentMode !== 'opencode') {
1512
+ term.write('\x1b[31m错误: 请先切换到 OpenCode 模式\x1b[0m\r\n');
1513
+ return;
1514
+ }
1515
+
1516
+ term.write('\x1b[33m正在重启 OpenCode 并恢复会话: ' + currentSessionData.id + '\x1b[0m\r\n');
1517
+ term.write('\r\n');
1518
+
1519
+ // 发送恢复请求
1520
+ ws.send(JSON.stringify({
1521
+ type: 'restore',
1522
+ sessionId: currentSessionData.id
1523
+ }));
1524
+ } else {
1525
+ term.write('\x1b[31m错误: WebSocket 未连接\x1b[0m\r\n');
540
1526
  }
541
1527
  });
542
1528
 
1529
+ // 初始化虚拟按键事件
1530
+ setupVirtualKeyEvents();
1531
+
543
1532
  connect();
544
1533
  setTimeout(resize, 100);
545
1534
  })();