neoctl 0.2.51 → 0.2.53
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/dist/core/query-engine.d.ts +1 -0
- package/dist/core/query-engine.js +6 -0
- package/dist/core/query-engine.js.map +1 -1
- package/dist/web/index.d.ts +15 -2
- package/dist/web/index.js +181 -22
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
package/dist/web/index.js
CHANGED
|
@@ -478,6 +478,66 @@ function parseResumeFlag(value) {
|
|
|
478
478
|
}
|
|
479
479
|
export class WebRuntimeRouter {
|
|
480
480
|
repls = new Map();
|
|
481
|
+
deletedSessions = new Set();
|
|
482
|
+
deletingSessions = new Set();
|
|
483
|
+
assertSessionAvailable(sessionId) {
|
|
484
|
+
if (sessionId && (this.deletedSessions.has(sessionId) || this.deletingSessions.has(sessionId))) {
|
|
485
|
+
throw new Error("会话已删除或正在删除");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async deleteSession(scope, sessionId) {
|
|
489
|
+
if (!sessionId)
|
|
490
|
+
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
491
|
+
if (this.deletingSessions.has(sessionId))
|
|
492
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
493
|
+
if (this.deletedSessions.has(sessionId))
|
|
494
|
+
return actionFailure("SESSION_NOT_FOUND", "会话已删除");
|
|
495
|
+
this.deletingSessions.add(sessionId);
|
|
496
|
+
try {
|
|
497
|
+
const entries = await Promise.all([...this.repls.entries()].map(async ([key, promise]) => ({ key, promise, repl: await promise.catch(() => undefined) })));
|
|
498
|
+
const owners = [...new Set(entries.filter(entry => entry.repl?.currentSessionId() === sessionId).map(entry => entry.repl))];
|
|
499
|
+
// Shared hubs should have one owner. Never orphan a legacy second writer.
|
|
500
|
+
if (owners.length > 1)
|
|
501
|
+
return actionFailure("SESSION_IN_USE", "会话被多个运行实例占用,请先关闭其它窗口");
|
|
502
|
+
// A cold current-session request must restore its owner without passing the deletion gate.
|
|
503
|
+
if (!owners.length && scope.sessionId === sessionId) {
|
|
504
|
+
const key = `session:${sessionId}`;
|
|
505
|
+
const promise = this.createRuntime({ sessionId, resume: true }).then(runtime => this.createRepl(runtime));
|
|
506
|
+
this.repls.set(key, promise);
|
|
507
|
+
try {
|
|
508
|
+
const repl = await promise;
|
|
509
|
+
entries.push({ key, promise, repl });
|
|
510
|
+
owners.push(repl);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
this.repls.delete(key);
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const repl = owners[0] || await this.get(scope);
|
|
518
|
+
if (entries.some(entry => entry.repl?.runningSessionIds().includes(sessionId)))
|
|
519
|
+
return actionFailure("SESSION_RUNNING", "运行中的会话不可删除");
|
|
520
|
+
const result = await repl.deleteSession(sessionId);
|
|
521
|
+
if (!result.ok)
|
|
522
|
+
return result;
|
|
523
|
+
this.deletedSessions.add(sessionId);
|
|
524
|
+
// Retire every cache alias of the deleted writer, then publish only the replacement key.
|
|
525
|
+
if (owners.length) {
|
|
526
|
+
for (const entry of entries)
|
|
527
|
+
if (entry.repl === repl && this.repls.get(entry.key) === entry.promise)
|
|
528
|
+
this.repls.delete(entry.key);
|
|
529
|
+
if (result.replacementSessionId)
|
|
530
|
+
this.repls.set(`session:${result.replacementSessionId}`, Promise.resolve(repl));
|
|
531
|
+
}
|
|
532
|
+
// Publish only after the replacement is routable; SSE clients may reconnect immediately.
|
|
533
|
+
if (owners.length)
|
|
534
|
+
repl.publishSessionDeletion(sessionId, result.replacementSessionId);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
this.deletingSessions.delete(sessionId);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
481
541
|
createRuntime;
|
|
482
542
|
createRepl;
|
|
483
543
|
clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -487,6 +547,7 @@ export class WebRuntimeRouter {
|
|
|
487
547
|
this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
|
|
488
548
|
}
|
|
489
549
|
get(scope = {}) {
|
|
550
|
+
this.assertSessionAvailable(scope.sessionId);
|
|
490
551
|
const key = webRuntimeScopeKey(scope);
|
|
491
552
|
let repl = this.repls.get(key);
|
|
492
553
|
if (!repl) {
|
|
@@ -551,6 +612,10 @@ function webRuntimeScopeKey(scope) {
|
|
|
551
612
|
export class WebRepl {
|
|
552
613
|
runtime;
|
|
553
614
|
demoOptions;
|
|
615
|
+
replCommandsEnabled = true;
|
|
616
|
+
currentSessionId() { return this.runtime.engine.snapshot().session?.sessionId; }
|
|
617
|
+
deletingSession = false;
|
|
618
|
+
sessionMutations = 0;
|
|
554
619
|
pendingContinuation;
|
|
555
620
|
runControl() {
|
|
556
621
|
const execution = this.runtime.engine.getExecutionState?.();
|
|
@@ -896,6 +961,8 @@ export class WebRepl {
|
|
|
896
961
|
await this.refreshToolConfiguration();
|
|
897
962
|
}
|
|
898
963
|
async setSessionTools(value) {
|
|
964
|
+
if (this.deletingSession)
|
|
965
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
899
966
|
if (this.busy)
|
|
900
967
|
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
|
|
901
968
|
if (!isRecord(value))
|
|
@@ -906,6 +973,7 @@ export class WebRepl {
|
|
|
906
973
|
const normalized = this.parseToolModes(value, support);
|
|
907
974
|
if (!normalized.ok)
|
|
908
975
|
return normalized;
|
|
976
|
+
this.sessionMutations++;
|
|
909
977
|
try {
|
|
910
978
|
await this.applySessionToolOverrides(normalized.value, true);
|
|
911
979
|
return { ok: true, state: this.sessionTools() };
|
|
@@ -913,6 +981,9 @@ export class WebRepl {
|
|
|
913
981
|
catch (error) {
|
|
914
982
|
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
915
983
|
}
|
|
984
|
+
finally {
|
|
985
|
+
this.sessionMutations--;
|
|
986
|
+
}
|
|
916
987
|
}
|
|
917
988
|
async loadSessionTools(sessionId) {
|
|
918
989
|
const support = this.runtime.toolSupport;
|
|
@@ -921,6 +992,8 @@ export class WebRepl {
|
|
|
921
992
|
await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
|
|
922
993
|
}
|
|
923
994
|
async setSessionPlugins(value) {
|
|
995
|
+
if (this.deletingSession)
|
|
996
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
924
997
|
if (!isRecord(value))
|
|
925
998
|
return actionFailure("INVALID_REQUEST", "plugin overrides must be an object");
|
|
926
999
|
const support = this.runtime.pluginSupport;
|
|
@@ -938,6 +1011,7 @@ export class WebRepl {
|
|
|
938
1011
|
else if (mode !== "inherit" && mode !== null && mode !== undefined)
|
|
939
1012
|
return actionFailure("PLUGIN_INVALID", `invalid plugin mode: ${id}`);
|
|
940
1013
|
}
|
|
1014
|
+
this.sessionMutations++;
|
|
941
1015
|
try {
|
|
942
1016
|
await this.applySessionPluginOverrides(overrides, true);
|
|
943
1017
|
return { ok: true, state: this.sessionPlugins() };
|
|
@@ -945,6 +1019,9 @@ export class WebRepl {
|
|
|
945
1019
|
catch (error) {
|
|
946
1020
|
return actionFailure("PLUGIN_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
947
1021
|
}
|
|
1022
|
+
finally {
|
|
1023
|
+
this.sessionMutations--;
|
|
1024
|
+
}
|
|
948
1025
|
}
|
|
949
1026
|
async loadSessionPlugins(sessionId) {
|
|
950
1027
|
const support = this.runtime.pluginSupport;
|
|
@@ -1035,7 +1112,9 @@ export class WebRepl {
|
|
|
1035
1112
|
this.publishRuntimeContext();
|
|
1036
1113
|
}
|
|
1037
1114
|
async submit(text, attachments = []) {
|
|
1038
|
-
if (this.
|
|
1115
|
+
if (this.deletingSession)
|
|
1116
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1117
|
+
if (this.runControl().canResume && (!this.replCommandsEnabled || !text.trimStart().startsWith("/") || !text.trim()))
|
|
1039
1118
|
return this.continueRun(text, attachments);
|
|
1040
1119
|
if (this.pendingContinuation)
|
|
1041
1120
|
return { ok: false, error: "续接请求已接收,请勿重复发送" };
|
|
@@ -1046,7 +1125,7 @@ export class WebRepl {
|
|
|
1046
1125
|
const trimmed = text.trim();
|
|
1047
1126
|
if (!trimmed && attachments.length === 0)
|
|
1048
1127
|
return { ok: true };
|
|
1049
|
-
const command = parseReplCommand(text);
|
|
1128
|
+
const command = this.replCommandsEnabled ? parseReplCommand(text) : { type: "input", text };
|
|
1050
1129
|
if (this.busy && command.type === "new") {
|
|
1051
1130
|
await this.detachRunningForeground("new session");
|
|
1052
1131
|
}
|
|
@@ -1063,6 +1142,8 @@ export class WebRepl {
|
|
|
1063
1142
|
return { ok: true };
|
|
1064
1143
|
}
|
|
1065
1144
|
async submitImmediately(text, attachments = []) {
|
|
1145
|
+
if (this.deletingSession)
|
|
1146
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1066
1147
|
if (this.runControl().canResume) {
|
|
1067
1148
|
const result = this.continueRun(text, attachments);
|
|
1068
1149
|
return result.ok ? { ok: true, interrupted: false } : result;
|
|
@@ -1083,6 +1164,8 @@ export class WebRepl {
|
|
|
1083
1164
|
return this.sendQueuedNow();
|
|
1084
1165
|
}
|
|
1085
1166
|
sendQueuedNow() {
|
|
1167
|
+
if (this.deletingSession)
|
|
1168
|
+
return Promise.reject(new Error("会话正在删除"));
|
|
1086
1169
|
if (this.immediateSend)
|
|
1087
1170
|
return this.immediateSend;
|
|
1088
1171
|
const queuedText = this.queuedInput;
|
|
@@ -1121,8 +1204,11 @@ export class WebRepl {
|
|
|
1121
1204
|
return { ...(options ? paginateSessions(sessions, options) : { sessions }), runningSessionIds };
|
|
1122
1205
|
}
|
|
1123
1206
|
async resumeSession(sessionId) {
|
|
1207
|
+
if (this.deletingSession)
|
|
1208
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1124
1209
|
if (!sessionId)
|
|
1125
1210
|
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
1211
|
+
this.sessionMutations++;
|
|
1126
1212
|
try {
|
|
1127
1213
|
const running = this.backgroundSessionRuns.get(sessionId);
|
|
1128
1214
|
if (running) {
|
|
@@ -1144,8 +1230,14 @@ export class WebRepl {
|
|
|
1144
1230
|
const message = error instanceof Error ? error.message : String(error);
|
|
1145
1231
|
return actionFailure("SESSION_RESUME_FAILED", message);
|
|
1146
1232
|
}
|
|
1233
|
+
finally {
|
|
1234
|
+
this.sessionMutations--;
|
|
1235
|
+
}
|
|
1147
1236
|
}
|
|
1148
1237
|
async newSession() {
|
|
1238
|
+
if (this.deletingSession)
|
|
1239
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1240
|
+
this.sessionMutations++;
|
|
1149
1241
|
try {
|
|
1150
1242
|
await this.detachRunningForeground("new session");
|
|
1151
1243
|
this.runtime.engine = this.runtime.engine.forkForSession(undefined, false);
|
|
@@ -1162,8 +1254,11 @@ export class WebRepl {
|
|
|
1162
1254
|
const message = error instanceof Error ? error.message : String(error);
|
|
1163
1255
|
return actionFailure("SESSION_CREATE_FAILED", message);
|
|
1164
1256
|
}
|
|
1257
|
+
finally {
|
|
1258
|
+
this.sessionMutations--;
|
|
1259
|
+
}
|
|
1165
1260
|
}
|
|
1166
|
-
async refreshSessionView() {
|
|
1261
|
+
async refreshSessionView(publish = true) {
|
|
1167
1262
|
const metrics = await this.runtime.engine.contextMetrics();
|
|
1168
1263
|
this.runtime.usage.reset();
|
|
1169
1264
|
this.status = initialStatus(this.runtime, metrics);
|
|
@@ -1175,27 +1270,75 @@ export class WebRepl {
|
|
|
1175
1270
|
this.thinkingLineId = undefined;
|
|
1176
1271
|
this.finalizedThinkingLineId = undefined;
|
|
1177
1272
|
this.liveToolLineIds.clear();
|
|
1178
|
-
|
|
1179
|
-
|
|
1273
|
+
if (publish) {
|
|
1274
|
+
this.broadcastSync();
|
|
1275
|
+
this.publishRuntimeContext();
|
|
1276
|
+
}
|
|
1180
1277
|
}
|
|
1181
1278
|
async deleteSession(sessionId) {
|
|
1182
1279
|
if (!sessionId)
|
|
1183
1280
|
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
1184
|
-
if (this.
|
|
1185
|
-
return actionFailure("
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1281
|
+
if (this.deletingSession)
|
|
1282
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1283
|
+
if (this.sessionMutations)
|
|
1284
|
+
return actionFailure("SESSION_IN_USE", "会话正在保存或切换,请稍后重试");
|
|
1285
|
+
const active = this.runtime.engine.snapshot().session?.sessionId === sessionId;
|
|
1286
|
+
if (this.backgroundSessionRuns.has(sessionId) || (active && (this.busy || this.foregroundRun || this.compactingSession || this.pendingContinuation || this.immediateSend || this.backgroundTaskCount))) {
|
|
1287
|
+
return actionFailure("SESSION_RUNNING", "运行中的会话不可删除");
|
|
1288
|
+
}
|
|
1289
|
+
this.deletingSession = true;
|
|
1290
|
+
const previous = this.runtime.engine;
|
|
1291
|
+
let deleted = false;
|
|
1190
1292
|
try {
|
|
1191
|
-
|
|
1192
|
-
|
|
1293
|
+
if (active) {
|
|
1294
|
+
// Retire the writer before removing files; an uninitialized fork owns no directory.
|
|
1295
|
+
await this.runtimeContextPublishQueue;
|
|
1296
|
+
await previous.settleSessionWrites();
|
|
1297
|
+
await this.demoMessageQueues.get(previous);
|
|
1298
|
+
this.queuedInput = undefined;
|
|
1299
|
+
this.queuedAttachments = undefined;
|
|
1300
|
+
this.pendingContinuation = undefined;
|
|
1301
|
+
this.runtime.engine = previous.forkForSession(undefined, false);
|
|
1302
|
+
activateSession(this.runtime);
|
|
1303
|
+
}
|
|
1304
|
+
deleted = await this.runtime.engine.deleteSession(sessionId);
|
|
1305
|
+
if (!deleted) {
|
|
1306
|
+
if (active) {
|
|
1307
|
+
this.runtime.engine = previous;
|
|
1308
|
+
activateSession(this.runtime);
|
|
1309
|
+
}
|
|
1193
1310
|
return actionFailure("SESSION_NOT_FOUND", `session not found: ${sessionId}`);
|
|
1194
|
-
|
|
1311
|
+
}
|
|
1312
|
+
if (!active)
|
|
1313
|
+
return { ok: true };
|
|
1314
|
+
await this.runtime.engine.initialize();
|
|
1315
|
+
activateSession(this.runtime);
|
|
1316
|
+
const replacementSessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
1317
|
+
if (!replacementSessionId)
|
|
1318
|
+
throw new Error("新会话初始化失败");
|
|
1319
|
+
await Promise.all([this.loadSessionPlugins(replacementSessionId), this.loadSessionTools(replacementSessionId)]);
|
|
1320
|
+
await this.refreshSessionView(false);
|
|
1321
|
+
return { ok: true, replacementSessionId };
|
|
1195
1322
|
}
|
|
1196
1323
|
catch (error) {
|
|
1197
|
-
|
|
1198
|
-
|
|
1324
|
+
if (active && !deleted) {
|
|
1325
|
+
this.runtime.engine = previous;
|
|
1326
|
+
activateSession(this.runtime);
|
|
1327
|
+
}
|
|
1328
|
+
if (active && deleted) {
|
|
1329
|
+
// A deletion cannot be rolled back; never restore the deleted writer.
|
|
1330
|
+
return { ok: true };
|
|
1331
|
+
}
|
|
1332
|
+
return actionFailure("SESSION_DELETE_FAILED", error instanceof Error ? error.message : String(error));
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
this.deletingSession = false;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
publishSessionDeletion(sessionId, replacementSessionId) {
|
|
1339
|
+
for (const subscriber of this.subscribers) {
|
|
1340
|
+
this.send(subscriber, replacementSessionId ? "session.replaced" : "session.deleted", replacementSessionId
|
|
1341
|
+
? { deletedSessionId: sessionId, sessionId: replacementSessionId } : { sessionId });
|
|
1199
1342
|
}
|
|
1200
1343
|
}
|
|
1201
1344
|
appPrompt() {
|
|
@@ -1274,6 +1417,8 @@ export class WebRepl {
|
|
|
1274
1417
|
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
1275
1418
|
}
|
|
1276
1419
|
async setFastMode(enabled) {
|
|
1420
|
+
if (this.deletingSession)
|
|
1421
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1277
1422
|
const engine = this.runtime.engine;
|
|
1278
1423
|
try {
|
|
1279
1424
|
const { deferred } = await engine.updateSessionSettings({ fastMode: enabled });
|
|
@@ -1286,6 +1431,8 @@ export class WebRepl {
|
|
|
1286
1431
|
}
|
|
1287
1432
|
}
|
|
1288
1433
|
async setContextWindowK(value) {
|
|
1434
|
+
if (this.deletingSession)
|
|
1435
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1289
1436
|
if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0 || !Number.isSafeInteger(Number(value) * 1000)) {
|
|
1290
1437
|
return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
|
|
1291
1438
|
}
|
|
@@ -1305,6 +1452,8 @@ export class WebRepl {
|
|
|
1305
1452
|
}
|
|
1306
1453
|
}
|
|
1307
1454
|
async setSessionModel(model, reasoning) {
|
|
1455
|
+
if (this.deletingSession)
|
|
1456
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1308
1457
|
if (typeof model !== "string" || !model.trim() || model.length > 256 || /[\s\x00-\x1f]/u.test(model.trim())) {
|
|
1309
1458
|
return actionFailure("MODEL_INVALID", "invalid model");
|
|
1310
1459
|
}
|
|
@@ -1337,6 +1486,8 @@ export class WebRepl {
|
|
|
1337
1486
|
return readSessionPrompt(this.runtime.engine);
|
|
1338
1487
|
}
|
|
1339
1488
|
async saveSessionPrompt(body) {
|
|
1489
|
+
if (this.deletingSession)
|
|
1490
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1340
1491
|
const engine = this.runtime.engine;
|
|
1341
1492
|
const result = await updateSessionPrompt(engine, body);
|
|
1342
1493
|
if (this.runtime.engine === engine) {
|
|
@@ -1348,6 +1499,8 @@ export class WebRepl {
|
|
|
1348
1499
|
return result;
|
|
1349
1500
|
}
|
|
1350
1501
|
async compactSession() {
|
|
1502
|
+
if (this.deletingSession)
|
|
1503
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1351
1504
|
const engine = this.runtime.engine;
|
|
1352
1505
|
if (this.isDemo())
|
|
1353
1506
|
return actionFailure("DEMO_SCRIPTED", "演示会话由后台安排压缩");
|
|
@@ -1484,6 +1637,8 @@ export class WebRepl {
|
|
|
1484
1637
|
return { ok: true };
|
|
1485
1638
|
}
|
|
1486
1639
|
continueRun(text = "", attachments = []) {
|
|
1640
|
+
if (this.deletingSession)
|
|
1641
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1487
1642
|
if (!this.runControl().canResume)
|
|
1488
1643
|
return actionFailure("INVALID_REQUEST", "没有可恢复的执行,或续接请求已经提交");
|
|
1489
1644
|
const useQueue = !text.trim() && attachments.length === 0 && this.queuedInput !== undefined;
|
|
@@ -2173,7 +2328,7 @@ export class WebRepl {
|
|
|
2173
2328
|
return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
|
|
2174
2329
|
}
|
|
2175
2330
|
async handleCommandOrPrompt(text, attachments = [], continuationOnly = false) {
|
|
2176
|
-
const command = parseReplCommand(text);
|
|
2331
|
+
const command = this.replCommandsEnabled ? parseReplCommand(text) : { type: "input", text };
|
|
2177
2332
|
if (command.type === "exit") {
|
|
2178
2333
|
this.append(systemLine("neo web server is still running. Close this tab or stop the server process with Ctrl+C."));
|
|
2179
2334
|
return;
|
|
@@ -2266,7 +2421,7 @@ export class WebRepl {
|
|
|
2266
2421
|
this.append({ kind: "error", text: "/secret is currently available in the interactive REPL only." });
|
|
2267
2422
|
return;
|
|
2268
2423
|
}
|
|
2269
|
-
if (text.trimStart().startsWith("/")) {
|
|
2424
|
+
if (this.replCommandsEnabled && text.trimStart().startsWith("/")) {
|
|
2270
2425
|
this.append({ kind: "error", text: `Unknown or incomplete command: ${text.trim()}\nType /help for commands.` });
|
|
2271
2426
|
return;
|
|
2272
2427
|
}
|
|
@@ -2440,6 +2595,8 @@ export class WebRepl {
|
|
|
2440
2595
|
});
|
|
2441
2596
|
}
|
|
2442
2597
|
publishRuntimeContext(targets) {
|
|
2598
|
+
if (this.deletingSession)
|
|
2599
|
+
return;
|
|
2443
2600
|
this.runtimeContextPublishQueue = this.runtimeContextPublishQueue.then(async () => {
|
|
2444
2601
|
const revision = ++this.runtimeContextRevision;
|
|
2445
2602
|
const payload = await this.buildRuntimeContext(revision);
|
|
@@ -2528,6 +2685,8 @@ export class WebRepl {
|
|
|
2528
2685
|
this.send(subscriber, "terminal.output", { updates });
|
|
2529
2686
|
}
|
|
2530
2687
|
broadcastSync() {
|
|
2688
|
+
if (this.deletingSession)
|
|
2689
|
+
return;
|
|
2531
2690
|
this.discardPendingDeltaOperations();
|
|
2532
2691
|
const payload = this.snapshot(false);
|
|
2533
2692
|
for (const subscriber of this.subscribers)
|
|
@@ -2590,6 +2749,10 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2590
2749
|
if (req.method === "GET" && url.pathname === "/api/sessions/running")
|
|
2591
2750
|
return sendJson(res, { runningSessionIds: await router.runningSessionIds() });
|
|
2592
2751
|
const scope = webRuntimeScopeFromUrl(url);
|
|
2752
|
+
if (req.method === "POST" && url.pathname === "/api/sessions/delete") {
|
|
2753
|
+
const body = await readJsonBody(req);
|
|
2754
|
+
return sendJson(res, await router.deleteSession(scope, String(body.sessionId ?? "")));
|
|
2755
|
+
}
|
|
2593
2756
|
const repl = await router.get(scope);
|
|
2594
2757
|
if (req.method === "GET" && url.pathname === "/events")
|
|
2595
2758
|
return repl.subscribe(res, router.clientInfo());
|
|
@@ -2800,10 +2963,6 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2800
2963
|
}
|
|
2801
2964
|
if (req.method === "POST" && url.pathname === "/api/sessions/new")
|
|
2802
2965
|
return sendJson(res, await repl.newSession());
|
|
2803
|
-
if (req.method === "POST" && url.pathname === "/api/sessions/delete") {
|
|
2804
|
-
const body = await readJsonBody(req);
|
|
2805
|
-
return sendJson(res, await repl.deleteSession(String(body.sessionId ?? "")));
|
|
2806
|
-
}
|
|
2807
2966
|
if (req.method === "GET" && url.pathname === "/api/login")
|
|
2808
2967
|
return sendJson(res, repl.loginForm(url.searchParams.get("provider") ?? undefined));
|
|
2809
2968
|
if (req.method === "POST" && url.pathname === "/api/login") {
|