@xcanwin/manyoyo 4.1.4 → 4.2.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.
@@ -46,6 +46,17 @@
46
46
  loadingMessages: false,
47
47
  mobileSidebarOpen: false,
48
48
  mobileActionsOpen: false,
49
+ configModalOpen: false,
50
+ createModalOpen: false,
51
+ configLoading: false,
52
+ configSaving: false,
53
+ createLoading: false,
54
+ createSubmitting: false,
55
+ createDefaults: null,
56
+ createRuns: {},
57
+ sessionNodeMap: new Map(),
58
+ sessionRenderMode: 'empty',
59
+ messageRequestId: 0,
49
60
  terminal: {
50
61
  term: null,
51
62
  fitAddon: null,
@@ -69,6 +80,35 @@
69
80
  const headerActions = document.getElementById('headerActions');
70
81
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
71
82
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
83
+ const openConfigBtn = document.getElementById('openConfigBtn');
84
+ const openCreateBtn = document.getElementById('openCreateBtn');
85
+ const configModal = document.getElementById('configModal');
86
+ const configPath = document.getElementById('configPath');
87
+ const configEditor = document.getElementById('configEditor');
88
+ const configError = document.getElementById('configError');
89
+ const configReloadBtn = document.getElementById('configReloadBtn');
90
+ const configSaveBtn = document.getElementById('configSaveBtn');
91
+ const configCancelBtn = document.getElementById('configCancelBtn');
92
+ const createModal = document.getElementById('createModal');
93
+ const createForm = document.getElementById('createSessionForm');
94
+ const createCancelBtn = document.getElementById('createCancelBtn');
95
+ const createResetBtn = document.getElementById('createResetBtn');
96
+ const createSubmitBtn = document.getElementById('createSubmitBtn');
97
+ const createError = document.getElementById('createError');
98
+ const createRun = document.getElementById('createRun');
99
+ const createContainerName = document.getElementById('createContainerName');
100
+ const createHostPath = document.getElementById('createHostPath');
101
+ const createContainerPath = document.getElementById('createContainerPath');
102
+ const createImageName = document.getElementById('createImageName');
103
+ const createImageVersion = document.getElementById('createImageVersion');
104
+ const createContainerMode = document.getElementById('createContainerMode');
105
+ const createShellPrefix = document.getElementById('createShellPrefix');
106
+ const createShell = document.getElementById('createShell');
107
+ const createShellSuffix = document.getElementById('createShellSuffix');
108
+ const createYolo = document.getElementById('createYolo');
109
+ const createEnv = document.getElementById('createEnv');
110
+ const createEnvFile = document.getElementById('createEnvFile');
111
+ const createVolumes = document.getElementById('createVolumes');
72
112
  const activeTitle = document.getElementById('activeTitle');
73
113
  const activeMeta = document.getElementById('activeMeta');
74
114
  const modeCommandBtn = document.getElementById('modeCommandBtn');
@@ -79,9 +119,6 @@
79
119
  const terminalDisconnectBtn = document.getElementById('terminalDisconnectBtn');
80
120
  const terminalStatus = document.getElementById('terminalStatus');
81
121
  const terminalScreen = document.getElementById('terminalScreen');
82
- const newSessionForm = document.getElementById('newSessionForm');
83
- const newSessionName = document.getElementById('newSessionName');
84
- const createSessionBtn = newSessionForm.querySelector('button[type="submit"]');
85
122
  const composer = document.getElementById('composer');
86
123
  const commandInput = document.getElementById('commandInput');
87
124
  const sendState = document.getElementById('sendState');
@@ -139,6 +176,215 @@
139
176
  });
140
177
  }
141
178
 
