@xcanwin/manyoyo 4.0.2 → 4.0.6

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.
@@ -39,11 +39,26 @@
39
39
  sessions: [],
40
40
  active: '',
41
41
  messages: [],
42
+ messageRenderKeys: [],
43
+ mode: 'command',
42
44
  sending: false,
43
45
  loadingSessions: false,
44
46
  loadingMessages: false,
45
47
  mobileSidebarOpen: false,
46
- mobileActionsOpen: false
48
+ mobileActionsOpen: false,
49
+ terminal: {
50
+ term: null,
51
+ fitAddon: null,
52
+ socket: null,
53
+ connected: false,
54
+ connecting: false,
55
+ status: '未连接',
56
+ sessionName: '',
57
+ terminalReady: false,
58
+ fitTimer: null,
59
+ lastSentCols: 0,
60
+ lastSentRows: 0
61
+ }
47
62
  };
48
63
 
49
64
  const sidebarNode = document.querySelector('.sidebar');
@@ -56,7 +71,14 @@
56
71
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
57
72
  const activeTitle = document.getElementById('activeTitle');
58
73
  const activeMeta = document.getElementById('activeMeta');
74
+ const modeCommandBtn = document.getElementById('modeCommandBtn');
75
+ const modeTerminalBtn = document.getElementById('modeTerminalBtn');
59
76
  const messagesNode = document.getElementById('messages');
77
+ const terminalPanel = document.getElementById('terminalPanel');
78
+ const terminalConnectBtn = document.getElementById('terminalConnectBtn');
79
+ const terminalDisconnectBtn = document.getElementById('terminalDisconnectBtn');
80
+ const terminalStatus = document.getElementById('terminalStatus');
81
+ const terminalScreen = document.getElementById('terminalScreen');
60
82
  const newSessionForm = document.getElementById('newSessionForm');
61
83
  const newSessionName = document.getElementById('newSessionName');
62
84
  const createSessionBtn = newSessionForm.querySelector('button[type="submit"]');
@@ -69,6 +91,11 @@
69
91
  const removeAllBtn = document.getElementById('removeAllBtn');
70
92
  const MOBILE_LAYOUT_MEDIA = window.matchMedia('(max-width: 980px)');
71
93
  const MOBILE_COMPACT_MEDIA = window.matchMedia('(max-width: 640px)');
94
+ const TERMINAL_FIT_DEBOUNCE_MS = 60;
95
+ const TERMINAL_MIN_COLS = 40;
96
+ const TERMINAL_MIN_ROWS = 12;
97
+ const TERMINAL_DEFAULT_COLS = 120;
98
+ const TERMINAL_DEFAULT_ROWS = 36;
72
99
 
73
100
  function roleName(role) {
74
101
  if (role === 'user') return '你';
@@ -144,6 +171,278 @@
144
171
  return parts.join(' · ');
145
172
  }
146
173
 
