@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.
- package/bin/manyoyo.js +0 -15
- package/lib/web/frontend/app.css +146 -4
- package/lib/web/frontend/app.html +18 -0
- package/lib/web/frontend/app.js +499 -31
- package/lib/web/frontend/login.css +2 -3
- package/lib/web/server.js +324 -6
- package/package.json +5 -2
package/lib/web/frontend/app.js
CHANGED
|
@@ -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
|
-
|
|
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 <
|
|
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
|
-
|
|
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
|
|
334
|
-
|
|
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
|
-
|
|
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.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|