179
+ function setModalVisible(modalNode, visible) {
180
+ if (!modalNode) return;
181
+ modalNode.hidden = !visible;
182
+ document.body.classList.toggle('modal-open', Boolean(visible));
183
+ }
184
+
185
+ function showCreateError(message) {
186
+ if (!createError) return;
187
+ const text = String(message || '').trim();
188
+ if (!text) {
189
+ createError.hidden = true;
190
+ createError.textContent = '';
191
+ return;
192
+ }
193
+ createError.hidden = false;
194
+ createError.textContent = text;
195
+ }
196
+
197
+ function showConfigError(message) {
198
+ if (!configError) return;
199
+ const text = String(message || '').trim();
200
+ if (!text) {
201
+ configError.hidden = true;
202
+ configError.textContent = '';
203
+ return;
204
+ }
205
+ configError.hidden = false;
206
+ configError.textContent = text;
207
+ }
208
+
209
+ function envMapToText(envMap) {
210
+ if (!envMap || typeof envMap !== 'object') {
211
+ return '';
212
+ }
213
+ return Object.entries(envMap)
214
+ .map(function (entry) {
215
+ return entry[0] + '=' + String(entry[1] == null ? '' : entry[1]);
216
+ })
217
+ .join('\n');
218
+ }
219
+
220
+ function textToLineArray(text) {
221
+ return String(text || '')
222
+ .split('\n')
223
+ .map(function (line) { return line.trim(); })
224
+ .filter(Boolean);
225
+ }
226
+
227
+ function textToEnvMap(text) {
228
+ const envMap = {};
229
+ const lines = textToLineArray(text);
230
+ lines.forEach(function (line) {
231
+ const idx = line.indexOf('=');
232
+ if (idx <= 0) {
233
+ throw new Error('env 每行必须是 KEY=VALUE');
234
+ }
235
+ const key = line.slice(0, idx).trim();
236
+ const value = line.slice(idx + 1);
237
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
238
+ throw new Error('env key 非法: ' + key);
239
+ }
240
+ envMap[key] = value;
241
+ });
242
+ return envMap;
243
+ }
244
+
245
+ function fillCreateForm(defaults) {
246
+ const value = defaults && typeof defaults === 'object' ? defaults : {};
247
+ createContainerName.value = value.containerName || '';
248
+ createHostPath.value = value.hostPath || '';
249
+ createContainerPath.value = value.containerPath || '';
250
+ createImageName.value = value.imageName || '';
251
+ createImageVersion.value = value.imageVersion || '';
252
+ createContainerMode.value = value.containerMode || '';
253
+ createShellPrefix.value = value.shellPrefix || '';
254
+ createShell.value = value.shell || '';
255
+ createShellSuffix.value = value.shellSuffix || '';
256
+ createYolo.value = value.yolo || '';
257
+ createEnv.value = envMapToText(value.env);
258
+ createEnvFile.value = Array.isArray(value.envFile) ? value.envFile.join('\n') : '';
259
+ createVolumes.value = Array.isArray(value.volumes) ? value.volumes.join('\n') : '';
260
+ }
261
+
262
+ function mergeCreateDefaults(baseDefaults, runConfig) {
263
+ const base = baseDefaults && typeof baseDefaults === 'object' ? baseDefaults : {};
264
+ const run = runConfig && typeof runConfig === 'object' ? runConfig : {};
265
+ const merged = {
266
+ containerName: run.containerName != null ? String(run.containerName) : (base.containerName || ''),
267
+ hostPath: run.hostPath != null ? String(run.hostPath) : (base.hostPath || ''),
268
+ containerPath: run.containerPath != null ? String(run.containerPath) : (base.containerPath || ''),
269
+ imageName: run.imageName != null ? String(run.imageName) : (base.imageName || ''),
270
+ imageVersion: run.imageVersion != null ? String(run.imageVersion) : (base.imageVersion || ''),
271
+ containerMode: run.containerMode != null ? String(run.containerMode) : (base.containerMode || ''),
272
+ shellPrefix: run.shellPrefix != null ? String(run.shellPrefix) : (base.shellPrefix || ''),
273
+ shell: run.shell != null ? String(run.shell) : (base.shell || ''),
274
+ shellSuffix: run.shellSuffix != null ? String(run.shellSuffix) : (base.shellSuffix || ''),
275
+ yolo: run.yolo != null ? String(run.yolo) : (base.yolo || ''),
276
+ env: {},
277
+ envFile: [],
278
+ volumes: []
279
+ };
280
+
281
+ const baseEnv = base.env && typeof base.env === 'object' ? base.env : {};
282
+ const runEnv = run.env && typeof run.env === 'object' ? run.env : {};
283
+ merged.env = Object.assign({}, baseEnv, runEnv);
284
+
285
+ const baseEnvFile = Array.isArray(base.envFile) ? base.envFile : [];
286
+ const runEnvFile = Array.isArray(run.envFile) ? run.envFile : [];
287
+ merged.envFile = baseEnvFile.concat(runEnvFile).map(function (item) {
288
+ return String(item || '').trim();
289
+ }).filter(Boolean);
290
+
291
+ const baseVolumes = Array.isArray(base.volumes) ? base.volumes : [];
292
+ const runVolumes = Array.isArray(run.volumes) ? run.volumes : [];
293
+ merged.volumes = baseVolumes.concat(runVolumes).map(function (item) {
294
+ return String(item || '').trim();
295
+ }).filter(Boolean);
296
+
297
+ return merged;
298
+ }
299
+
300
+ function normalizeRunProfiles(parsedConfig) {
301
+ const config = parsedConfig && typeof parsedConfig === 'object' ? parsedConfig : {};
302
+ if (!config.runs || typeof config.runs !== 'object' || Array.isArray(config.runs)) {
303
+ return {};
304
+ }
305
+ const result = {};
306
+ Object.entries(config.runs).forEach(function (entry) {
307
+ const key = String(entry[0] || '').trim();
308
+ const value = entry[1];
309
+ if (!key || !value || typeof value !== 'object' || Array.isArray(value)) {
310
+ return;
311
+ }
312
+ result[key] = value;
313
+ });
314
+ return result;
315
+ }
316
+
317
+ function renderRunOptions(runs) {
318
+ if (!createRun) return;
319
+ const current = createRun.value || '';
320
+ createRun.innerHTML = '';
321
+ const placeholder = document.createElement('option');
322
+ placeholder.value = '';
323
+ placeholder.textContent = '(不使用 run)';
324
+ createRun.appendChild(placeholder);
325
+
326
+ Object.keys(runs || {}).sort().forEach(function (runName) {
327
+ const option = document.createElement('option');
328
+ option.value = runName;
329
+ option.textContent = runName;
330
+ createRun.appendChild(option);
331
+ });
332
+
333
+ if (current && runs && Object.prototype.hasOwnProperty.call(runs, current)) {
334
+ createRun.value = current;
335
+ } else {
336
+ createRun.value = '';
337
+ }
338
+ }
339
+
340
+ function applyCurrentRunDefaults() {
341
+ const selectedRun = createRun ? String(createRun.value || '').trim() : '';
342
+ if (!selectedRun) {
343
+ fillCreateForm(state.createDefaults || {});
344
+ return;
345
+ }
346
+ const runConfig = state.createRuns && state.createRuns[selectedRun] ? state.createRuns[selectedRun] : {};
347
+ fillCreateForm(mergeCreateDefaults(state.createDefaults || {}, runConfig));
348
+ }
349
+
350
+ function collectCreateOptions() {
351
+ const options = {
352
+ containerName: (createContainerName.value || '').trim(),
353
+ hostPath: (createHostPath.value || '').trim(),
354
+ containerPath: (createContainerPath.value || '').trim(),
355
+ imageName: (createImageName.value || '').trim(),
356
+ imageVersion: (createImageVersion.value || '').trim(),
357
+ containerMode: (createContainerMode.value || '').trim(),
358
+ shellPrefix: (createShellPrefix.value || '').trim(),
359
+ shell: (createShell.value || '').trim(),
360
+ shellSuffix: (createShellSuffix.value || '').trim(),
361
+ yolo: (createYolo.value || '').trim(),
362
+ env: textToEnvMap(createEnv.value),
363
+ envFile: textToLineArray(createEnvFile.value),
364
+ volumes: textToLineArray(createVolumes.value)
365
+ };
366
+
367
+ Object.keys(options).forEach(function (key) {
368
+ if (Array.isArray(options[key])) {
369
+ if (!options[key].length) {
370
+ delete options[key];
371
+ }
372
+ return;
373
+ }
374
+ if (typeof options[key] === 'object') {
375
+ if (!options[key] || !Object.keys(options[key]).length) {
376
+ delete options[key];
377
+ }
378
+ return;
379
+ }
380
+ if (!options[key]) {
381
+ delete options[key];
382
+ }
383
+ });
384
+
385
+ return options;
386
+ }
387
+
142
388
  function getActiveSession() {
143
389
  if (!state.active) return null;
144
390
  return state.sessions.find(function (session) {
@@ -156,19 +402,19 @@
156
402
  return `${status.label} · ${messageCount} 条对话 · ${updatedAt}`;
157
403
  }
158
404
 
159
- function buildMessageMeta(message) {
160
- const parts = [roleName(message.role)];
161
- const timeText = formatDateTime(message.timestamp);
405
+ function buildMessageMetaLines(message) {
406
+ const lines = [];
407
+ const timeText = formatDateTime(message && message.timestamp);
162
408
  if (timeText) {
163
- parts.push(timeText);
409
+ lines.push({ className: 'msg-meta-time', text: timeText });
164
410
  }
165
- if (typeof message.exitCode === 'number') {
166
- parts.push(`exit ${message.exitCode}`);
167
- }
168
- if (message.pending) {
169
- parts.push('发送中');
170
- }
171
- return parts.join(' · ');
411
+
412
+ lines.push({
413
+ className: 'msg-meta-role',
414
+ text: roleName(message && message.role)
415
+ });
416
+
417
+ return lines;
172
418
  }
173
419
 
174
420
  function writeTerminalLine(text) {
@@ -206,6 +452,30 @@
206
452
  };
207
453
  }
208
454
 
455
+ function readCssVar(name, fallbackValue) {
456
+ if (!name || !window.getComputedStyle) {
457
+ return fallbackValue;
458
+ }
459
+ const root = document.documentElement;
460
+ if (!root) {
461
+ return fallbackValue;
462
+ }
463
+ const value = window.getComputedStyle(root).getPropertyValue(name);
464
+ if (!value) {
465
+ return fallbackValue;
466
+ }
467
+ const trimmed = value.trim();
468
+ return trimmed || fallbackValue;
469
+ }
470
+
471
+ function resolveTerminalTheme() {
472
+ return {
473
+ background: readCssVar('--terminal-bg', '#11161d'),
474
+ foreground: readCssVar('--terminal-fg', '#e8edf5'),
475
+ cursor: readCssVar('--terminal-cursor', '#ffd166')
476
+ };
477
+ }
478
+
209
479
  function notifyTerminalResize(force) {
210
480
  if (!state.terminal.term) return;
211
481
  if (!state.terminal.socket || state.terminal.socket.readyState !== window.WebSocket.OPEN) return;
@@ -272,14 +542,10 @@
272
542
  state.terminal.term = new window.Terminal({
273
543
  cursorBlink: true,
274
544
  convertEol: false,
275
- fontFamily: '"IBM Plex Mono", "SFMono-Regular", Consolas, Menlo, monospace',
545
+ fontFamily: readCssVar('--font-mono', '"IBM Plex Mono", "SFMono-Regular", Consolas, Menlo, monospace'),
276
546
  fontSize: 13,
277
547
  scrollback: 5000,
278
- theme: {
279
- background: '#0c131a',
280
- foreground: '#dde8f3',
281
- cursor: '#6fe7b5'
282
- }
548
+ theme: resolveTerminalTheme()
283
549
  });
284
550
  state.terminal.fitAddon = new FitAddonCtor();
285
551
  state.terminal.term.loadAddon(state.terminal.fitAddon);
@@ -527,7 +793,36 @@
527
793
  removeAllBtn.disabled = !state.active || busy;
528
794
  sendBtn.disabled = !commandMode || !state.active || busy;
529
795
  commandInput.disabled = !commandMode || !state.active || state.sending;
530
- createSessionBtn.disabled = state.loadingSessions || state.sending;
796
+ if (openCreateBtn) {
797
+ openCreateBtn.disabled = state.createLoading || state.createSubmitting;
798
+ }
799
+ if (openConfigBtn) {
800
+ openConfigBtn.disabled = state.configLoading || state.configSaving;
801
+ }
802
+ if (configSaveBtn) {
803
+ configSaveBtn.disabled = state.configLoading || state.configSaving;
804
+ }
805
+ if (configReloadBtn) {
806
+ configReloadBtn.disabled = state.configLoading || state.configSaving;
807
+ }
808
+ if (configCancelBtn) {
809
+ configCancelBtn.disabled = state.configSaving;
810
+ }
811
+ if (createSubmitBtn) {
812
+ createSubmitBtn.disabled = state.createLoading || state.createSubmitting;
813
+ }
814
+ if (createResetBtn) {
815
+ createResetBtn.disabled = state.createSubmitting;
816
+ }
817
+ if (createCancelBtn) {
818
+ createCancelBtn.disabled = state.createSubmitting;
819
+ }
820
+ if (configModal) {
821
+ configModal.hidden = !state.configModalOpen;
822
+ }
823
+ if (createModal) {
824
+ createModal.hidden = !state.createModalOpen;
825
+ }
531
826
  if (terminalConnectBtn) {
532
827
  terminalConnectBtn.disabled = !state.active || busy || state.terminal.connecting || state.terminal.connected;
533
828
  }
@@ -569,12 +864,104 @@
569
864
  data = {};
570
865
  }
571
866
  if (!response.ok) {
572
- throw new Error(data.error || '请求失败');
867
+ const errorText = data && data.detail ? `${data.error || '请求失败'}: ${data.detail}` : (data.error || '请求失败');
868
+ throw new Error(errorText);
573
869
  }
574
870
  return data;
575
871
  }
576
872
 
873
+ async function fetchConfigSnapshot() {
874
+ return await api('/api/config');
875
+ }
876
+
877
+ async function openConfigModal() {
878
+ closeCreateModal();
879
+ state.configLoading = true;
880
+ showConfigError('');
881
+ syncUi();
882
+ try {
883
+ const config = await fetchConfigSnapshot();
884
+ if (configPath) {
885
+ configPath.textContent = config.path || '';
886
+ }
887
+ if (configEditor) {
888
+ configEditor.value = typeof config.raw === 'string' ? config.raw : '';
889
+ }
890
+ if (config.parseError) {
891
+ showConfigError('当前文件存在解析错误:' + config.parseError);
892
+ }
893
+ state.configModalOpen = true;
894
+ setModalVisible(configModal, true);
895
+ } catch (e) {
896
+ alert(e.message);
897
+ } finally {
898
+ state.configLoading = false;
899
+ syncUi();
900
+ }
901
+ }
902
+
903
+ function closeConfigModal() {
904
+ state.configModalOpen = false;
905
+ setModalVisible(configModal, false);
906
+ }
907
+
908
+ async function saveConfig() {
909
+ state.configSaving = true;
910
+ showConfigError('');
911
+ syncUi();
912
+ try {
913
+ await api('/api/config', {
914
+ method: 'PUT',
915
+ body: JSON.stringify({ raw: configEditor ? configEditor.value : '' })
916
+ });
917
+ showConfigError('');
918
+ alert('配置已保存。后续新建会读取最新配置。');
919
+ } catch (e) {
920
+ showConfigError(e.message);
921
+ } finally {
922
+ state.configSaving = false;
923
+ syncUi();
924
+ }
925
+ }
926
+
927
+ async function openCreateModal() {
928
+ closeConfigModal();
929
+ state.createLoading = true;
930
+ showCreateError('');
931
+ syncUi();
932
+ try {
933
+ const config = await fetchConfigSnapshot();
934
+ state.createDefaults = config.defaults || {};
935
+ state.createRuns = normalizeRunProfiles(config.parsed || {});
936
+ renderRunOptions(state.createRuns);
937
+ applyCurrentRunDefaults();
938
+ if (config.parseError) {
939
+ showCreateError('配置文件解析失败,已使用安全默认值。建议先修复配置:' + config.parseError);
940
+ }
941
+ state.createModalOpen = true;
942
+ setModalVisible(createModal, true);
943
+ } catch (e) {
944
+ alert(e.message);
945
+ } finally {
946
+ state.createLoading = false;
947
+ syncUi();
948
+ }
949
+ }
950
+
951
+ function closeCreateModal() {
952
+ state.createModalOpen = false;
953
+ setModalVisible(createModal, false);
954
+ showCreateError('');
955
+ }
956
+
957
+ function resetCreateModal() {
958
+ applyCurrentRunDefaults();
959
+ showCreateError('');
960
+ }
961
+
577
962
  function renderSessionsLoading() {
963
+ state.sessionNodeMap.clear();
964
+ state.sessionRenderMode = 'loading';
578
965
  sessionList.innerHTML = '';
579
966
  for (let i = 0; i < 3; i++) {
580
967
  const skeleton = document.createElement('div');
@@ -583,9 +970,114 @@
583
970
  }
584
971
  }
585
972
 
973
+ function getSessionRenderKey(session) {
974
+ return [
975
+ String(session && session.name ? session.name : ''),
976
+ String(session && session.status ? session.status : ''),
977
+ String(safeMessageCount(session && session.messageCount)),
978
+ String(session && session.updatedAt ? session.updatedAt : ''),
979
+ String(session && session.image ? session.image : '')
980
+ ].join('|');
981
+ }
982
+
983
+ function renderSessionActiveState() {
984
+ for (const [name, node] of state.sessionNodeMap.entries()) {
985
+ node.classList.toggle('active', state.active === name);
986
+ }
987
+ }
988
+
989
+ function updateSessionRow(row, session, index) {
990
+ if (!row || !session) return;
991
+ const status = sessionStatusInfo(session.status);
992
+ row.style.setProperty('--item-index', String(index));
993
+ row.classList.toggle('active', state.active === session.name);
994
+ row.classList.toggle('history-only', status.tone === 'history');
995
+ if (row.__sessionNameNode) {
996
+ row.__sessionNameNode.textContent = session.name;
997
+ }
998
+ if (row.__statusBadgeNode) {
999
+ row.__statusBadgeNode.className = `session-status ${status.tone}`;
1000
+ row.__statusBadgeNode.textContent = status.label;
1001
+ }
1002
+ if (row.__messageCountNode) {
1003
+ row.__messageCountNode.textContent = `${safeMessageCount(session.messageCount)} 条`;
1004
+ }
1005
+ if (row.__timeNode) {
1006
+ row.__timeNode.textContent = formatDateTime(session.updatedAt) || '暂无更新';
1007
+ }
1008
+ row.__renderKey = getSessionRenderKey(session);
1009
+ }
1010
+
1011
+ function handleSessionItemClick(sessionName) {
1012
+ if (state.loadingMessages) return;
1013
+ if (!sessionName) return;
1014
+ if (state.active === sessionName) {
1015
+ if (isMobileLayout()) {
1016
+ closeMobileSessionPanel();
1017
+ }
1018
+ return;
1019
+ }
1020
+ if ((state.terminal.connected || state.terminal.connecting) && state.terminal.sessionName && state.terminal.sessionName !== sessionName) {
1021
+ disconnectTerminal('会话切换,终端已断开', true);
1022
+ }
1023
+ state.active = sessionName;
1024
+ if (isMobileLayout()) {
1025
+ closeMobileSessionPanel();
1026
+ }
1027
+ if (state.mode === 'terminal' && ensureTerminalReady()) {
1028
+ renderTerminalIntro();
1029
+ scheduleTerminalFit(false);
1030
+ }
1031
+ renderSessionActiveState();
1032
+ syncUi();
1033
+ loadMessagesForSession(sessionName).catch(function (e) {
1034
+ alert(e.message);
1035
+ });
1036
+ }
1037
+
1038
+ function createSessionRow(session, index) {
1039
+ const status = sessionStatusInfo(session.status);
1040
+ const btn = document.createElement('button');
1041
+ btn.type = 'button';
1042
+ btn.className = 'session-item';
1043
+ btn.dataset.sessionName = session.name;
1044
+
1045
+ const sessionName = document.createElement('div');
1046
+ sessionName.className = 'session-name';
1047
+
1048
+ const meta = document.createElement('div');
1049
+ meta.className = 'session-meta';
1050
+
1051
+ const statusBadge = document.createElement('span');
1052
+ statusBadge.className = `session-status ${status.tone}`;
1053
+
1054
+ const messageCount = document.createElement('span');
1055
+ messageCount.className = 'session-count';
1056
+
1057
+ meta.appendChild(statusBadge);
1058
+ meta.appendChild(messageCount);
1059
+
1060
+ const time = document.createElement('div');
1061
+ time.className = 'session-time';
1062
+
1063
+ btn.appendChild(sessionName);
1064
+ btn.appendChild(meta);
1065
+ btn.appendChild(time);
1066
+ btn.__sessionNameNode = sessionName;
1067
+ btn.__statusBadgeNode = statusBadge;
1068
+ btn.__messageCountNode = messageCount;
1069
+ btn.__timeNode = time;
1070
+
1071
+ btn.addEventListener('click', function () {
1072
+ handleSessionItemClick(btn.dataset.sessionName || '');
1073
+ });
1074
+
1075
+ updateSessionRow(btn, session, index);
1076
+ return btn;
1077
+ }
1078
+
586
1079
  function renderSessions() {
587
1080
  sessionCount.textContent = state.loadingSessions ? '加载中...' : `${state.sessions.length} 个`;
588
- sessionList.innerHTML = '';
589
1081
 
590
1082
  if (state.loadingSessions) {
591
1083
  renderSessionsLoading();
@@ -593,6 +1085,9 @@
593
1085
  }
594
1086
 
595
1087
  if (!state.sessions.length) {
1088
+ state.sessionNodeMap.clear();
1089
+ state.sessionRenderMode = 'empty';
1090
+ sessionList.innerHTML = '';
596
1091
  const empty = document.createElement('div');
597
1092
  empty.className = 'empty';
598
1093
  empty.textContent = '暂无 manyoyo 会话';
@@ -600,60 +1095,49 @@
600
1095
  return;
601
1096
  }
602
1097
 
1098
+ if (state.sessionRenderMode !== 'list') {
1099
+ sessionList.innerHTML = '';
1100
+ state.sessionNodeMap.clear();
1101
+ state.sessionRenderMode = 'list';
1102
+ }
1103
+
1104
+ const nextNameSet = new Set();
603
1105
  state.sessions.forEach(function (session, index) {
604
- const status = sessionStatusInfo(session.status);
605
- const btn = document.createElement('button');
606
- btn.type = 'button';
607
- btn.className = 'session-item' + (state.active === session.name ? ' active' : '');
608
- btn.style.setProperty('--item-index', String(index));
609
-
610
- const sessionName = document.createElement('div');
611
- sessionName.className = 'session-name';
612
- sessionName.textContent = session.name;
613
-
614
- const meta = document.createElement('div');
615
- meta.className = 'session-meta';
616
-
617
- const statusBadge = document.createElement('span');
618
- statusBadge.className = `session-status ${status.tone}`;
619
- statusBadge.textContent = status.label;
620
-
621
- const messageCount = document.createElement('span');
622
- messageCount.className = 'session-count';
623
- messageCount.textContent = `${safeMessageCount(session.messageCount)} 条`;
624
-
625
- meta.appendChild(statusBadge);
626
- meta.appendChild(messageCount);
627
-
628
- const time = document.createElement('div');
629
- time.className = 'session-time';
630
- time.textContent = formatDateTime(session.updatedAt) || '暂无更新';
631
-
632
- btn.appendChild(sessionName);
633
- btn.appendChild(meta);
634
- btn.appendChild(time);
635
-
636
- btn.addEventListener('click', function () {
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
- }
641
- state.active = session.name;
642
- if (isMobileLayout()) {
643
- closeMobileSessionPanel();
644
- }
645
- if (state.mode === 'terminal' && ensureTerminalReady()) {
646
- renderTerminalIntro();
647
- scheduleTerminalFit(false);
648
- }
649
- syncUi();
650
- renderSessions();
651
- loadMessages().catch(function (e) {
652
- alert(e.message);
653
- });
654
- });
655
- sessionList.appendChild(btn);
1106
+ nextNameSet.add(session.name);
1107
+ let row = state.sessionNodeMap.get(session.name);
1108
+ if (!row) {
1109
+ row = createSessionRow(session, index);
1110
+ state.sessionNodeMap.set(session.name, row);
1111
+ } else if (row.__renderKey !== getSessionRenderKey(session)) {
1112
+ updateSessionRow(row, session, index);
1113
+ } else {
1114
+ row.style.setProperty('--item-index', String(index));
1115
+ }
1116
+
1117
+ const currentAtIndex = sessionList.children[index];
1118
+ if (currentAtIndex !== row) {
1119
+ sessionList.insertBefore(row, currentAtIndex || null);
1120
+ }
1121
+ });
1122
+
1123
+ const removeNames = [];
1124
+ for (const existingName of state.sessionNodeMap.keys()) {
1125
+ if (!nextNameSet.has(existingName)) {
1126
+ removeNames.push(existingName);
1127
+ }
1128
+ }
1129
+ removeNames.forEach(function (name) {
1130
+ const row = state.sessionNodeMap.get(name);
1131
+ if (row && row.parentNode === sessionList) {
1132
+ sessionList.removeChild(row);
1133
+ }
1134
+ state.sessionNodeMap.delete(name);
656
1135
  });
1136
+
1137
+ while (sessionList.children.length > state.sessions.length) {
1138
+ sessionList.removeChild(sessionList.lastChild);
1139
+ }
1140
+ renderSessionActiveState();
657
1141
  }
