@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.
- package/README.md +3 -3
- package/bin/manyoyo.js +13 -3
- package/config.example.json +1 -1
- package/docker/manyoyo.Dockerfile +36 -39
- package/lib/agent-resume.js +14 -1
- package/lib/image-build.js +128 -1
- package/lib/web/frontend/app.css +420 -190
- package/lib/web/frontend/app.html +71 -4
- package/lib/web/frontend/app.js +840 -136
- package/lib/web/frontend/login.css +77 -63
- package/lib/web/server.js +609 -40
- package/package.json +2 -2
package/lib/web/frontend/app.js
CHANGED
|
@@ -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
|
|
160
|
-
const
|
|
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
|
-
|
|
409
|
+
lines.push({ className: 'msg-meta-time', text: timeText });
|
|
164
410
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
779
|
-
state.
|
|
780
|
-
renderSessions();
|
|
781
|
-
syncUi();
|
|
1280
|
+
function applySessionsSnapshot(rawSessions, preferredName) {
|
|
1281
|
+
state.sessions = Array.isArray(rawSessions) ? rawSessions : [];
|
|
782
1282
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
789
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1298
|
+
async function refreshSessions(options) {
|
|
1299
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
1300
|
+
const withLoading = opts.withLoading !== false;
|
|
795
1301
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1302
|
+
if (withLoading) {
|
|
1303
|
+
state.loadingSessions = true;
|
|
1304
|
+
renderSessions();
|
|
1305
|
+
syncUi();
|
|
1306
|
+
}
|
|
799
1307
|
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1331
|
+
if (opts.reloadMessages) {
|
|
1332
|
+
await loadMessagesForSession(state.active, { silent: false });
|
|
1333
|
+
}
|
|
821
1334
|
}
|
|
822
1335
|
|
|
823
|
-
async function
|
|
824
|
-
|
|
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.
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
846
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
889
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|