174
+ function writeTerminalLine(text) {
175
+ if (!state.terminal.term) return;
176
+ state.terminal.term.writeln(text);
177
+ }
178
+
179
+ function renderTerminalIntro() {
180
+ if (!state.terminal.term) return;
181
+ state.terminal.term.reset();
182
+ writeTerminalLine('MANYOYO Interactive Terminal');
183
+ writeTerminalLine(state.active ? ('当前会话: ' + state.active) : '当前会话: 未选择');
184
+ writeTerminalLine('点击“连接终端”后可运行 codex / claude 等交互式 agent。');
185
+ writeTerminalLine('');
186
+ }
187
+
188
+ function resolveFitAddonCtor() {
189
+ if (window.FitAddon && typeof window.FitAddon.FitAddon === 'function') {
190
+ return window.FitAddon.FitAddon;
191
+ }
192
+ if (typeof window.FitAddon === 'function') {
193
+ return window.FitAddon;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function normalizeTerminalSize(cols, rows) {
199
+ const parsedCols = Number.parseInt(cols, 10);
200
+ const parsedRows = Number.parseInt(rows, 10);
201
+ const safeCols = Number.isFinite(parsedCols) && parsedCols > 0 ? parsedCols : TERMINAL_DEFAULT_COLS;
202
+ const safeRows = Number.isFinite(parsedRows) && parsedRows > 0 ? parsedRows : TERMINAL_DEFAULT_ROWS;
203
+ return {
204
+ cols: Math.max(TERMINAL_MIN_COLS, safeCols),
205
+ rows: Math.max(TERMINAL_MIN_ROWS, safeRows)
206
+ };
207
+ }
208
+
209
+ function notifyTerminalResize(force) {
210
+ if (!state.terminal.term) return;
211
+ if (!state.terminal.socket || state.terminal.socket.readyState !== window.WebSocket.OPEN) return;
212
+ const size = normalizeTerminalSize(
213
+ state.terminal.term.cols,
214
+ state.terminal.term.rows
215
+ );
216
+ const cols = size.cols;
217
+ const rows = size.rows;
218
+ if (!force && cols === state.terminal.lastSentCols && rows === state.terminal.lastSentRows) {
219
+ return;
220
+ }
221
+ state.terminal.lastSentCols = cols;
222
+ state.terminal.lastSentRows = rows;
223
+ state.terminal.socket.send(JSON.stringify({
224
+ type: 'resize',
225
+ cols: cols,
226
+ rows: rows
227
+ }));
228
+ }
229
+
230
+ function fitTerminalNow(forceNotify) {
231
+ if (!state.terminal.term || !state.terminal.fitAddon) {
232
+ return;
233
+ }
234
+ if (!terminalScreen || terminalScreen.clientWidth <= 0 || terminalScreen.clientHeight <= 0) {
235
+ return;
236
+ }
237
+ try {
238
+ state.terminal.fitAddon.fit();
239
+ } catch (e) {
240
+ return;
241
+ }
242
+ notifyTerminalResize(Boolean(forceNotify));
243
+ }
244
+
245
+ function scheduleTerminalFit(forceNotify) {
246
+ if (!state.terminal.term || !state.terminal.fitAddon) {
247
+ return;
248
+ }
249
+ if (state.terminal.fitTimer) {
250
+ window.clearTimeout(state.terminal.fitTimer);
251
+ state.terminal.fitTimer = null;
252
+ }
253
+ state.terminal.fitTimer = window.setTimeout(function () {
254
+ state.terminal.fitTimer = null;
255
+ fitTerminalNow(forceNotify);
256
+ }, TERMINAL_FIT_DEBOUNCE_MS);
257
+ }
258
+
259
+ function ensureTerminalReady() {
260
+ if (state.terminal.terminalReady) {
261
+ return true;
262
+ }
263
+ if (!window.Terminal) {
264
+ state.terminal.status = '终端组件加载失败';
265
+ return false;
266
+ }
267
+ const FitAddonCtor = resolveFitAddonCtor();
268
+ if (!FitAddonCtor) {
269
+ state.terminal.status = '终端组件加载失败';
270
+ return false;
271
+ }
272
+ state.terminal.term = new window.Terminal({
273
+ cursorBlink: true,
274
+ convertEol: false,
275
+ fontFamily: '"IBM Plex Mono", "SFMono-Regular", Consolas, Menlo, monospace',
276
+ fontSize: 13,
277
+ scrollback: 5000,
278
+ theme: {
279
+ background: '#0c131a',
280
+ foreground: '#dde8f3',
281
+ cursor: '#6fe7b5'
282
+ }
283
+ });
284
+ state.terminal.fitAddon = new FitAddonCtor();
285
+ state.terminal.term.loadAddon(state.terminal.fitAddon);
286
+ state.terminal.term.open(terminalScreen);
287
+ scheduleTerminalFit(false);
288
+ state.terminal.term.onData(function (data) {
289
+ if (!data || !state.terminal.socket || state.terminal.socket.readyState !== window.WebSocket.OPEN) {
290
+ return;
291
+ }
292
+ state.terminal.socket.send(JSON.stringify({
293
+ type: 'input',
294
+ data: data
295
+ }));
296
+ });
297
+ state.terminal.term.onResize(function (size) {
298
+ if (!size || !size.cols || !size.rows) {
299
+ return;
300
+ }
301
+ notifyTerminalResize(false);
302
+ });
303
+ state.terminal.terminalReady = true;
304
+ renderTerminalIntro();
305
+ return true;
306
+ }
307
+
308
+ function buildTerminalWsUrl(sessionName) {
309
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
310
+ const url = new URL(
311
+ '/api/sessions/' + encodeURIComponent(sessionName) + '/terminal/ws',
312
+ protocol + '://' + window.location.host
313
+ );
314
+ const size = normalizeTerminalSize(
315
+ state.terminal.term ? state.terminal.term.cols : TERMINAL_DEFAULT_COLS,
316
+ state.terminal.term ? state.terminal.term.rows : TERMINAL_DEFAULT_ROWS
317
+ );
318
+ url.searchParams.set('cols', String(size.cols));
319
+ url.searchParams.set('rows', String(size.rows));
320
+ return url.toString();
321
+ }
322
+
323
+ function disconnectTerminal(reason, silent) {
324
+ const socket = state.terminal.socket;
325
+ state.terminal.socket = null;
326
+ state.terminal.connected = false;
327
+ state.terminal.connecting = false;
328
+ state.terminal.sessionName = '';
329
+ state.terminal.lastSentCols = 0;
330
+ state.terminal.lastSentRows = 0;
331
+ if (state.terminal.fitTimer) {
332
+ window.clearTimeout(state.terminal.fitTimer);
333
+ state.terminal.fitTimer = null;
334
+ }
335
+ if (socket && (socket.readyState === window.WebSocket.OPEN || socket.readyState === window.WebSocket.CONNECTING)) {
336
+ try {
337
+ socket.close();
338
+ } catch (e) {
339
+ // noop
340
+ }
341
+ }
342
+ if (typeof reason === 'string' && reason) {
343
+ state.terminal.status = reason;
344
+ if (!silent && state.terminal.term) {
345
+ writeTerminalLine('[system] ' + reason);
346
+ }
347
+ }
348
+ }
349
+
350
+ function connectTerminal() {
351
+ if (!state.active) {
352
+ return;
353
+ }
354
+ if (!ensureTerminalReady()) {
355
+ syncUi();
356
+ return;
357
+ }
358
+ if (state.terminal.connected && state.terminal.sessionName === state.active) {
359
+ return;
360
+ }
361
+ if (state.terminal.connected || state.terminal.connecting) {
362
+ disconnectTerminal('终端会话已重置', true);
363
+ }
364
+
365
+ const sessionName = state.active;
366
+ fitTerminalNow(false);
367
+ const socket = new window.WebSocket(buildTerminalWsUrl(sessionName));
368
+ state.terminal.socket = socket;
369
+ state.terminal.connecting = true;
370
+ state.terminal.connected = false;
371
+ state.terminal.status = '连接中...';
372
+ state.terminal.sessionName = sessionName;
373
+ state.terminal.lastSentCols = 0;
374
+ state.terminal.lastSentRows = 0;
375
+ writeTerminalLine('[system] 正在连接终端...');
376
+ syncUi();
377
+
378
+ socket.addEventListener('open', function () {
379
+ if (state.terminal.socket !== socket) {
380
+ return;
381
+ }
382
+ state.terminal.connecting = false;
383
+ state.terminal.connected = true;
384
+ state.terminal.status = '已连接';
385
+ if (state.terminal.term) {
386
+ state.terminal.term.focus();
387
+ scheduleTerminalFit(true);
388
+ }
389
+ syncUi();
390
+ });
391
+
392
+ socket.addEventListener('message', function (event) {
393
+ if (!state.terminal.term) return;
394
+ let payload = null;
395
+ try {
396
+ payload = JSON.parse(event.data);
397
+ } catch (e) {
398
+ payload = { type: 'output', data: String(event.data || '') };
399
+ }
400
+
401
+ if (!payload || typeof payload !== 'object') return;
402
+ if (payload.type === 'output' && typeof payload.data === 'string') {
403
+ state.terminal.term.write(payload.data);
404
+ return;
405
+ }
406
+ if (payload.type === 'status') {
407
+ if (payload.phase === 'ready') {
408
+ state.terminal.status = '已连接';
409
+ } else if (payload.phase === 'closed') {
410
+ state.terminal.status = '终端已关闭';
411
+ writeTerminalLine('[system] 终端会话已结束');
412
+ }
413
+ syncUi();
414
+ return;
415
+ }
416
+ if (payload.type === 'error' && typeof payload.error === 'string') {
417
+ state.terminal.status = '终端异常';
418
+ writeTerminalLine('[error] ' + payload.error);
419
+ syncUi();
420
+ }
421
+ });
422
+
423
+ socket.addEventListener('error', function () {
424
+ if (state.terminal.socket !== socket) {
425
+ return;
426
+ }
427
+ state.terminal.status = '终端连接异常';
428
+ syncUi();
429
+ });
430
+
431
+ socket.addEventListener('close', function () {
432
+ if (state.terminal.socket !== socket) {
433
+ return;
434
+ }
435
+ state.terminal.socket = null;
436
+ state.terminal.connecting = false;
437
+ state.terminal.connected = false;
438
+ state.terminal.sessionName = '';
439
+ if (state.terminal.status === '连接中...' || state.terminal.status === '已连接') {
440
+ state.terminal.status = '终端已断开';
441
+ }
442
+ syncUi();
443
+ });
444
+ }
445
+
147
446
  function isMobileLayout() {
148
447
  return MOBILE_LAYOUT_MEDIA.matches;
149
448
  }
@@ -196,19 +495,48 @@
196
495
  if (!state.active) {
197
496
  activeTitle.textContent = '未选择会话';
198
497
  activeMeta.textContent = '请选择左侧会话';
199
- commandInput.value = '';
498
+ if (state.mode === 'command') {
499
+ commandInput.value = '';
500
+ }
200
501
  } else {
201
502
  activeTitle.textContent = state.active;
202
503
  activeMeta.textContent = buildActiveMeta(getActiveSession());
203
504
  }
204
505
 
506
+ const commandMode = state.mode !== 'terminal';
507
+ document.body.classList.toggle('command-mode', commandMode);
508
+ document.body.classList.toggle('terminal-mode', !commandMode);
509
+ if (modeCommandBtn) {
510
+ modeCommandBtn.classList.toggle('is-active', commandMode);
511
+ modeCommandBtn.setAttribute('aria-pressed', commandMode ? 'true' : 'false');
512
+ }
513
+ if (modeTerminalBtn) {
514
+ modeTerminalBtn.classList.toggle('is-active', !commandMode);
515
+ modeTerminalBtn.setAttribute('aria-pressed', !commandMode ? 'true' : 'false');
516
+ }
517
+ if (terminalPanel) {
518
+ terminalPanel.hidden = commandMode;
519
+ }
520
+ if (!commandMode && state.terminal.terminalReady) {
521
+ scheduleTerminalFit(false);
522
+ }
523
+
205
524
  const busy = state.loadingSessions || state.loadingMessages || state.sending;
206
525
  refreshBtn.disabled = busy;
207
526
  removeBtn.disabled = !state.active || busy;
208
527
  removeAllBtn.disabled = !state.active || busy;
209
- sendBtn.disabled = !state.active || busy;
210
- commandInput.disabled = !state.active || state.sending;
528
+ sendBtn.disabled = !commandMode || !state.active || busy;
529
+ commandInput.disabled = !commandMode || !state.active || state.sending;
211
530
  createSessionBtn.disabled = state.loadingSessions || state.sending;
531
+ if (terminalConnectBtn) {
532
+ terminalConnectBtn.disabled = !state.active || busy || state.terminal.connecting || state.terminal.connected;
533
+ }
534
+ if (terminalDisconnectBtn) {
535
+ terminalDisconnectBtn.disabled = !(state.terminal.connecting || state.terminal.connected);
536
+ }
537
+ if (terminalStatus) {
538
+ terminalStatus.textContent = state.terminal.status;
539
+ }
212
540
 
213
541
  if (!state.active) {
214
542
  sendState.textContent = '未选择会话';
@@ -248,7 +576,7 @@
248
576
 
249
577
  function renderSessionsLoading() {
250
578
  sessionList.innerHTML = '';
251
- for (let i = 0; i < 5; i++) {
579
+ for (let i = 0; i < 3; i++) {
252
580
  const skeleton = document.createElement('div');
253
581
  skeleton.className = 'skeleton session';
254
582
  sessionList.appendChild(skeleton);
@@ -307,10 +635,17 @@
307
635
 
308
636
  btn.addEventListener('click', function () {
309
637
  if (state.loadingMessages || state.sending) return;
638
+ if ((state.terminal.connected || state.terminal.connecting) && state.terminal.sessionName && state.terminal.sessionName !== session.name) {
639
+ disconnectTerminal('会话切换,终端已断开', true);
640
+ }
310
641
  state.active = session.name;
311
642
  if (isMobileLayout()) {
312
643
  closeMobileSessionPanel();
313
644
  }
645
+ if (state.mode === 'terminal' && ensureTerminalReady()) {
646
+ renderTerminalIntro();
647
+ scheduleTerminalFit(false);
648
+ }
314
649
  syncUi();
315
650
  renderSessions();
316
651
  loadMessages().catch(function (e) {
@@ -323,51 +658,121 @@
323
658
 
324
659
  function renderMessagesLoading() {
325
660
  messagesNode.innerHTML = '';
326
- for (let i = 0; i < 3; i++) {
661
+ state.messageRenderKeys = [];
662
+ for (let i = 0; i < 2; i++) {
327
663
  const skeleton = document.createElement('div');
328
664
  skeleton.className = 'skeleton message';
329
665
  messagesNode.appendChild(skeleton);
330
666
  }
331
667
  }
332
668
 
333
- function renderMessages(messages) {
334
- messagesNode.innerHTML = '';
669
+ function isMessagesNearBottom(thresholdPx) {
670
+ const threshold = Number.isFinite(thresholdPx) ? thresholdPx : 40;
671
+ if (!messagesNode) return true;
672
+ return (messagesNode.scrollHeight - (messagesNode.scrollTop + messagesNode.clientHeight)) <= threshold;
673
+ }
674
+
675
+ function scrollMessagesToBottomImmediate() {
676
+ if (!messagesNode) return;
677
+ const previousBehavior = messagesNode.style.scrollBehavior;
678
+ messagesNode.style.scrollBehavior = 'auto';
679
+ messagesNode.scrollTop = messagesNode.scrollHeight;
680
+ messagesNode.style.scrollBehavior = previousBehavior;
681
+ }
682
+
683
+ function getMessageRenderKey(msg, index) {
684
+ if (msg && msg.id) {
685
+ return `id:${msg.id}`;
686
+ }
687
+ const role = msg && msg.role ? String(msg.role) : '';
688
+ const timestamp = msg && msg.timestamp ? String(msg.timestamp) : '';
689
+ const exitCode = msg && typeof msg.exitCode === 'number' ? String(msg.exitCode) : '';
690
+ const pending = msg && msg.pending ? '1' : '0';
691
+ const content = msg && msg.content ? String(msg.content) : '';
692
+ return `idx:${index}|${role}|${timestamp}|${exitCode}|${pending}|${content}`;
693
+ }
694
+
695
+ function createMessageRow(msg, index) {
696
+ const row = document.createElement('article');
697
+ row.className = 'msg ' + (msg.role || 'system') + (msg.pending ? ' pending' : '');
698
+ row.style.setProperty('--msg-index', String(index));
335
699
 
336
- if (state.loadingMessages) {
700
+ const meta = document.createElement('div');
701
+ meta.className = 'msg-meta';
702
+ meta.textContent = buildMessageMeta(msg);
703
+
704
+ const bubble = document.createElement('div');
705
+ bubble.className = 'bubble';
706
+
707
+ const pre = document.createElement('pre');
708
+ pre.textContent = msg.content || '';
709
+ bubble.appendChild(pre);
710
+
711
+ row.appendChild(meta);
712
+ row.appendChild(bubble);
713
+ return row;
714
+ }
715
+
716
+ function renderMessages(messages, options) {
717
+ const renderOptions = options && typeof options === 'object' ? options : {};
718
+ const stickToBottom = renderOptions.stickToBottom === true || isMessagesNearBottom(40);
719
+
720
+ if (state.loadingMessages && !messages.length) {
337
721
  renderMessagesLoading();
338
722
  return;
339
723
  }
340
724
 
341
725
  if (!messages.length) {
726
+ messagesNode.innerHTML = '';
342
727
  const empty = document.createElement('div');
343
728
  empty.className = 'empty';
344
729
  empty.textContent = '输入命令后,容器输出会显示在这里。';
345
730
  messagesNode.appendChild(empty);
731
+ state.messageRenderKeys = [];
346
732
  return;
347
733
  }
348
734
 
349
- messages.forEach(function (msg, index) {
350
- const row = document.createElement('article');
351
- row.className = 'msg ' + (msg.role || 'system') + (msg.pending ? ' pending' : '');
352
- row.style.setProperty('--msg-index', String(index));
353
-
354
- const meta = document.createElement('div');
355
- meta.className = 'msg-meta';
356
- meta.textContent = buildMessageMeta(msg);
357
-
358
- const bubble = document.createElement('div');
359
- bubble.className = 'bubble';
735
+ const nextKeys = messages.map(function (msg, index) {
736
+ return getMessageRenderKey(msg, index);
737
+ });
738
+ const prevKeys = Array.isArray(state.messageRenderKeys) ? state.messageRenderKeys : [];
739
+ const hasRenderedMessages = messagesNode.children.length === prevKeys.length && prevKeys.length > 0;
740
+ let updated = false;
741
+
742
+ if (!renderOptions.forceFullRender && hasRenderedMessages) {
743
+ let prefix = 0;
744
+ while (
745
+ prefix < prevKeys.length &&
746
+ prefix < nextKeys.length &&
747
+ prevKeys[prefix] === nextKeys[prefix]
748
+ ) {
749
+ prefix += 1;
750
+ }
360
751
 
361
- const pre = document.createElement('pre');
362
- pre.textContent = msg.content || '';
363
- bubble.appendChild(pre);
752
+ if (prefix === prevKeys.length && prefix === nextKeys.length) {
753
+ updated = true;
754
+ } else if (prefix > 0) {
755
+ while (messagesNode.children.length > prefix) {
756
+ messagesNode.removeChild(messagesNode.lastChild);
757
+ }
758
+ for (let i = prefix; i < messages.length; i++) {
759
+ messagesNode.appendChild(createMessageRow(messages[i], i));
760
+ }
761
+ updated = true;
762
+ }
763
+ }
364
764
 
365
- row.appendChild(meta);
366
- row.appendChild(bubble);
367
- messagesNode.appendChild(row);
368
- });
765
+ if (!updated) {
766
+ messagesNode.innerHTML = '';
767
+ messages.forEach(function (msg, index) {
768
+ messagesNode.appendChild(createMessageRow(msg, index));
769
+ });
770
+ }
771
+ state.messageRenderKeys = nextKeys;
369
772
 
370
- messagesNode.scrollTop = messagesNode.scrollHeight;
773
+ if (stickToBottom) {
774
+ scrollMessagesToBottomImmediate();
775
+ }
371
776
  }
372
777
 
373
778
  async function loadSessions(preferredName) {
@@ -391,6 +796,10 @@
391
796
  if (!state.active && state.sessions.length) {
392
797
  state.active = state.sessions[0].name;
393
798
  }
799
+
800
+ if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
801
+ disconnectTerminal('会话已变化,终端已断开', true);
802
+ }
394
803
  } catch (e) {
395
804
  requestError = e;
396
805
  } finally {
@@ -403,6 +812,11 @@
403
812
  throw requestError;
404
813
  }
405
814
 
815
+ if (state.mode === 'terminal' && ensureTerminalReady() && !state.terminal.connected && !state.terminal.connecting) {
816
+ renderTerminalIntro();
817
+ scheduleTerminalFit(false);
818
+ }
819
+
406
820
  await loadMessages();
407
821
  }
408
822
 
@@ -415,7 +829,9 @@
415
829
  }
416
830
 
417
831
  state.loadingMessages = true;
418
- renderMessages(state.messages);
832
+ if (!state.messages.length) {
833
+ renderMessages(state.messages);
834
+ }
419
835
  syncUi();
420
836
 
421
837
  let requestError = null;
@@ -476,7 +892,7 @@
476
892
  timestamp: new Date().toISOString(),
477
893
  pending: true
478
894
  }]);
479
- renderMessages(state.messages);
895
+ renderMessages(state.messages, { stickToBottom: true });
480
896
 
481
897
  state.sending = true;
482
898
  syncUi();
@@ -491,7 +907,7 @@
491
907
  } catch (e) {
492
908
  if (state.active === submitSession) {
493
909
  state.messages = previousMessages;
494
- renderMessages(state.messages);
910
+ renderMessages(state.messages, { stickToBottom: true });
495
911
  }
496
912
  alert(e.message);
497
913
  } finally {
@@ -519,6 +935,41 @@
519
935
  composer.requestSubmit();
520
936
  });
521
937
 
938
+ if (modeCommandBtn) {
939
+ modeCommandBtn.addEventListener('click', function () {
940
+ state.mode = 'command';
941
+ syncUi();
942
+ commandInput.focus();
943
+ });
944
+ }
945
+
946
+ if (modeTerminalBtn) {
947
+ modeTerminalBtn.addEventListener('click', function () {
948
+ state.mode = 'terminal';
949
+ syncUi();
950
+ if (ensureTerminalReady()) {
951
+ if (!state.terminal.connected && !state.terminal.connecting) {
952
+ renderTerminalIntro();
953
+ }
954
+ scheduleTerminalFit(false);
955
+ state.terminal.term.focus();
956
+ }
957
+ });
958
+ }
959
+
960
+ if (terminalConnectBtn) {
961
+ terminalConnectBtn.addEventListener('click', function () {
962
+ connectTerminal();
963
+ });
964
+ }
965
+
966
+ if (terminalDisconnectBtn) {
967
+ terminalDisconnectBtn.addEventListener('click', function () {
968
+ disconnectTerminal('终端已手动断开');
969
+ syncUi();
970
+ });
971
+ }
972
+
522
973
  refreshBtn.addEventListener('click', function () {
523
974
  closeMobileActionsMenu();
524
975
  loadSessions(state.active).catch(function (e) { alert(e.message); });
@@ -560,8 +1011,17 @@
560
1011
  function onLayoutMediaChange() {
561
1012
  setMobileSessionPanel(state.mobileSidebarOpen);
562
1013
  setMobileActionsMenu(state.mobileActionsOpen);
1014
+ if (state.mode === 'terminal' && state.terminal.terminalReady) {
1015
+ scheduleTerminalFit(false);
1016
+ }
563
1017
  }
564
1018
 
1019
+ window.addEventListener('resize', function () {
1020
+ if (state.mode === 'terminal' && state.terminal.terminalReady) {
1021
+ scheduleTerminalFit(false);
1022
+ }
1023
+ });
1024
+
565
1025
  if (typeof MOBILE_LAYOUT_MEDIA.addEventListener === 'function') {
566
1026
  MOBILE_LAYOUT_MEDIA.addEventListener('change', onLayoutMediaChange);
567
1027
  } else if (typeof MOBILE_LAYOUT_MEDIA.addListener === 'function') {
@@ -590,6 +1050,9 @@
590
1050
  if (!yes) return;
591
1051
  try {
592
1052
  const current = state.active;
1053
+ if (state.terminal.sessionName === current && (state.terminal.connected || state.terminal.connecting)) {
1054
+ disconnectTerminal('容器删除,终端已断开', true);
1055
+ }
593
1056
  await api('/api/sessions/' + encodeURIComponent(current) + '/remove', {
594
1057
  method: 'POST'
595
1058
  });
@@ -615,9 +1078,14 @@
615
1078
  }
616
1079
  });
617
1080
 
1081
+ window.addEventListener('beforeunload', function () {
1082
+ disconnectTerminal('', true);
1083
+ });
1084
+
618
1085
  renderSessions();
619
1086
  renderMessages(state.messages);
620
1087
  setMobileSessionPanel(false);
1088
+ document.body.classList.add('command-mode');
621
1089
  syncUi();
622
1090
  loadSessions().catch(function (e) {
623
1091
  alert(e.message);