658
1142
 
659
1143
  function renderMessagesLoading() {
@@ -680,6 +1164,12 @@
680
1164
  messagesNode.style.scrollBehavior = previousBehavior;
681
1165
  }
682
1166
 
1167
+ function createLocalMessageId(prefix) {
1168
+ const head = String(prefix || 'local');
1169
+ const tail = Math.random().toString(16).slice(2, 8);
1170
+ return `${head}-${Date.now()}-${tail}`;
1171
+ }
1172
+
683
1173
  function getMessageRenderKey(msg, index) {
684
1174
  if (msg && msg.id) {
685
1175
  return `id:${msg.id}`;
@@ -699,7 +1189,13 @@
699
1189
 
700
1190
  const meta = document.createElement('div');
701
1191
  meta.className = 'msg-meta';
702
- meta.textContent = buildMessageMeta(msg);
1192
+ const metaLines = buildMessageMetaLines(msg);
1193
+ metaLines.forEach(function (line) {
1194
+ const lineNode = document.createElement('div');
1195
+ lineNode.className = 'msg-meta-line ' + line.className;
1196
+ lineNode.textContent = line.text;
1197
+ meta.appendChild(lineNode);
1198
+ });
703
1199
 
704
1200
  const bubble = document.createElement('div');
705
1201
  bubble.className = 'bubble';
@@ -710,6 +1206,12 @@
710
1206
 
711
1207
  row.appendChild(meta);
712
1208
  row.appendChild(bubble);
1209
+ if ((msg.role || '') === 'assistant' && typeof msg.exitCode === 'number') {
1210
+ const exitNode = document.createElement('div');
1211
+ exitNode.className = 'msg-exit';
1212
+ exitNode.textContent = `exit ${msg.exitCode}`;
1213
+ row.appendChild(exitNode);
1214
+ }
713
1215
  return row;
714
1216
  }
715
1217
 
@@ -775,35 +1277,44 @@
775
1277
  }
