neoctl 0.2.44 → 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.
Files changed (75) hide show
  1. package/dist/context/compaction.d.ts +5 -0
  2. package/dist/context/compaction.js +4 -3
  3. package/dist/context/compaction.js.map +1 -1
  4. package/dist/context/prompts.js +2 -0
  5. package/dist/context/prompts.js.map +1 -1
  6. package/dist/core/abortable-stream.d.ts +2 -0
  7. package/dist/core/abortable-stream.js +35 -0
  8. package/dist/core/abortable-stream.js.map +1 -0
  9. package/dist/core/compaction-progress.d.ts +5 -0
  10. package/dist/core/compaction-progress.js +36 -0
  11. package/dist/core/compaction-progress.js.map +1 -0
  12. package/dist/core/execution-state.d.ts +10 -0
  13. package/dist/core/execution-state.js +18 -0
  14. package/dist/core/execution-state.js.map +1 -0
  15. package/dist/core/image-registry.d.ts +4 -2
  16. package/dist/core/image-registry.js +32 -7
  17. package/dist/core/image-registry.js.map +1 -1
  18. package/dist/core/image-storage.js +4 -0
  19. package/dist/core/image-storage.js.map +1 -1
  20. package/dist/core/query-engine.d.ts +25 -0
  21. package/dist/core/query-engine.js +142 -61
  22. package/dist/core/query-engine.js.map +1 -1
  23. package/dist/core/query.d.ts +5 -1
  24. package/dist/core/query.js +35 -20
  25. package/dist/core/query.js.map +1 -1
  26. package/dist/core/session-images.d.ts +52 -0
  27. package/dist/core/session-images.js +395 -0
  28. package/dist/core/session-images.js.map +1 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/model/model-metadata.json +41 -1
  33. package/dist/model/openai-mappers.js +0 -2
  34. package/dist/model/openai-mappers.js.map +1 -1
  35. package/dist/repl/index.js +2 -0
  36. package/dist/repl/index.js.map +1 -1
  37. package/dist/session/session-store.d.ts +9 -0
  38. package/dist/session/session-store.js +18 -2
  39. package/dist/session/session-store.js.map +1 -1
  40. package/dist/tools/builtins/exec-process-manager.d.ts +2 -0
  41. package/dist/tools/builtins/exec-process-manager.js +24 -1
  42. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  43. package/dist/tools/builtins/framework-feedback-tool.d.ts +26 -0
  44. package/dist/tools/builtins/framework-feedback-tool.js +97 -0
  45. package/dist/tools/builtins/framework-feedback-tool.js.map +1 -0
  46. package/dist/tools/builtins/image-generation-tool.d.ts +2 -0
  47. package/dist/tools/builtins/image-generation-tool.js +31 -141
  48. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  49. package/dist/tools/builtins/image-loader-tool.d.ts +6 -1
  50. package/dist/tools/builtins/image-loader-tool.js +48 -26
  51. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  52. package/dist/tools/builtins/image-note-tool.js +2 -2
  53. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  54. package/dist/tools/run-tool-use.js +3 -2
  55. package/dist/tools/run-tool-use.js.map +1 -1
  56. package/dist/tools/tool-catalog.d.ts +1 -0
  57. package/dist/tools/tool-catalog.js +2 -0
  58. package/dist/tools/tool-catalog.js.map +1 -1
  59. package/dist/tools/tool.d.ts +1 -0
  60. package/dist/tools/tool.js.map +1 -1
  61. package/dist/types/messages.d.ts +3 -1
  62. package/dist/types/messages.js.map +1 -1
  63. package/dist/web/demo-mode.d.ts +37 -0
  64. package/dist/web/demo-mode.js +141 -0
  65. package/dist/web/demo-mode.js.map +1 -0
  66. package/dist/web/demo-tools.d.ts +37 -0
  67. package/dist/web/demo-tools.js +259 -0
  68. package/dist/web/demo-tools.js.map +1 -0
  69. package/dist/web/demo-video.d.ts +1 -0
  70. package/dist/web/demo-video.js +3 -0
  71. package/dist/web/demo-video.js.map +1 -0
  72. package/dist/web/index.d.ts +43 -2
  73. package/dist/web/index.js +269 -9
  74. package/dist/web/index.js.map +1 -1
  75. 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
- constructor(runtime) {
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
- this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
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
- for await (const event of engine.sendUserText(promptPayload.text, {
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 },