neoctl 0.2.45 → 0.2.46
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/context/compaction.d.ts +5 -0
- package/dist/context/compaction.js +4 -3
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/prompts.js +2 -0
- package/dist/context/prompts.js.map +1 -1
- package/dist/core/abortable-stream.d.ts +2 -0
- package/dist/core/abortable-stream.js +35 -0
- package/dist/core/abortable-stream.js.map +1 -0
- package/dist/core/compaction-progress.d.ts +5 -0
- package/dist/core/compaction-progress.js +36 -0
- package/dist/core/compaction-progress.js.map +1 -0
- package/dist/core/execution-state.d.ts +10 -0
- package/dist/core/execution-state.js +18 -0
- package/dist/core/execution-state.js.map +1 -0
- package/dist/core/image-registry.d.ts +4 -2
- package/dist/core/image-registry.js +32 -7
- package/dist/core/image-registry.js.map +1 -1
- package/dist/core/image-storage.js +4 -0
- package/dist/core/image-storage.js.map +1 -1
- package/dist/core/query-engine.d.ts +25 -0
- package/dist/core/query-engine.js +142 -61
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +5 -1
- package/dist/core/query.js +35 -20
- package/dist/core/query.js.map +1 -1
- package/dist/core/session-images.d.ts +52 -0
- package/dist/core/session-images.js +395 -0
- package/dist/core/session-images.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/openai-mappers.js +0 -2
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/repl/index.js +2 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-store.d.ts +9 -0
- package/dist/session/session-store.js +18 -2
- package/dist/session/session-store.js.map +1 -1
- package/dist/tools/builtins/exec-process-manager.d.ts +2 -0
- package/dist/tools/builtins/exec-process-manager.js +24 -1
- package/dist/tools/builtins/exec-process-manager.js.map +1 -1
- package/dist/tools/builtins/framework-feedback-tool.d.ts +26 -0
- package/dist/tools/builtins/framework-feedback-tool.js +97 -0
- package/dist/tools/builtins/framework-feedback-tool.js.map +1 -0
- package/dist/tools/builtins/image-generation-tool.d.ts +2 -0
- package/dist/tools/builtins/image-generation-tool.js +31 -141
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/tools/builtins/image-loader-tool.d.ts +6 -1
- package/dist/tools/builtins/image-loader-tool.js +48 -26
- package/dist/tools/builtins/image-loader-tool.js.map +1 -1
- package/dist/tools/builtins/image-note-tool.js +2 -2
- package/dist/tools/builtins/image-note-tool.js.map +1 -1
- package/dist/tools/run-tool-use.js +3 -2
- package/dist/tools/run-tool-use.js.map +1 -1
- package/dist/tools/tool-catalog.d.ts +1 -0
- package/dist/tools/tool-catalog.js +2 -0
- package/dist/tools/tool-catalog.js.map +1 -1
- package/dist/tools/tool.d.ts +1 -0
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/messages.d.ts +3 -1
- package/dist/types/messages.js.map +1 -1
- package/dist/web/demo-mode.d.ts +37 -0
- package/dist/web/demo-mode.js +141 -0
- package/dist/web/demo-mode.js.map +1 -0
- package/dist/web/demo-tools.d.ts +37 -0
- package/dist/web/demo-tools.js +259 -0
- package/dist/web/demo-tools.js.map +1 -0
- package/dist/web/demo-video.d.ts +1 -0
- package/dist/web/demo-video.js +3 -0
- package/dist/web/demo-video.js.map +1 -0
- package/dist/web/index.d.ts +43 -2
- package/dist/web/index.js +269 -9
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
package/dist/web/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { DemoMode, DEMO_MARKER, DEMO_PROMPT } from "./demo-mode.js";
|
|
3
|
+
import { DemoTools } from "./demo-tools.js";
|
|
4
|
+
import { createTextMessage } from "../types/messages.js";
|
|
2
5
|
import { imageResultMetadata } from "./image-result-metadata.js";
|
|
3
6
|
import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
|
|
4
7
|
import { handlePromptConfigRequest } from "./prompt-config-protocol.js";
|
|
@@ -26,6 +29,7 @@ import { ExecProcessManager } from "../tools/builtins/exec-process-manager.js";
|
|
|
26
29
|
import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
|
|
27
30
|
import { grepTool } from "../tools/builtins/grep-tool.js";
|
|
28
31
|
import { searchTool } from "../tools/builtins/search-tool.js";
|
|
32
|
+
import { createFrameworkFeedbackTool, FrameworkFeedbackStore } from "../tools/builtins/framework-feedback-tool.js";
|
|
29
33
|
import { planTool } from "../tools/builtins/plan-tool.js";
|
|
30
34
|
import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
|
|
31
35
|
import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
|
|
@@ -165,6 +169,8 @@ export async function createWebRuntime(options = {}) {
|
|
|
165
169
|
if (modelConfig?.provider === "openai")
|
|
166
170
|
registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
|
|
167
171
|
registerTool(planTool, { source: "builtin" });
|
|
172
|
+
const frameworkFeedback = new FrameworkFeedbackStore(options.frameworkFeedbackDir);
|
|
173
|
+
registerTool(createFrameworkFeedbackTool(frameworkFeedback), { source: "builtin" });
|
|
168
174
|
for (const tool of options.externalTools ?? [])
|
|
169
175
|
registerTool(tool, { source: "external" });
|
|
170
176
|
for (const plugin of pluginCatalog) {
|
|
@@ -232,6 +238,7 @@ export async function createWebRuntime(options = {}) {
|
|
|
232
238
|
activateSession({ engine, taskStore });
|
|
233
239
|
const initialMetrics = await engine.contextMetrics();
|
|
234
240
|
runtime = {
|
|
241
|
+
frameworkFeedback,
|
|
235
242
|
engine,
|
|
236
243
|
appPromptStore,
|
|
237
244
|
communicationLogger,
|
|
@@ -526,6 +533,24 @@ function webRuntimeScopeKey(scope) {
|
|
|
526
533
|
}
|
|
527
534
|
export class WebRepl {
|
|
528
535
|
runtime;
|
|
536
|
+
demoOptions;
|
|
537
|
+
pendingContinuation;
|
|
538
|
+
runControl() {
|
|
539
|
+
const execution = this.runtime.engine.getExecutionState?.();
|
|
540
|
+
const resumable = (execution?.status === "paused" || execution?.status === "interrupted")
|
|
541
|
+
&& (!this.activeAbortController || this.activeAbortController.signal.aborted);
|
|
542
|
+
const waiting = this.pendingContinuation?.engine === this.runtime.engine;
|
|
543
|
+
return {
|
|
544
|
+
state: waiting ? "resuming" : resumable ? execution.status : this.busy ? "running" : "idle",
|
|
545
|
+
canPause: !waiting && !resumable && !this.stoppingRun && !this.immediateSend && Boolean(this.activeAbortController),
|
|
546
|
+
canResume: resumable && !waiting && !this.immediateSend,
|
|
547
|
+
reason: execution?.reason,
|
|
548
|
+
timer: execution?.elapsedMs === undefined ? undefined : {
|
|
549
|
+
elapsedMs: execution.elapsedMs,
|
|
550
|
+
running: execution.status === "running" && !waiting,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
529
554
|
agentContentReader = createAgentContentDetailResolver();
|
|
530
555
|
subscribers = new Set();
|
|
531
556
|
eventSequence = 0;
|
|
@@ -544,6 +569,7 @@ export class WebRepl {
|
|
|
544
569
|
lines;
|
|
545
570
|
status;
|
|
546
571
|
busy = false;
|
|
572
|
+
compactingSession;
|
|
547
573
|
queuedInput;
|
|
548
574
|
queuedAttachments;
|
|
549
575
|
foregroundRun;
|
|
@@ -556,8 +582,91 @@ export class WebRepl {
|
|
|
556
582
|
runtimeContextPublishQueue = Promise.resolve();
|
|
557
583
|
backgroundSessionRuns = new Map();
|
|
558
584
|
suppressReattachedStreaming = new Set();
|
|
559
|
-
|
|
585
|
+
demoSessions = new WeakMap();
|
|
586
|
+
demoStatus = new WeakMap();
|
|
587
|
+
demoMessageQueues = new WeakMap();
|
|
588
|
+
demoStarting = false;
|
|
589
|
+
isDemo(engine = this.runtime.engine) {
|
|
590
|
+
const session = engine.snapshot().session;
|
|
591
|
+
const key = `${session?.sessionId}:${session?.firstWindowId}`;
|
|
592
|
+
const cached = this.demoStatus.get(engine);
|
|
593
|
+
if (cached?.key === key)
|
|
594
|
+
return cached.active || this.demoSessions.has(engine);
|
|
595
|
+
if (cached)
|
|
596
|
+
this.demoSessions.delete(engine);
|
|
597
|
+
const active = engine.getDisplayEntries().some(entry => entry.type === "message" && entry.message.metadata?.demoMode === DEMO_MARKER);
|
|
598
|
+
this.demoStatus.set(engine, { key, active });
|
|
599
|
+
if (active)
|
|
600
|
+
this.demoSessions.set(engine, null);
|
|
601
|
+
return active;
|
|
602
|
+
}
|
|
603
|
+
async startDemo(expectedSessionId) {
|
|
604
|
+
const engine = this.runtime.engine;
|
|
605
|
+
if (!expectedSessionId || expectedSessionId !== engine.snapshot().session?.sessionId)
|
|
606
|
+
return actionFailure("SESSION_MISMATCH", "会话已切换");
|
|
607
|
+
if (this.demoStarting || this.isDemo() || this.busy || this.foregroundRun || this.stoppingRun || this.queuedInput !== undefined
|
|
608
|
+
|| this.backgroundTaskCount || engine.getDisplayEntries().length || this.lines.some(line => line.kind !== "system")) {
|
|
609
|
+
return actionFailure("DEMO_REQUIRES_EMPTY_SESSION", "仅空闲的空会话可以进入演示模式");
|
|
610
|
+
}
|
|
611
|
+
// Reserve synchronously before any await: double clicks and concurrent submits cannot start a second run.
|
|
612
|
+
this.demoStarting = true;
|
|
613
|
+
this.busy = true;
|
|
614
|
+
let demo;
|
|
615
|
+
try {
|
|
616
|
+
demo = new DemoMode(new DemoTools(this.runtime.tools, this.runtime.taskStore, this.runtime.execProcessManager), this.demoOptions.wait);
|
|
617
|
+
this.demoSessions.set(engine, demo);
|
|
618
|
+
const marker = createTextMessage("system", "");
|
|
619
|
+
marker.isMeta = true;
|
|
620
|
+
marker.metadata = { demoMode: DEMO_MARKER };
|
|
621
|
+
await engine.recordDisplayMessage(marker);
|
|
622
|
+
await demo.backend.prepare(engine.snapshot().session.sessionDir);
|
|
623
|
+
demo.controller.signal.throwIfAborted();
|
|
624
|
+
engine.setSimulation(demo);
|
|
625
|
+
this.startRun(DEMO_PROMPT);
|
|
626
|
+
return { ok: true };
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
demo?.stop();
|
|
630
|
+
this.busy = false;
|
|
631
|
+
this.broadcastSync();
|
|
632
|
+
return actionFailure("DEMO_START_FAILED", error instanceof Error ? error.message : String(error));
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
this.demoStarting = false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async recordDemoInput(text, attachments) {
|
|
639
|
+
const engine = this.runtime.engine;
|
|
640
|
+
if (!text.trim() && attachments.length === 0)
|
|
641
|
+
return { ok: true };
|
|
642
|
+
const previous = this.demoMessageQueues.get(engine) ?? Promise.resolve();
|
|
643
|
+
const pending = previous.catch(() => undefined).then(async () => {
|
|
644
|
+
const payload = buildWebPromptPayload(text, attachments);
|
|
645
|
+
const message = createTextMessage("user", payload.text);
|
|
646
|
+
if (payload.blocks)
|
|
647
|
+
message.blocks = payload.blocks;
|
|
648
|
+
const stored = await engine.recordDisplayMessage(message);
|
|
649
|
+
if (this.runtime.engine === engine) {
|
|
650
|
+
renderMessage(stored, line => this.append(line));
|
|
651
|
+
this.broadcastSync();
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
this.demoMessageQueues.set(engine, pending);
|
|
655
|
+
try {
|
|
656
|
+
await pending;
|
|
657
|
+
return { ok: true };
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
if (this.demoMessageQueues.get(engine) === pending)
|
|
664
|
+
this.demoMessageQueues.delete(engine);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
constructor(runtime, demoOptions = {}) {
|
|
560
668
|
this.runtime = runtime;
|
|
669
|
+
this.demoOptions = demoOptions;
|
|
561
670
|
const lineId = { current: 0 };
|
|
562
671
|
this.lines = initialLines(runtime, lineId);
|
|
563
672
|
this.lineId = lineId.current;
|
|
@@ -640,7 +749,9 @@ export class WebRepl {
|
|
|
640
749
|
return {
|
|
641
750
|
lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
|
|
642
751
|
status: this.timedStatus(),
|
|
643
|
-
busy: this.busy,
|
|
752
|
+
busy: this.runControl().canResume ? false : this.busy,
|
|
753
|
+
runControl: this.runControl(),
|
|
754
|
+
demoMode: this.isDemo(),
|
|
644
755
|
queuedInput: this.queuedInput,
|
|
645
756
|
backgroundTaskCount: backgroundTasks.length,
|
|
646
757
|
backgroundTasks,
|
|
@@ -894,6 +1005,12 @@ export class WebRepl {
|
|
|
894
1005
|
this.publishRuntimeContext();
|
|
895
1006
|
}
|
|
896
1007
|
async submit(text, attachments = []) {
|
|
1008
|
+
if (this.runControl().canResume && (!text.trimStart().startsWith("/") || !text.trim()))
|
|
1009
|
+
return this.continueRun(text, attachments);
|
|
1010
|
+
if (this.pendingContinuation)
|
|
1011
|
+
return { ok: false, error: "续接请求已接收,请勿重复发送" };
|
|
1012
|
+
if (this.isDemo())
|
|
1013
|
+
return this.recordDemoInput(text, attachments);
|
|
897
1014
|
if (this.stoppingRun || this.immediateSend)
|
|
898
1015
|
return { ok: false, error: "正在停止上一轮请求,请稍后发送" };
|
|
899
1016
|
const trimmed = text.trim();
|
|
@@ -916,6 +1033,14 @@ export class WebRepl {
|
|
|
916
1033
|
return { ok: true };
|
|
917
1034
|
}
|
|
918
1035
|
async submitImmediately(text, attachments = []) {
|
|
1036
|
+
if (this.runControl().canResume) {
|
|
1037
|
+
const result = this.continueRun(text, attachments);
|
|
1038
|
+
return result.ok ? { ok: true, interrupted: false } : result;
|
|
1039
|
+
}
|
|
1040
|
+
if (this.isDemo()) {
|
|
1041
|
+
const result = await this.recordDemoInput(text, attachments);
|
|
1042
|
+
return result.ok ? { ok: true, interrupted: false } : result;
|
|
1043
|
+
}
|
|
919
1044
|
if (this.immediateSend)
|
|
920
1045
|
return { ok: false, error: "正在发送,请勿重复提交" };
|
|
921
1046
|
if (this.stoppingRun)
|
|
@@ -948,8 +1073,8 @@ export class WebRepl {
|
|
|
948
1073
|
this.queuedInput = mergeWebQueuedInput(this.queuedInput, text);
|
|
949
1074
|
this.queuedAttachments = [...(this.queuedAttachments ?? []), ...attachments].slice(0, 20);
|
|
950
1075
|
}
|
|
951
|
-
startRun(text, attachments = []) {
|
|
952
|
-
const run = this.handleCommandOrPrompt(text, attachments).catch((error) => {
|
|
1076
|
+
startRun(text, attachments = [], continuationOnly = false) {
|
|
1077
|
+
const run = this.handleCommandOrPrompt(text, attachments, continuationOnly).catch((error) => {
|
|
953
1078
|
this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
954
1079
|
this.setBusy(false);
|
|
955
1080
|
this.setStatus({ ...this.status, phase: "ready", detail: undefined });
|
|
@@ -1186,9 +1311,25 @@ export class WebRepl {
|
|
|
1186
1311
|
}
|
|
1187
1312
|
async compactSession() {
|
|
1188
1313
|
const engine = this.runtime.engine;
|
|
1314
|
+
if (this.isDemo())
|
|
1315
|
+
return actionFailure("DEMO_SCRIPTED", "演示会话由后台安排压缩");
|
|
1316
|
+
if (this.compactingSession === engine)
|
|
1317
|
+
return actionFailure("COMPACTION_IN_PROGRESS", "Context compaction is already running");
|
|
1318
|
+
const showProgress = !this.busy;
|
|
1319
|
+
const runToken = this.foregroundRunToken;
|
|
1320
|
+
let finishForeground;
|
|
1321
|
+
const foreground = showProgress ? new Promise(resolve => { finishForeground = resolve; }) : undefined;
|
|
1322
|
+
if (foreground)
|
|
1323
|
+
this.foregroundRun = foreground;
|
|
1324
|
+
if (showProgress) {
|
|
1325
|
+
this.compactingSession = engine;
|
|
1326
|
+
this.busy = true;
|
|
1327
|
+
this.setStatus({ ...this.status, phase: "compacting", detail: "manualcompact: generating context summary", activityTick: this.status.activityTick + 1 });
|
|
1328
|
+
}
|
|
1189
1329
|
try {
|
|
1330
|
+
// An idle click executes now through the settings barrier, not the chat/input queue.
|
|
1190
1331
|
const result = await engine.updateSessionSettings({ compact: true });
|
|
1191
|
-
if (this.runtime.engine === engine) {
|
|
1332
|
+
if (this.runtime.engine === engine && this.foregroundRunToken === runToken) {
|
|
1192
1333
|
if (result.compaction?.report)
|
|
1193
1334
|
this.appendCompaction(result.compaction.report);
|
|
1194
1335
|
if (!result.deferred)
|
|
@@ -1200,6 +1341,24 @@ export class WebRepl {
|
|
|
1200
1341
|
catch (error) {
|
|
1201
1342
|
return actionFailure("COMPACTION_FAILED", error instanceof Error ? error.message : String(error));
|
|
1202
1343
|
}
|
|
1344
|
+
finally {
|
|
1345
|
+
if (this.compactingSession === engine)
|
|
1346
|
+
this.compactingSession = undefined;
|
|
1347
|
+
if (this.foregroundRun === foreground)
|
|
1348
|
+
this.foregroundRun = undefined;
|
|
1349
|
+
finishForeground?.();
|
|
1350
|
+
if (showProgress && this.runtime.engine === engine && this.foregroundRunToken === runToken) {
|
|
1351
|
+
this.busy = false;
|
|
1352
|
+
this.setStatus({ ...this.status, phase: "ready", detail: undefined, activityTick: this.status.activityTick + 1 });
|
|
1353
|
+
const queued = this.queuedInput;
|
|
1354
|
+
const attachments = this.queuedAttachments ?? [];
|
|
1355
|
+
if (queued !== undefined) {
|
|
1356
|
+
this.queuedInput = undefined;
|
|
1357
|
+
this.queuedAttachments = undefined;
|
|
1358
|
+
this.startRun(queued, attachments);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1203
1362
|
}
|
|
1204
1363
|
loginForm(providerValue) {
|
|
1205
1364
|
const provider = parseLoginProvider(providerValue);
|
|
@@ -1244,9 +1403,72 @@ export class WebRepl {
|
|
|
1244
1403
|
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
1245
1404
|
this.publishRuntimeContext();
|
|
1246
1405
|
}
|
|
1406
|
+
async listFrameworkFeedback(page, pageSize) {
|
|
1407
|
+
if (!this.runtime.frameworkFeedback)
|
|
1408
|
+
throw new Error("Framework feedback storage unavailable");
|
|
1409
|
+
return this.runtime.frameworkFeedback.list(page, pageSize);
|
|
1410
|
+
}
|
|
1411
|
+
async deleteFrameworkFeedback(id) {
|
|
1412
|
+
if (typeof id !== "string" || !/^\d{13}-[a-f0-9-]{36}$/.test(id))
|
|
1413
|
+
return actionFailure("INVALID_REQUEST", "建议编号无效");
|
|
1414
|
+
try {
|
|
1415
|
+
if (!this.runtime.frameworkFeedback)
|
|
1416
|
+
throw new Error("unavailable");
|
|
1417
|
+
return { ok: true, deleted: await this.runtime.frameworkFeedback.delete(id) };
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
return actionFailure("FEEDBACK_DELETE_FAILED", "删除失败,请重试");
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
pauseRun() {
|
|
1424
|
+
if (this.runControl().canResume)
|
|
1425
|
+
return { ok: true };
|
|
1426
|
+
if (!this.runControl().canPause)
|
|
1427
|
+
return actionFailure("INVALID_REQUEST", "当前没有可暂停的会话执行");
|
|
1428
|
+
this.runtime.engine.markExecutionPaused();
|
|
1429
|
+
const queuedText = this.queuedInput;
|
|
1430
|
+
const queuedAttachments = this.queuedAttachments;
|
|
1431
|
+
// Do not await cooperative cancellation: the UI must stop immediately.
|
|
1432
|
+
void this.stopForegroundRun("用户暂停了会话").catch(() => this.broadcastSync());
|
|
1433
|
+
this.queuedInput = queuedText;
|
|
1434
|
+
this.queuedAttachments = queuedAttachments;
|
|
1435
|
+
this.status = { ...this.status, phase: "ready", detail: "paused", currentTool: undefined, retryCooldownUntil: undefined };
|
|
1436
|
+
this.broadcastSync();
|
|
1437
|
+
return { ok: true };
|
|
1438
|
+
}
|
|
1439
|
+
continueRun(text = "", attachments = []) {
|
|
1440
|
+
if (!this.runControl().canResume)
|
|
1441
|
+
return actionFailure("INVALID_REQUEST", "没有可恢复的执行,或续接请求已经提交");
|
|
1442
|
+
const useQueue = !text.trim() && attachments.length === 0 && this.queuedInput !== undefined;
|
|
1443
|
+
const request = { engine: this.runtime.engine,
|
|
1444
|
+
text: text || (useQueue ? this.queuedInput : ""),
|
|
1445
|
+
attachments: useQueue ? this.queuedAttachments ?? [] : attachments };
|
|
1446
|
+
this.queuedInput = undefined;
|
|
1447
|
+
this.queuedAttachments = undefined;
|
|
1448
|
+
this.pendingContinuation = request;
|
|
1449
|
+
this.broadcastSync();
|
|
1450
|
+
// Accept immediately, but never overlap two queries on the same mutable engine.
|
|
1451
|
+
void (async () => {
|
|
1452
|
+
try {
|
|
1453
|
+
await this.stoppingRun;
|
|
1454
|
+
await this.foregroundRun;
|
|
1455
|
+
if (this.pendingContinuation !== request || this.runtime.engine !== request.engine)
|
|
1456
|
+
return;
|
|
1457
|
+
this.pendingContinuation = undefined;
|
|
1458
|
+
this.startRun(request.text, request.attachments, !request.text.trim() && request.attachments.length === 0);
|
|
1459
|
+
}
|
|
1460
|
+
finally {
|
|
1461
|
+
if (this.pendingContinuation === request)
|
|
1462
|
+
this.pendingContinuation = undefined;
|
|
1463
|
+
this.broadcastSync();
|
|
1464
|
+
}
|
|
1465
|
+
})().catch(error => this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) }));
|
|
1466
|
+
return { ok: true };
|
|
1467
|
+
}
|
|
1247
1468
|
async interrupt() {
|
|
1248
1469
|
if (this.immediateSend)
|
|
1249
1470
|
await this.immediateSend;
|
|
1471
|
+
this.demoSessions.get(this.runtime.engine)?.stop();
|
|
1250
1472
|
const interrupted = await this.stopForegroundRun("Interrupted from neo web");
|
|
1251
1473
|
return { ok: true, interrupted };
|
|
1252
1474
|
}
|
|
@@ -1451,6 +1673,14 @@ export class WebRepl {
|
|
|
1451
1673
|
.sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
|
|
1452
1674
|
}
|
|
1453
1675
|
async detachRunningForeground(reason) {
|
|
1676
|
+
if (this.pendingContinuation || this.stoppingRun)
|
|
1677
|
+
throw new Error("上一轮请求正在取消,请稍后切换会话");
|
|
1678
|
+
if (this.demoStarting)
|
|
1679
|
+
throw new Error("演示模式正在启动");
|
|
1680
|
+
if (this.isDemo() && this.busy) {
|
|
1681
|
+
this.demoSessions.get(this.runtime.engine)?.stop();
|
|
1682
|
+
return this.stopForegroundRun(reason);
|
|
1683
|
+
}
|
|
1454
1684
|
if (!this.busy)
|
|
1455
1685
|
return false;
|
|
1456
1686
|
const snapshot = this.runtime.engine.snapshot().session;
|
|
@@ -1818,7 +2048,7 @@ export class WebRepl {
|
|
|
1818
2048
|
.map((block) => block.imageId);
|
|
1819
2049
|
return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
|
|
1820
2050
|
}
|
|
1821
|
-
async handleCommandOrPrompt(text, attachments = []) {
|
|
2051
|
+
async handleCommandOrPrompt(text, attachments = [], continuationOnly = false) {
|
|
1822
2052
|
const command = parseReplCommand(text);
|
|
1823
2053
|
if (command.type === "exit") {
|
|
1824
2054
|
this.append(systemLine("neo web server is still running. Close this tab or stop the server process with Ctrl+C."));
|
|
@@ -1918,7 +2148,8 @@ export class WebRepl {
|
|
|
1918
2148
|
}
|
|
1919
2149
|
const promptPayload = buildWebPromptPayload(command.text, attachments);
|
|
1920
2150
|
const optimisticMessageId = `web-user-${this.lineId + 1}`;
|
|
1921
|
-
|
|
2151
|
+
if (!continuationOnly)
|
|
2152
|
+
this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
|
|
1922
2153
|
const optimisticImageIds = (promptPayload.blocks ?? [])
|
|
1923
2154
|
.filter((block) => block.type === "image" && typeof block.imageId === "string")
|
|
1924
2155
|
.map((block) => block.imageId);
|
|
@@ -1932,12 +2163,14 @@ export class WebRepl {
|
|
|
1932
2163
|
this.setStatus({ ...this.status, phase: "running", detail: "working", modelOutput: undefined, usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
|
|
1933
2164
|
const engine = this.runtime.engine;
|
|
1934
2165
|
try {
|
|
1935
|
-
|
|
2166
|
+
const options = {
|
|
1936
2167
|
abortSignal: abortController.signal,
|
|
1937
2168
|
blocks: promptPayload.blocks,
|
|
1938
2169
|
displayText: promptPayload.displayText,
|
|
1939
2170
|
stopAfterTurn: () => this.queuedInput !== undefined,
|
|
1940
|
-
}
|
|
2171
|
+
};
|
|
2172
|
+
const stream = continuationOnly ? engine.continueExecution(options) : engine.sendUserText(promptPayload.text, options);
|
|
2173
|
+
for await (const event of stream) {
|
|
1941
2174
|
if (this.foregroundRunToken !== runToken)
|
|
1942
2175
|
continue;
|
|
1943
2176
|
if (this.runtime.engine !== engine)
|
|
@@ -1960,6 +2193,7 @@ export class WebRepl {
|
|
|
1960
2193
|
}
|
|
1961
2194
|
}
|
|
1962
2195
|
finally {
|
|
2196
|
+
this.demoSessions.get(engine)?.backend.stop();
|
|
1963
2197
|
if (this.foregroundRunToken !== runToken)
|
|
1964
2198
|
return;
|
|
1965
2199
|
if (this.runtime.engine !== engine)
|
|
@@ -1970,6 +2204,11 @@ export class WebRepl {
|
|
|
1970
2204
|
this.finalizeForegroundView();
|
|
1971
2205
|
this.pendingUserImageEchoIds = undefined;
|
|
1972
2206
|
this.pendingUserImageEchoMessageId = undefined;
|
|
2207
|
+
if (engine.getExecutionState?.()?.status === "interrupted") {
|
|
2208
|
+
this.setBusy(false);
|
|
2209
|
+
this.setStatus({ ...this.status, phase: "ready", detail: "interrupted", retryCooldownUntil: undefined });
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
1973
2212
|
const queuedText = this.queuedInput;
|
|
1974
2213
|
const queuedAttach = this.queuedAttachments;
|
|
1975
2214
|
this.queuedInput = undefined;
|
|
@@ -2263,6 +2502,19 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2263
2502
|
? await workspaceRepl.deleteWorkspaceDirectory(body.path)
|
|
2264
2503
|
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
2265
2504
|
}
|
|
2505
|
+
if (req.method === "POST" && url.pathname === "/api/framework-feedback/delete") {
|
|
2506
|
+
const body = await readJsonBody(req);
|
|
2507
|
+
return sendJson(res, await repl.deleteFrameworkFeedback(body?.id));
|
|
2508
|
+
}
|
|
2509
|
+
if (req.method === "GET" && url.pathname === "/api/framework-feedback") {
|
|
2510
|
+
res.setHeader("Cache-Control", "no-store");
|
|
2511
|
+
try {
|
|
2512
|
+
return sendJson(res, await repl.listFrameworkFeedback(Number(url.searchParams.get("page") ?? 1), Number(url.searchParams.get("pageSize") ?? 10)));
|
|
2513
|
+
}
|
|
2514
|
+
catch {
|
|
2515
|
+
return sendJson(res, { ok: false, error: "建议加载失败" }, 500);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2266
2518
|
if (req.method === "GET" && url.pathname === "/api/tools")
|
|
2267
2519
|
return sendJson(res, repl.globalTools());
|
|
2268
2520
|
if (req.method === "POST" && url.pathname === "/api/tools/global") {
|
|
@@ -2321,6 +2573,8 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2321
2573
|
const body = await readJsonBody(req);
|
|
2322
2574
|
return sendJson(res, await repl.setSessionModel(body.model, body.reasoning));
|
|
2323
2575
|
}
|
|
2576
|
+
if (req.method === "POST" && url.pathname === "/api/demo")
|
|
2577
|
+
return sendJson(res, await repl.startDemo(url.searchParams.get("sessionId") ?? ""));
|
|
2324
2578
|
if (req.method === "POST" && url.pathname === "/api/compact")
|
|
2325
2579
|
return sendJson(res, await repl.compactSession());
|
|
2326
2580
|
if (req.method === "POST" && url.pathname === "/api/fast-mode") {
|
|
@@ -2339,6 +2593,10 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2339
2593
|
const body = await readJsonBody(req);
|
|
2340
2594
|
return sendJson(res, await repl.submitImmediately(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
2341
2595
|
}
|
|
2596
|
+
if (req.method === "POST" && url.pathname === "/api/run/pause")
|
|
2597
|
+
return sendJson(res, repl.pauseRun());
|
|
2598
|
+
if (req.method === "POST" && url.pathname === "/api/run/continue")
|
|
2599
|
+
return sendJson(res, repl.continueRun());
|
|
2342
2600
|
if (req.method === "POST" && url.pathname === "/api/interrupt")
|
|
2343
2601
|
return sendJson(res, await repl.interrupt());
|
|
2344
2602
|
if (req.method === "POST" && url.pathname === "/api/queue/cancel")
|
|
@@ -3150,6 +3408,8 @@ function compactionLine(report, createdAt = new Date().toISOString(), current =
|
|
|
3150
3408
|
return {
|
|
3151
3409
|
kind: "meta",
|
|
3152
3410
|
title: "Context compaction",
|
|
3411
|
+
presentationLevel: "primary",
|
|
3412
|
+
collapsible: false,
|
|
3153
3413
|
text: formatCompactionReportSummary(report),
|
|
3154
3414
|
format: "plain",
|
|
3155
3415
|
compaction: { ...report, createdAt, current },
|