776
1278
  }
777
1279
 
778
- async function loadSessions(preferredName) {
779
- state.loadingSessions = true;
780
- renderSessions();
781
- syncUi();
1280
+ function applySessionsSnapshot(rawSessions, preferredName) {
1281
+ state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
782
1282
 
783
- let requestError = null;
784
- try {
785
- const data = await api('/api/sessions');
786
- state.sessions = Array.isArray(data.sessions) ? data.sessions : [];
1283
+ if (typeof preferredName === 'string' && preferredName.trim()) {
1284
+ state.active = preferredName.trim();
1285
+ }
787
1286
 
788
- if (typeof preferredName === 'string') {
789
- state.active = preferredName;
790
- }
1287
+ if (state.active && !state.sessions.some(function (session) { return session.name === state.active; })) {
1288
+ state.active = '';
1289
+ }
1290
+ if (!state.active && state.sessions.length) {
1291
+ state.active = state.sessions[0].name;
1292
+ }
1293
+ if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
1294
+ disconnectTerminal('会话已变化,终端已断开', true);
1295
+ }
1296
+ }
791
1297
 
792
- if (state.active && !state.sessions.some(function (session) { return session.name === state.active; })) {
793
- state.active = '';
794
- }
1298
+ async function refreshSessions(options) {
1299
+ const opts = options && typeof options === 'object' ? options : {};
1300
+ const withLoading = opts.withLoading !== false;
795
1301
 
796
- if (!state.active && state.sessions.length) {
797
- state.active = state.sessions[0].name;
798
- }
1302
+ if (withLoading) {
1303
+ state.loadingSessions = true;
1304
+ renderSessions();
1305
+ syncUi();
1306
+ }
799
1307
 
800
- if (state.terminal.sessionName && state.terminal.sessionName !== state.active) {
801
- disconnectTerminal('会话已变化,终端已断开', true);
802
- }
1308
+ let requestError = null;
1309
+ try {
1310
+ const data = await api('/api/sessions');
1311
+ applySessionsSnapshot(data.sessions, opts.preferredName);
803
1312
  } catch (e) {
804
1313
  requestError = e;
805
1314
  } finally {
806
- state.loadingSessions = false;
1315
+ if (withLoading) {
1316
+ state.loadingSessions = false;
1317
+ }
807
1318
  renderSessions();
808
1319
  syncUi();
809
1320
  }
@@ -817,64 +1328,212 @@
817
1328
  scheduleTerminalFit(false);
818
1329
  }
819
1330
 
820
- await loadMessages();
1331
+ if (opts.reloadMessages) {
1332
+ await loadMessagesForSession(state.active, { silent: false });
1333
+ }
821
1334
  }
822
1335
 
823
- async function loadMessages() {
824
- if (!state.active) {
1336
+ async function loadSessions(preferredName) {
1337
+ await refreshSessions({
1338
+ preferredName: preferredName,
1339
+ withLoading: true,
1340
+ reloadMessages: true
1341
+ });
1342
+ }
1343
+
1344
+ async function refreshSessionsSilent(options) {
1345
+ const opts = options && typeof options === 'object' ? options : {};
1346
+ await refreshSessions({
1347
+ preferredName: opts.preferredName,
1348
+ withLoading: false,
1349
+ reloadMessages: false
1350
+ });
1351
+ }
1352
+
1353
+ async function loadMessagesForSession(sessionName, options) {
1354
+ const opts = options && typeof options === 'object' ? options : {};
1355
+ const targetSession = typeof sessionName === 'string' ? sessionName.trim() : '';
1356
+
1357
+ if (!targetSession) {
1358
+ state.messageRequestId += 1;
825
1359
  state.messages = [];
1360
+ state.loadingMessages = false;
826
1361
  renderMessages(state.messages);
827
1362
  syncUi();
828
1363
  return;
829
1364
  }
830
1365
 
831
- state.loadingMessages = true;
832
- if (!state.messages.length) {
833
- renderMessages(state.messages);
1366
+ const requestId = state.messageRequestId + 1;
1367
+ state.messageRequestId = requestId;
1368
+ const silent = opts.silent === true;
1369
+
1370
+ if (!silent) {
1371
+ state.loadingMessages = true;
1372
+ if (!state.messages.length) {
1373
+ renderMessages(state.messages);
1374
+ }
1375
+ syncUi();
834
1376
  }
835
- syncUi();
836
1377
 
837
1378
  let requestError = null;
1379
+ let data = null;
838
1380
  try {
839
- const data = await api('/api/sessions/' + encodeURIComponent(state.active) + '/messages');
840
- state.messages = Array.isArray(data.messages) ? data.messages : [];
1381
+ data = await api('/api/sessions/' + encodeURIComponent(targetSession) + '/messages');
841
1382
  } catch (e) {
842
1383
  requestError = e;
843
1384
  } finally {
1385
+ if (requestId !== state.messageRequestId) {
1386
+ return;
1387
+ }
844
1388
  state.loadingMessages = false;
845
- renderMessages(state.messages);
846
- syncUi();
1389
+ if (!requestError && targetSession === state.active) {
1390
+ state.messages = Array.isArray(data && data.messages) ? data.messages : [];
1391
+ }
1392
+ if (!requestError || targetSession === state.active) {
1393
+ renderMessages(state.messages);
1394
+ syncUi();
1395
+ } else {
1396
+ syncUi();
1397
+ }
847
1398
  }
848
1399
 
1400
+ if (requestId !== state.messageRequestId) {
1401
+ return;
1402
+ }
849
1403
  if (requestError) {
850
1404
  throw requestError;
851
1405
  }
852
1406
  }
853
1407
 
854
- newSessionForm.addEventListener('submit', async function (event) {
855
- event.preventDefault();
856
- if (state.loadingSessions || state.sending) return;
857
- const previousText = createSessionBtn.textContent;
858
- createSessionBtn.textContent = '创建中...';
859
- createSessionBtn.disabled = true;
860
- try {
861
- const name = (newSessionName.value || '').trim();
862
- const data = await api('/api/sessions', {
863
- method: 'POST',
864
- body: JSON.stringify({ name: name })
865
- });
866
- newSessionName.value = '';
867
- await loadSessions(data.name);
868
- if (isMobileLayout()) {
869
- closeMobileSessionPanel();
1408
+ async function loadMessages() {
1409
+ await loadMessagesForSession(state.active, { silent: false });
1410
+ }
1411
+
1412
+ function bumpSessionMetaAfterSend(sessionName) {
1413
+ if (!sessionName) return;
1414
+ const session = state.sessions.find(function (item) {
1415
+ return item && item.name === sessionName;
1416
+ });
1417
+ if (!session) return;
1418
+ session.messageCount = safeMessageCount(session.messageCount) + 2;
1419
+ session.updatedAt = new Date().toISOString();
1420
+ renderSessions();
1421
+ syncUi();
1422
+ }
1423
+
1424
+ function confirmPendingUserMessage(sessionName, pendingMessageId) {
1425
+ if (state.active !== sessionName) {
1426
+ return -1;
1427
+ }
1428
+ for (let i = state.messages.length - 1; i >= 0; i -= 1) {
1429
+ const message = state.messages[i];
1430
+ if (!message || message.role !== 'user') {
1431
+ continue;
870
1432
  }
871
- } catch (e) {
872
- alert(e.message);
873
- } finally {
874
- createSessionBtn.textContent = previousText;
875
- syncUi();
1433
+ if (!message.pending) {
1434
+ continue;
1435
+ }
1436
+ if (String(message.id || '') !== String(pendingMessageId || '')) {
1437
+ continue;
1438
+ }
1439
+ message.pending = false;
1440
+ return i;
876
1441
  }
877
- });
1442
+ return -1;
1443
+ }
1444
+
1445
+ function appendAssistantMessageLocal(sessionName, result) {
1446
+ if (state.active !== sessionName) {
1447
+ return;
1448
+ }
1449
+ const exitCode = typeof (result && result.exitCode) === 'number' ? result.exitCode : 1;
1450
+ const outputText = String(result && result.output ? result.output : '(无输出)');
1451
+ state.messages.push({
1452
+ id: createLocalMessageId('local-assistant'),
1453
+ role: 'assistant',
1454
+ content: outputText,
1455
+ timestamp: new Date().toISOString(),
1456
+ exitCode: exitCode
1457
+ });
1458
+ }
1459
+
1460
+ if (openConfigBtn) {
1461
+ openConfigBtn.addEventListener('click', function () {
1462
+ openConfigModal();
1463
+ });
1464
+ }
1465
+
1466
+ if (openCreateBtn) {
1467
+ openCreateBtn.addEventListener('click', function () {
1468
+ openCreateModal();
1469
+ });
1470
+ }
1471
+
1472
+ if (configCancelBtn) {
1473
+ configCancelBtn.addEventListener('click', function () {
1474
+ closeConfigModal();
1475
+ syncUi();
1476
+ });
1477
+ }
1478
+
1479
+ if (configReloadBtn) {
1480
+ configReloadBtn.addEventListener('click', function () {
1481
+ openConfigModal();
1482
+ });
1483
+ }
1484
+
1485
+ if (configSaveBtn) {
1486
+ configSaveBtn.addEventListener('click', function () {
1487
+ saveConfig();
1488
+ });
1489
+ }
1490
+
1491
+ if (createCancelBtn) {
1492
+ createCancelBtn.addEventListener('click', function () {
1493
+ closeCreateModal();
1494
+ syncUi();
1495
+ });
1496
+ }
1497
+
1498
+ if (createResetBtn) {
1499
+ createResetBtn.addEventListener('click', function () {
1500
+ resetCreateModal();
1501
+ });
1502
+ }
1503
+
1504
+ if (createRun) {
1505
+ createRun.addEventListener('change', function () {
1506
+ applyCurrentRunDefaults();
1507
+ showCreateError('');
1508
+ });
1509
+ }
1510
+
1511
+ if (createForm) {
1512
+ createForm.addEventListener('submit', async function (event) {
1513
+ event.preventDefault();
1514
+ if (state.createSubmitting || state.createLoading) return;
1515
+ state.createSubmitting = true;
1516
+ showCreateError('');
1517
+ syncUi();
1518
+ try {
1519
+ const createOptions = collectCreateOptions();
1520
+ const data = await api('/api/sessions', {
1521
+ method: 'POST',
1522
+ body: JSON.stringify({ createOptions: createOptions })
1523
+ });
1524
+ closeCreateModal();
1525
+ await loadSessions(data.name);
1526
+ if (isMobileLayout()) {
1527
+ closeMobileSessionPanel();
1528
+ }
1529
+ } catch (e) {
1530
+ showCreateError(e.message);
1531
+ } finally {
1532
+ state.createSubmitting = false;
1533
+ syncUi();
1534
+ }
1535
+ });
1536
+ }
878
1537
 
879
1538
  composer.addEventListener('submit', async function (event) {
880
1539
  event.preventDefault();
@@ -885,13 +1544,14 @@
885
1544
  if (!command) return;
886
1545
 
887
1546
  const submitSession = state.active;
888
- const previousMessages = state.messages.slice();
889
- state.messages = state.messages.concat([{
1547
+ const pendingMessage = {
1548
+ id: createLocalMessageId('local-user'),
890
1549
  role: 'user',
891
1550
  content: command,
892
1551
  timestamp: new Date().toISOString(),
893
1552
  pending: true
894
- }]);
1553
+ };
1554
+ state.messages.push(pendingMessage);
895
1555
  renderMessages(state.messages, { stickToBottom: true });
896
1556
 
897
1557
  state.sending = true;
@@ -899,14 +1559,32 @@
899
1559
  try {
900
1560
  commandInput.value = '';
901
1561
  commandInput.focus();
902
- await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
1562
+ const runResult = await api('/api/sessions/' + encodeURIComponent(submitSession) + '/run', {
903
1563
  method: 'POST',
904
1564
  body: JSON.stringify({ command: command })
905
1565
  });
906
- await loadSessions(submitSession);
1566
+ const pendingIndex = confirmPendingUserMessage(submitSession, pendingMessage.id);
1567
+ if (pendingIndex >= 0 && pendingIndex < state.messageRenderKeys.length) {
1568
+ if (pendingIndex < messagesNode.children.length) {
1569
+ const pendingRow = messagesNode.children[pendingIndex];
1570
+ if (pendingRow && pendingRow.classList.contains('pending')) {
1571
+ pendingRow.classList.remove('pending');
1572
+ }
1573
+ }
1574
+ }
1575
+ appendAssistantMessageLocal(submitSession, runResult);
1576
+ if (state.active === submitSession) {
1577
+ renderMessages(state.messages, { stickToBottom: true });
1578
+ }
1579
+ bumpSessionMetaAfterSend(submitSession);
1580
+ refreshSessionsSilent({ preferredName: submitSession }).catch(function () {
1581
+ // 静默同步失败不打断当前交互
1582
+ });
907
1583
  } catch (e) {
908
1584
  if (state.active === submitSession) {
909
- state.messages = previousMessages;
1585
+ state.messages = state.messages.filter(function (message) {
1586
+ return !(message && message.id === pendingMessage.id);
1587
+ });
910
1588
  renderMessages(state.messages, { stickToBottom: true });
911
1589
  }
912
1590
  alert(e.message);
@@ -999,7 +1677,33 @@
999
1677
  });
1000
1678
  }
1001
1679
 
1680
+ if (configModal) {
1681
+ configModal.addEventListener('click', function (event) {
1682
+ if (event.target === configModal && !state.configSaving) {
1683
+ closeConfigModal();
1684
+ syncUi();
1685
+ }
1686
+ });
1687
+ }
1688
+
1689
+ if (createModal) {
1690
+ createModal.addEventListener('click', function (event) {
1691
+ if (event.target === createModal && !state.createSubmitting) {
1692
+ closeCreateModal();
1693
+ syncUi();
1694
+ }
1695
+ });
1696
+ }
1697
+
1002
1698
  window.addEventListener('keydown', function (event) {
1699
+ if (event.key === 'Escape' && state.configModalOpen) {
1700
+ closeConfigModal();
1701
+ syncUi();
1702
+ }
1703
+ if (event.key === 'Escape' && state.createModalOpen) {
1704
+ closeCreateModal();
1705
+ syncUi();
1706
+ }
1003
1707
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
1004
1708
  closeMobileSessionPanel();
1005
1709
  }