neoctl 0.2.45 → 0.2.47

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 (102) hide show
  1. package/dist/agents/agent-tool.js +2 -1
  2. package/dist/agents/agent-tool.js.map +1 -1
  3. package/dist/context/compaction.d.ts +6 -0
  4. package/dist/context/compaction.js +10 -5
  5. package/dist/context/compaction.js.map +1 -1
  6. package/dist/context/prompts.js +2 -0
  7. package/dist/context/prompts.js.map +1 -1
  8. package/dist/core/abortable-stream.d.ts +4 -0
  9. package/dist/core/abortable-stream.js +52 -0
  10. package/dist/core/abortable-stream.js.map +1 -0
  11. package/dist/core/compaction-progress.d.ts +5 -0
  12. package/dist/core/compaction-progress.js +38 -0
  13. package/dist/core/compaction-progress.js.map +1 -0
  14. package/dist/core/execution-state.d.ts +12 -0
  15. package/dist/core/execution-state.js +19 -0
  16. package/dist/core/execution-state.js.map +1 -0
  17. package/dist/core/image-registry.d.ts +4 -2
  18. package/dist/core/image-registry.js +32 -7
  19. package/dist/core/image-registry.js.map +1 -1
  20. package/dist/core/image-storage.js +4 -0
  21. package/dist/core/image-storage.js.map +1 -1
  22. package/dist/core/query-engine.d.ts +31 -1
  23. package/dist/core/query-engine.js +198 -70
  24. package/dist/core/query-engine.js.map +1 -1
  25. package/dist/core/query.d.ts +5 -1
  26. package/dist/core/query.js +77 -22
  27. package/dist/core/query.js.map +1 -1
  28. package/dist/core/session-images.d.ts +52 -0
  29. package/dist/core/session-images.js +395 -0
  30. package/dist/core/session-images.js.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/model/model-gateway.d.ts +4 -0
  35. package/dist/model/model-gateway.js.map +1 -1
  36. package/dist/model/openai-chat-mapper.js +2 -0
  37. package/dist/model/openai-chat-mapper.js.map +1 -1
  38. package/dist/model/openai-mappers.js +0 -2
  39. package/dist/model/openai-mappers.js.map +1 -1
  40. package/dist/model/openai-responses-mapper.js +3 -0
  41. package/dist/model/openai-responses-mapper.js.map +1 -1
  42. package/dist/model/tool-input-stream.d.ts +31 -0
  43. package/dist/model/tool-input-stream.js +226 -0
  44. package/dist/model/tool-input-stream.js.map +1 -0
  45. package/dist/repl/index.js +2 -0
  46. package/dist/repl/index.js.map +1 -1
  47. package/dist/session/session-store.d.ts +9 -0
  48. package/dist/session/session-store.js +20 -2
  49. package/dist/session/session-store.js.map +1 -1
  50. package/dist/tasks/subagent-tools.js +1 -1
  51. package/dist/tasks/subagent-tools.js.map +1 -1
  52. package/dist/tools/builtins/exec-process-manager.d.ts +4 -1
  53. package/dist/tools/builtins/exec-process-manager.js +26 -2
  54. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  55. package/dist/tools/builtins/exec-tool.js +1 -1
  56. package/dist/tools/builtins/exec-tool.js.map +1 -1
  57. package/dist/tools/builtins/framework-feedback-tool.d.ts +26 -0
  58. package/dist/tools/builtins/framework-feedback-tool.js +97 -0
  59. package/dist/tools/builtins/framework-feedback-tool.js.map +1 -0
  60. package/dist/tools/builtins/image-generation-tool.d.ts +2 -0
  61. package/dist/tools/builtins/image-generation-tool.js +31 -141
  62. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  63. package/dist/tools/builtins/image-loader-tool.d.ts +6 -1
  64. package/dist/tools/builtins/image-loader-tool.js +48 -26
  65. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  66. package/dist/tools/builtins/image-note-tool.js +2 -2
  67. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  68. package/dist/tools/run-tool-use.js +4 -2
  69. package/dist/tools/run-tool-use.js.map +1 -1
  70. package/dist/tools/terminal-output-store.d.ts +1 -0
  71. package/dist/tools/terminal-output-store.js +13 -1
  72. package/dist/tools/terminal-output-store.js.map +1 -1
  73. package/dist/tools/tool-catalog.d.ts +1 -0
  74. package/dist/tools/tool-catalog.js +2 -0
  75. package/dist/tools/tool-catalog.js.map +1 -1
  76. package/dist/tools/tool.d.ts +1 -0
  77. package/dist/tools/tool.js.map +1 -1
  78. package/dist/types/events.d.ts +11 -0
  79. package/dist/types/messages.d.ts +3 -1
  80. package/dist/types/messages.js.map +1 -1
  81. package/dist/web/demo-mode.d.ts +37 -0
  82. package/dist/web/demo-mode.js +141 -0
  83. package/dist/web/demo-mode.js.map +1 -0
  84. package/dist/web/demo-tools.d.ts +37 -0
  85. package/dist/web/demo-tools.js +259 -0
  86. package/dist/web/demo-tools.js.map +1 -0
  87. package/dist/web/demo-video.d.ts +1 -0
  88. package/dist/web/demo-video.js +3 -0
  89. package/dist/web/demo-video.js.map +1 -0
  90. package/dist/web/image-result-metadata.d.ts +6 -0
  91. package/dist/web/image-result-metadata.js +3 -0
  92. package/dist/web/image-result-metadata.js.map +1 -1
  93. package/dist/web/index.d.ts +57 -3
  94. package/dist/web/index.js +388 -24
  95. package/dist/web/index.js.map +1 -1
  96. package/dist/web/tool-activity.d.ts +9 -0
  97. package/dist/web/tool-activity.js +19 -0
  98. package/dist/web/tool-activity.js.map +1 -0
  99. package/dist/web/tool-observation-result.d.ts +1 -0
  100. package/dist/web/tool-observation-result.js +35 -0
  101. package/dist/web/tool-observation-result.js.map +1 -0
  102. package/package.json +1 -1
package/dist/web/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { applyToolInputChanges } from "../model/tool-input-stream.js";
3
+ import { toolActivity } from "./tool-activity.js";
4
+ import { toolObservationResult } from "./tool-observation-result.js";
5
+ import { DemoMode, DEMO_MARKER, DEMO_PROMPT } from "./demo-mode.js";
6
+ import { DemoTools } from "./demo-tools.js";
7
+ import { createTextMessage } from "../types/messages.js";
2
8
  import { imageResultMetadata } from "./image-result-metadata.js";
3
9
  import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
4
10
  import { handlePromptConfigRequest } from "./prompt-config-protocol.js";
@@ -26,6 +32,7 @@ import { ExecProcessManager } from "../tools/builtins/exec-process-manager.js";
26
32
  import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
27
33
  import { grepTool } from "../tools/builtins/grep-tool.js";
28
34
  import { searchTool } from "../tools/builtins/search-tool.js";
35
+ import { createFrameworkFeedbackTool, FrameworkFeedbackStore } from "../tools/builtins/framework-feedback-tool.js";
29
36
  import { planTool } from "../tools/builtins/plan-tool.js";
30
37
  import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
31
38
  import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
@@ -165,6 +172,8 @@ export async function createWebRuntime(options = {}) {
165
172
  if (modelConfig?.provider === "openai")
166
173
  registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
167
174
  registerTool(planTool, { source: "builtin" });
175
+ const frameworkFeedback = new FrameworkFeedbackStore(options.frameworkFeedbackDir);
176
+ registerTool(createFrameworkFeedbackTool(frameworkFeedback), { source: "builtin" });
168
177
  for (const tool of options.externalTools ?? [])
169
178
  registerTool(tool, { source: "external" });
170
179
  for (const plugin of pluginCatalog) {
@@ -232,6 +241,7 @@ export async function createWebRuntime(options = {}) {
232
241
  activateSession({ engine, taskStore });
233
242
  const initialMetrics = await engine.contextMetrics();
234
243
  runtime = {
244
+ frameworkFeedback,
235
245
  engine,
236
246
  appPromptStore,
237
247
  communicationLogger,
@@ -526,6 +536,24 @@ function webRuntimeScopeKey(scope) {
526
536
  }
527
537
  export class WebRepl {
528
538
  runtime;
539
+ demoOptions;
540
+ pendingContinuation;
541
+ runControl() {
542
+ const execution = this.runtime.engine.getExecutionState?.();
543
+ const resumable = (execution?.status === "paused" || execution?.status === "interrupted")
544
+ && (!this.activeAbortController || this.activeAbortController.signal.aborted);
545
+ const waiting = this.pendingContinuation?.engine === this.runtime.engine;
546
+ return {
547
+ state: waiting ? "resuming" : resumable ? execution.status : this.busy ? "running" : "idle",
548
+ canPause: !waiting && !resumable && !this.stoppingRun && !this.immediateSend && Boolean(this.activeAbortController),
549
+ canResume: resumable && !waiting && !this.immediateSend,
550
+ reason: execution?.reason,
551
+ timer: execution?.elapsedMs === undefined ? undefined : {
552
+ elapsedMs: execution.elapsedMs,
553
+ running: execution.status === "running" && !waiting,
554
+ },
555
+ };
556
+ }
529
557
  agentContentReader = createAgentContentDetailResolver();
530
558
  subscribers = new Set();
531
559
  eventSequence = 0;
@@ -537,6 +565,7 @@ export class WebRepl {
537
565
  lineId = 0;
538
566
  assistantLineId;
539
567
  thinkingLineId;
568
+ previousTurnToolId;
540
569
  finalizedThinkingLineId;
541
570
  liveToolLineIds = new Map();
542
571
  activeAbortController;
@@ -544,6 +573,7 @@ export class WebRepl {
544
573
  lines;
545
574
  status;
546
575
  busy = false;
576
+ compactingSession;
547
577
  queuedInput;
548
578
  queuedAttachments;
549
579
  foregroundRun;
@@ -556,8 +586,91 @@ export class WebRepl {
556
586
  runtimeContextPublishQueue = Promise.resolve();
557
587
  backgroundSessionRuns = new Map();
558
588
  suppressReattachedStreaming = new Set();
559
- constructor(runtime) {
589
+ demoSessions = new WeakMap();
590
+ demoStatus = new WeakMap();
591
+ demoMessageQueues = new WeakMap();
592
+ demoStarting = false;
593
+ isDemo(engine = this.runtime.engine) {
594
+ const session = engine.snapshot().session;
595
+ const key = `${session?.sessionId}:${session?.firstWindowId}`;
596
+ const cached = this.demoStatus.get(engine);
597
+ if (cached?.key === key)
598
+ return cached.active || this.demoSessions.has(engine);
599
+ if (cached)
600
+ this.demoSessions.delete(engine);
601
+ const active = engine.getDisplayEntries().some(entry => entry.type === "message" && entry.message.metadata?.demoMode === DEMO_MARKER);
602
+ this.demoStatus.set(engine, { key, active });
603
+ if (active)
604
+ this.demoSessions.set(engine, null);
605
+ return active;
606
+ }
607
+ async startDemo(expectedSessionId) {
608
+ const engine = this.runtime.engine;
609
+ if (!expectedSessionId || expectedSessionId !== engine.snapshot().session?.sessionId)
610
+ return actionFailure("SESSION_MISMATCH", "会话已切换");
611
+ if (this.demoStarting || this.isDemo() || this.busy || this.foregroundRun || this.stoppingRun || this.queuedInput !== undefined
612
+ || this.backgroundTaskCount || engine.getDisplayEntries().length || this.lines.some(line => line.kind !== "system")) {
613
+ return actionFailure("DEMO_REQUIRES_EMPTY_SESSION", "仅空闲的空会话可以进入演示模式");
614
+ }
615
+ // Reserve synchronously before any await: double clicks and concurrent submits cannot start a second run.
616
+ this.demoStarting = true;
617
+ this.busy = true;
618
+ let demo;
619
+ try {
620
+ demo = new DemoMode(new DemoTools(this.runtime.tools, this.runtime.taskStore, this.runtime.execProcessManager), this.demoOptions.wait);
621
+ this.demoSessions.set(engine, demo);
622
+ const marker = createTextMessage("system", "");
623
+ marker.isMeta = true;
624
+ marker.metadata = { demoMode: DEMO_MARKER };
625
+ await engine.recordDisplayMessage(marker);
626
+ await demo.backend.prepare(engine.snapshot().session.sessionDir);
627
+ demo.controller.signal.throwIfAborted();
628
+ engine.setSimulation(demo);
629
+ this.startRun(DEMO_PROMPT);
630
+ return { ok: true };
631
+ }
632
+ catch (error) {
633
+ demo?.stop();
634
+ this.busy = false;
635
+ this.broadcastSync();
636
+ return actionFailure("DEMO_START_FAILED", error instanceof Error ? error.message : String(error));
637
+ }
638
+ finally {
639
+ this.demoStarting = false;
640
+ }
641
+ }
642
+ async recordDemoInput(text, attachments) {
643
+ const engine = this.runtime.engine;
644
+ if (!text.trim() && attachments.length === 0)
645
+ return { ok: true };
646
+ const previous = this.demoMessageQueues.get(engine) ?? Promise.resolve();
647
+ const pending = previous.catch(() => undefined).then(async () => {
648
+ const payload = buildWebPromptPayload(text, attachments);
649
+ const message = createTextMessage("user", payload.text);
650
+ if (payload.blocks)
651
+ message.blocks = payload.blocks;
652
+ const stored = await engine.recordDisplayMessage(message);
653
+ if (this.runtime.engine === engine) {
654
+ renderMessage(stored, line => this.append(line));
655
+ this.broadcastSync();
656
+ }
657
+ });
658
+ this.demoMessageQueues.set(engine, pending);
659
+ try {
660
+ await pending;
661
+ return { ok: true };
662
+ }
663
+ catch (error) {
664
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
665
+ }
666
+ finally {
667
+ if (this.demoMessageQueues.get(engine) === pending)
668
+ this.demoMessageQueues.delete(engine);
669
+ }
670
+ }
671
+ constructor(runtime, demoOptions = {}) {
560
672
  this.runtime = runtime;
673
+ this.demoOptions = demoOptions;
561
674
  const lineId = { current: 0 };
562
675
  this.lines = initialLines(runtime, lineId);
563
676
  this.lineId = lineId.current;
@@ -614,7 +727,7 @@ export class WebRepl {
614
727
  redact: value => this.runtime.engine.redactDisplayValue(value),
615
728
  }, request);
616
729
  }
617
- terminalOutput(sessionId, runId, stream, offset, limitBytes) {
730
+ terminalOutput(sessionId, runId, stream, offset, limitBytes, tail = false) {
618
731
  const session = this.runtime.engine.snapshot().session;
619
732
  if (!session?.sessionDir || !sessionId || sessionId !== session.sessionId || !runId || !["stdout", "stderr"].includes(stream))
620
733
  return undefined;
@@ -624,7 +737,7 @@ export class WebRepl {
624
737
  const registered = manager.registerOwnerSession(sessionId, session.sessionDir);
625
738
  if (!registered.ok)
626
739
  return undefined;
627
- const page = manager.readOutput(sessionId, runId, { stream: stream, offset, limitBytes });
740
+ const page = manager.readOutput(sessionId, runId, { stream: stream, offset, limitBytes, tail });
628
741
  // Cursor addresses stored bytes, not the possibly additionally redacted response length.
629
742
  return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
630
743
  }
@@ -640,7 +753,9 @@ export class WebRepl {
640
753
  return {
641
754
  lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
642
755
  status: this.timedStatus(),
643
- busy: this.busy,
756
+ busy: this.runControl().canResume ? false : this.busy,
757
+ runControl: this.runControl(),
758
+ demoMode: this.isDemo(),
644
759
  queuedInput: this.queuedInput,
645
760
  backgroundTaskCount: backgroundTasks.length,
646
761
  backgroundTasks,
@@ -894,6 +1009,12 @@ export class WebRepl {
894
1009
  this.publishRuntimeContext();
895
1010
  }
896
1011
  async submit(text, attachments = []) {
1012
+ if (this.runControl().canResume && (!text.trimStart().startsWith("/") || !text.trim()))
1013
+ return this.continueRun(text, attachments);
1014
+ if (this.pendingContinuation)
1015
+ return { ok: false, error: "续接请求已接收,请勿重复发送" };
1016
+ if (this.isDemo())
1017
+ return this.recordDemoInput(text, attachments);
897
1018
  if (this.stoppingRun || this.immediateSend)
898
1019
  return { ok: false, error: "正在停止上一轮请求,请稍后发送" };
899
1020
  const trimmed = text.trim();
@@ -916,6 +1037,14 @@ export class WebRepl {
916
1037
  return { ok: true };
917
1038
  }
918
1039
  async submitImmediately(text, attachments = []) {
1040
+ if (this.runControl().canResume) {
1041
+ const result = this.continueRun(text, attachments);
1042
+ return result.ok ? { ok: true, interrupted: false } : result;
1043
+ }
1044
+ if (this.isDemo()) {
1045
+ const result = await this.recordDemoInput(text, attachments);
1046
+ return result.ok ? { ok: true, interrupted: false } : result;
1047
+ }
919
1048
  if (this.immediateSend)
920
1049
  return { ok: false, error: "正在发送,请勿重复提交" };
921
1050
  if (this.stoppingRun)
@@ -948,8 +1077,8 @@ export class WebRepl {
948
1077
  this.queuedInput = mergeWebQueuedInput(this.queuedInput, text);
949
1078
  this.queuedAttachments = [...(this.queuedAttachments ?? []), ...attachments].slice(0, 20);
950
1079
  }
951
- startRun(text, attachments = []) {
952
- const run = this.handleCommandOrPrompt(text, attachments).catch((error) => {
1080
+ startRun(text, attachments = [], continuationOnly = false) {
1081
+ const run = this.handleCommandOrPrompt(text, attachments, continuationOnly).catch((error) => {
953
1082
  this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
954
1083
  this.setBusy(false);
955
1084
  this.setStatus({ ...this.status, phase: "ready", detail: undefined });
@@ -1013,6 +1142,7 @@ export class WebRepl {
1013
1142
  this.runtime.usage.reset();
1014
1143
  this.status = initialStatus(this.runtime, metrics);
1015
1144
  const lineId = { current: 0 };
1145
+ this.previousTurnToolId = undefined;
1016
1146
  this.lines = initialLines(this.runtime, lineId);
1017
1147
  this.lineId = lineId.current;
1018
1148
  this.assistantLineId = undefined;
@@ -1186,9 +1316,28 @@ export class WebRepl {
1186
1316
  }
1187
1317
  async compactSession() {
1188
1318
  const engine = this.runtime.engine;
1319
+ if (this.isDemo())
1320
+ return actionFailure("DEMO_SCRIPTED", "演示会话由后台安排压缩");
1321
+ if (this.compactingSession === engine)
1322
+ return actionFailure("COMPACTION_IN_PROGRESS", "Context compaction is already running");
1323
+ const showProgress = !this.busy;
1324
+ const runToken = this.foregroundRunToken;
1325
+ const controller = showProgress ? new AbortController() : undefined;
1326
+ if (controller)
1327
+ this.activeAbortController = controller;
1328
+ let finishForeground;
1329
+ const foreground = showProgress ? new Promise(resolve => { finishForeground = resolve; }) : undefined;
1330
+ if (foreground)
1331
+ this.foregroundRun = foreground;
1332
+ if (showProgress) {
1333
+ this.compactingSession = engine;
1334
+ this.busy = true;
1335
+ this.setStatus({ ...this.status, phase: "compacting", detail: "manualcompact: generating context summary", activityTick: this.status.activityTick + 1 });
1336
+ }
1189
1337
  try {
1190
- const result = await engine.updateSessionSettings({ compact: true });
1191
- if (this.runtime.engine === engine) {
1338
+ // An idle click executes now through the settings barrier, not the chat/input queue.
1339
+ const result = await engine.updateSessionSettings({ compact: true }, { abortSignal: controller?.signal });
1340
+ if (this.runtime.engine === engine && this.foregroundRunToken === runToken) {
1192
1341
  if (result.compaction?.report)
1193
1342
  this.appendCompaction(result.compaction.report);
1194
1343
  if (!result.deferred)
@@ -1200,6 +1349,26 @@ export class WebRepl {
1200
1349
  catch (error) {
1201
1350
  return actionFailure("COMPACTION_FAILED", error instanceof Error ? error.message : String(error));
1202
1351
  }
1352
+ finally {
1353
+ if (controller && this.activeAbortController === controller)
1354
+ this.activeAbortController = undefined;
1355
+ if (this.compactingSession === engine)
1356
+ this.compactingSession = undefined;
1357
+ if (this.foregroundRun === foreground)
1358
+ this.foregroundRun = undefined;
1359
+ finishForeground?.();
1360
+ if (showProgress && this.runtime.engine === engine && this.foregroundRunToken === runToken) {
1361
+ this.busy = false;
1362
+ this.setStatus({ ...this.status, phase: "ready", detail: undefined, activityTick: this.status.activityTick + 1 });
1363
+ const queued = this.queuedInput;
1364
+ const attachments = this.queuedAttachments ?? [];
1365
+ if (queued !== undefined) {
1366
+ this.queuedInput = undefined;
1367
+ this.queuedAttachments = undefined;
1368
+ this.startRun(queued, attachments);
1369
+ }
1370
+ }
1371
+ }
1203
1372
  }
1204
1373
  loginForm(providerValue) {
1205
1374
  const provider = parseLoginProvider(providerValue);
@@ -1244,9 +1413,72 @@ export class WebRepl {
1244
1413
  this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
1245
1414
  this.publishRuntimeContext();
1246
1415
  }
1416
+ async listFrameworkFeedback(page, pageSize) {
1417
+ if (!this.runtime.frameworkFeedback)
1418
+ throw new Error("Framework feedback storage unavailable");
1419
+ return this.runtime.frameworkFeedback.list(page, pageSize);
1420
+ }
1421
+ async deleteFrameworkFeedback(id) {
1422
+ if (typeof id !== "string" || !/^\d{13}-[a-f0-9-]{36}$/.test(id))
1423
+ return actionFailure("INVALID_REQUEST", "建议编号无效");
1424
+ try {
1425
+ if (!this.runtime.frameworkFeedback)
1426
+ throw new Error("unavailable");
1427
+ return { ok: true, deleted: await this.runtime.frameworkFeedback.delete(id) };
1428
+ }
1429
+ catch {
1430
+ return actionFailure("FEEDBACK_DELETE_FAILED", "删除失败,请重试");
1431
+ }
1432
+ }
1433
+ pauseRun() {
1434
+ if (this.runControl().canResume)
1435
+ return { ok: true };
1436
+ if (!this.runControl().canPause)
1437
+ return actionFailure("INVALID_REQUEST", "当前没有可暂停的会话执行");
1438
+ this.runtime.engine.markExecutionPaused();
1439
+ const queuedText = this.queuedInput;
1440
+ const queuedAttachments = this.queuedAttachments;
1441
+ // Do not await cooperative cancellation: the UI must stop immediately.
1442
+ void this.stopForegroundRun("用户暂停了会话").catch(() => this.broadcastSync());
1443
+ this.queuedInput = queuedText;
1444
+ this.queuedAttachments = queuedAttachments;
1445
+ this.status = { ...this.status, phase: "ready", detail: "paused", currentTool: undefined, retryCooldownUntil: undefined };
1446
+ this.broadcastSync();
1447
+ return { ok: true };
1448
+ }
1449
+ continueRun(text = "", attachments = []) {
1450
+ if (!this.runControl().canResume)
1451
+ return actionFailure("INVALID_REQUEST", "没有可恢复的执行,或续接请求已经提交");
1452
+ const useQueue = !text.trim() && attachments.length === 0 && this.queuedInput !== undefined;
1453
+ const request = { engine: this.runtime.engine,
1454
+ text: text || (useQueue ? this.queuedInput : ""),
1455
+ attachments: useQueue ? this.queuedAttachments ?? [] : attachments };
1456
+ this.queuedInput = undefined;
1457
+ this.queuedAttachments = undefined;
1458
+ this.pendingContinuation = request;
1459
+ this.broadcastSync();
1460
+ // Accept immediately, but never overlap two queries on the same mutable engine.
1461
+ void (async () => {
1462
+ try {
1463
+ await this.stoppingRun;
1464
+ await this.foregroundRun;
1465
+ if (this.pendingContinuation !== request || this.runtime.engine !== request.engine)
1466
+ return;
1467
+ this.pendingContinuation = undefined;
1468
+ this.startRun(request.text, request.attachments, !request.text.trim() && request.attachments.length === 0);
1469
+ }
1470
+ finally {
1471
+ if (this.pendingContinuation === request)
1472
+ this.pendingContinuation = undefined;
1473
+ this.broadcastSync();
1474
+ }
1475
+ })().catch(error => this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) }));
1476
+ return { ok: true };
1477
+ }
1247
1478
  async interrupt() {
1248
1479
  if (this.immediateSend)
1249
1480
  await this.immediateSend;
1481
+ this.demoSessions.get(this.runtime.engine)?.stop();
1250
1482
  const interrupted = await this.stopForegroundRun("Interrupted from neo web");
1251
1483
  return { ok: true, interrupted };
1252
1484
  }
@@ -1261,6 +1493,10 @@ export class WebRepl {
1261
1493
  append(line, streaming = false) {
1262
1494
  const id = ++this.lineId;
1263
1495
  const nextLine = { id, ...line };
1496
+ if (line.activity && line.toolName && line.toolUseId && !line.image) {
1497
+ nextLine.connectPreviousTool = this.previousTurnToolId !== undefined;
1498
+ this.previousTurnToolId = id;
1499
+ }
1264
1500
  this.lines.push(nextLine);
1265
1501
  if (streaming)
1266
1502
  this.broadcastDelta({ type: "line.append", line: nextLine });
@@ -1283,6 +1519,9 @@ export class WebRepl {
1283
1519
  this.broadcastSync();
1284
1520
  }
1285
1521
  replaceLine(id, patch) {
1522
+ const previous = this.lines.find(line => line.id === id);
1523
+ if (previous?.activity && patch.titleStatus)
1524
+ patch.activity = { ...previous.activity, phase: patch.titleStatus === "success" ? "completed" : "failed" };
1286
1525
  this.lines = this.lines.map((line) => line.id === id ? { ...line, ...patch } : line);
1287
1526
  this.broadcastSync();
1288
1527
  }
@@ -1431,6 +1670,7 @@ export class WebRepl {
1431
1670
  signal: terminal.signal,
1432
1671
  terminationReason: terminal.termination_reason,
1433
1672
  outputAvailability: terminal.output_ref.availability,
1673
+ outputRevision: terminal.output_ref.streams,
1434
1674
  }));
1435
1675
  const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1436
1676
  kind: "session",
@@ -1451,6 +1691,14 @@ export class WebRepl {
1451
1691
  .sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
1452
1692
  }
1453
1693
  async detachRunningForeground(reason) {
1694
+ if (this.pendingContinuation || this.stoppingRun)
1695
+ throw new Error("上一轮请求正在取消,请稍后切换会话");
1696
+ if (this.demoStarting)
1697
+ throw new Error("演示模式正在启动");
1698
+ if (this.isDemo() && this.busy) {
1699
+ this.demoSessions.get(this.runtime.engine)?.stop();
1700
+ return this.stopForegroundRun(reason);
1701
+ }
1454
1702
  if (!this.busy)
1455
1703
  return false;
1456
1704
  const snapshot = this.runtime.engine.snapshot().session;
@@ -1512,6 +1760,12 @@ export class WebRepl {
1512
1760
  finalizeLiveLine(id) {
1513
1761
  if (id === undefined)
1514
1762
  return;
1763
+ const previous = this.lines.find(line => line.id === id);
1764
+ if (previous?.activity && ["generating", "queued", "running"].includes(previous.activity.phase)) {
1765
+ const activity = { ...previous.activity, phase: previous.titleStatus === "success" ? "completed" : previous.titleStatus === "failure" ? "failed" : "interrupted" };
1766
+ this.lines = this.lines.map(line => line.id === id ? { ...line, activity } : line);
1767
+ this.queueDeltaOperation({ type: "line.patch", id, patch: { activity } });
1768
+ }
1515
1769
  this.lines = this.lines.map((line) => line.id === id ? { ...line, live: false } : line);
1516
1770
  this.queueDeltaOperation({ type: "line.patch", id, patch: { live: false } });
1517
1771
  }
@@ -1531,6 +1785,8 @@ export class WebRepl {
1531
1785
  cancelLiveToolLines(reason) {
1532
1786
  for (const id of this.liveToolLineIds.values()) {
1533
1787
  const line = this.lines.find((item) => item.id === id);
1788
+ if (line?.activity)
1789
+ this.patchLiveToolLine(line.toolUseId, { activity: { ...line.activity, phase: "interrupted" } });
1534
1790
  const steps = (line?.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "failed", phase: "cancelled" } : step);
1535
1791
  this.lines = this.lines.map((item) => item.id === id ? { ...item, live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...item.toolStream, phase: "cancelled", message: reason, steps } } : item);
1536
1792
  this.queueDeltaOperation({ type: "line.patch", id, patch: { live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...line?.toolStream, phase: "cancelled", message: reason, steps } } });
@@ -1631,7 +1887,12 @@ export class WebRepl {
1631
1887
  else {
1632
1888
  stream.message = progress.message;
1633
1889
  }
1634
- this.patchLiveToolLine(event.toolUse.id, { toolStream: stream });
1890
+ const data = progress.data;
1891
+ const observation = data && typeof data.task_id === "string" && Number.isSafeInteger(data.run_generation)
1892
+ ? { ...line.observation, task_id: data.task_id, run_generation: data.run_generation }
1893
+ : data && typeof data.session_id === "string" && ["terminal.started", "terminal.output.delta"].includes(String(data.type)) && ["terminal_run", "terminal_control"].includes(event.toolUse.name)
1894
+ ? { ...line.observation, session_id: data.session_id, status: "running", command: typeof data.command === "string" ? data.command : line.observation?.command } : line.observation;
1895
+ this.patchLiveToolLine(event.toolUse.id, { toolStream: stream, observation });
1635
1896
  }
1636
1897
  applyAvailableToolResult(event) {
1637
1898
  const liveLineId = this.liveToolLineIds.get(event.toolUse.id);
@@ -1663,19 +1924,28 @@ export class WebRepl {
1663
1924
  }
1664
1925
  }
1665
1926
  handleEvent(event) {
1927
+ // Model lifecycle defines membership only. The frontend owns the fixed
1928
+ // two-second playback and must not be interrupted by the next request.
1929
+ if (event.type === "state" && event.phase === "calling_model")
1930
+ this.previousTurnToolId = undefined;
1666
1931
  this.reduce(event);
1667
1932
  if (event.type === "timing.updated") {
1668
1933
  if (event.timing.kind === "tool") {
1669
1934
  const line = this.lines.find(item => item.timing?.id === event.timing.id)
1670
1935
  ?? this.lines.find(item => item.id === this.liveToolLineIds.get(event.timing.toolUseId));
1671
1936
  if (line) {
1672
- this.lines = this.lines.map(item => item.id === line.id ? { ...item, timing: event.timing } : item);
1673
- this.queueDeltaOperation({ type: "line.patch", id: line.id, patch: { timing: event.timing } });
1937
+ const activity = line.activity && event.timing.status === "running" ? { ...line.activity, phase: "running" } : line.activity;
1938
+ this.lines = this.lines.map(item => item.id === line.id ? { ...item, timing: event.timing, activity } : item);
1939
+ this.queueDeltaOperation({ type: "line.patch", id: line.id, patch: { timing: event.timing, activity } });
1674
1940
  }
1675
1941
  }
1676
1942
  this.queueDeltaStatus();
1677
1943
  return;
1678
1944
  }
1945
+ if (event.type === "message" && event.message.metadata?.loadedImages === true && typeof event.message.metadata.sourceToolUseId === "string") {
1946
+ renderObservedImages(event.message, line => this.append(line));
1947
+ return;
1948
+ }
1679
1949
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
1680
1950
  const messageId = this.pendingUserImageEchoMessageId;
1681
1951
  this.pendingUserImageEchoIds = undefined;
@@ -1684,7 +1954,35 @@ export class WebRepl {
1684
1954
  this.broadcastSync();
1685
1955
  return;
1686
1956
  }
1687
- if (event.type === "tool_call.started" || event.type === "tool_call.delta") {
1957
+ if (event.type === "tool_call.started" || event.type === "tool_call.input") {
1958
+ let id = this.liveToolLineIds.get(event.callId);
1959
+ let previous = this.lines.find(line => line.id === id);
1960
+ const name = event.name || previous?.toolName || "tool";
1961
+ if (id === undefined) {
1962
+ const activity = { phase: "generating", input: {} };
1963
+ id = this.append({ ...formatToolUse({ id: event.callId, name, input: {} }), activity, live: true });
1964
+ this.liveToolLineIds.set(event.callId, id);
1965
+ previous = this.lines.find(line => line.id === id);
1966
+ }
1967
+ if (previous?.toolName !== name && name !== "tool")
1968
+ this.patchLiveToolLine(event.callId, { toolName: name, title: toolTitle(name, "running"), toolPresentation: toolPresentationForSource(name) });
1969
+ if (event.type === "tool_call.input") {
1970
+ const activity = { phase: "generating", input: applyToolInputChanges(previous?.activity?.input || {}, event.changes), truncated: event.truncated };
1971
+ this.lines = this.lines.map(line => line.id === id ? { ...line, activity } : line);
1972
+ this.queueDeltaOperation({ type: "line.activity.input", id, changes: event.changes, truncated: event.truncated });
1973
+ }
1974
+ this.queueDeltaStatus();
1975
+ return;
1976
+ }
1977
+ if (event.type === "tool_call.ready") {
1978
+ const patch = { ...formatToolUse(event.toolUse), activity: toolActivity(event.toolUse.input, "queued"), live: true };
1979
+ if (this.liveToolLineIds.has(event.toolUse.id))
1980
+ this.patchLiveToolLine(event.toolUse.id, patch);
1981
+ else
1982
+ this.liveToolLineIds.set(event.toolUse.id, this.append(patch));
1983
+ return;
1984
+ }
1985
+ if (event.type === "tool_call.delta") {
1688
1986
  this.queueDeltaStatus();
1689
1987
  return;
1690
1988
  }
@@ -1696,6 +1994,10 @@ export class WebRepl {
1696
1994
  this.broadcastSync();
1697
1995
  return;
1698
1996
  }
1997
+ if (event.type === "assistant.output.done") {
1998
+ this.queueDeltaStatus();
1999
+ return;
2000
+ }
1699
2001
  if (event.type === "assistant.delta") {
1700
2002
  this.finalizeThinkingLine();
1701
2003
  const id = this.assistantLineId ?? this.append({ kind: "assistant", text: "", live: true }, true);
@@ -1788,8 +2090,12 @@ export class WebRepl {
1788
2090
  this.finalizeLiveLine(this.assistantLineId);
1789
2091
  this.finalizeThinkingLine();
1790
2092
  this.assistantLineId = undefined;
1791
- const id = this.append({ ...formatToolUse(event.toolUse), live: true });
1792
- this.liveToolLineIds.set(event.toolUse.id, id);
2093
+ const patch = { ...formatToolUse(event.toolUse), activity: toolActivity(event.toolUse.input, "queued"), live: true };
2094
+ const existing = this.liveToolLineIds.get(event.toolUse.id);
2095
+ if (existing === undefined)
2096
+ this.liveToolLineIds.set(event.toolUse.id, this.append(patch));
2097
+ else
2098
+ this.patchLiveToolLine(event.toolUse.id, patch);
1793
2099
  return;
1794
2100
  }
1795
2101
  if (event.type === "tool.finished") {
@@ -1818,7 +2124,7 @@ export class WebRepl {
1818
2124
  .map((block) => block.imageId);
1819
2125
  return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
1820
2126
  }
1821
- async handleCommandOrPrompt(text, attachments = []) {
2127
+ async handleCommandOrPrompt(text, attachments = [], continuationOnly = false) {
1822
2128
  const command = parseReplCommand(text);
1823
2129
  if (command.type === "exit") {
1824
2130
  this.append(systemLine("neo web server is still running. Close this tab or stop the server process with Ctrl+C."));
@@ -1918,7 +2224,8 @@ export class WebRepl {
1918
2224
  }
1919
2225
  const promptPayload = buildWebPromptPayload(command.text, attachments);
1920
2226
  const optimisticMessageId = `web-user-${this.lineId + 1}`;
1921
- this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
2227
+ if (!continuationOnly)
2228
+ this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
1922
2229
  const optimisticImageIds = (promptPayload.blocks ?? [])
1923
2230
  .filter((block) => block.type === "image" && typeof block.imageId === "string")
1924
2231
  .map((block) => block.imageId);
@@ -1932,12 +2239,14 @@ export class WebRepl {
1932
2239
  this.setStatus({ ...this.status, phase: "running", detail: "working", modelOutput: undefined, usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
1933
2240
  const engine = this.runtime.engine;
1934
2241
  try {
1935
- for await (const event of engine.sendUserText(promptPayload.text, {
2242
+ const options = {
1936
2243
  abortSignal: abortController.signal,
1937
2244
  blocks: promptPayload.blocks,
1938
2245
  displayText: promptPayload.displayText,
1939
2246
  stopAfterTurn: () => this.queuedInput !== undefined,
1940
- })) {
2247
+ };
2248
+ const stream = continuationOnly ? engine.continueExecution(options) : engine.sendUserText(promptPayload.text, options);
2249
+ for await (const event of stream) {
1941
2250
  if (this.foregroundRunToken !== runToken)
1942
2251
  continue;
1943
2252
  if (this.runtime.engine !== engine)
@@ -1960,6 +2269,7 @@ export class WebRepl {
1960
2269
  }
1961
2270
  }
1962
2271
  finally {
2272
+ this.demoSessions.get(engine)?.backend.stop();
1963
2273
  if (this.foregroundRunToken !== runToken)
1964
2274
  return;
1965
2275
  if (this.runtime.engine !== engine)
@@ -1970,6 +2280,11 @@ export class WebRepl {
1970
2280
  this.finalizeForegroundView();
1971
2281
  this.pendingUserImageEchoIds = undefined;
1972
2282
  this.pendingUserImageEchoMessageId = undefined;
2283
+ if (engine.getExecutionState?.()?.status === "interrupted") {
2284
+ this.setBusy(false);
2285
+ this.setStatus({ ...this.status, phase: "ready", detail: "interrupted", retryCooldownUntil: undefined });
2286
+ return;
2287
+ }
1973
2288
  const queuedText = this.queuedInput;
1974
2289
  const queuedAttach = this.queuedAttachments;
1975
2290
  this.queuedInput = undefined;
@@ -2170,11 +2485,16 @@ export function compactLineOperations(operations) {
2170
2485
  previous.text += operation.text;
2171
2486
  continue;
2172
2487
  }
2488
+ if (operation.type === "line.activity.input" && previous?.type === "line.activity.input" && previous.id === operation.id) {
2489
+ previous.changes.push(...operation.changes);
2490
+ previous.truncated = operation.truncated;
2491
+ continue;
2492
+ }
2173
2493
  if (operation.type === "line.patch" && previous?.type === "line.patch" && previous.id === operation.id) {
2174
2494
  previous.patch = { ...previous.patch, ...operation.patch };
2175
2495
  continue;
2176
2496
  }
2177
- compacted.push(operation.type === "line.patch" ? { ...operation, patch: { ...operation.patch } } : { ...operation });
2497
+ compacted.push(operation.type === "line.patch" ? { ...operation, patch: { ...operation.patch } } : operation.type === "line.activity.input" ? { ...operation, changes: [...operation.changes] } : { ...operation });
2178
2498
  }
2179
2499
  return compacted;
2180
2500
  }
@@ -2232,7 +2552,7 @@ export async function handleWebRequest(req, res, router) {
2232
2552
  }
2233
2553
  if (req.method === "GET" && url.pathname === "/api/terminal-output") {
2234
2554
  res.setHeader("Cache-Control", "no-store");
2235
- const page = repl.terminalOutput(url.searchParams.get("sessionId") ?? "", url.searchParams.get("runId") ?? "", url.searchParams.get("stream") ?? "stdout", Number(url.searchParams.get("offset") ?? 0), Number(url.searchParams.get("limitBytes") ?? 65536));
2555
+ const page = repl.terminalOutput(url.searchParams.get("sessionId") ?? "", url.searchParams.get("runId") ?? "", url.searchParams.get("stream") ?? "stdout", Number(url.searchParams.get("offset") ?? 0), Number(url.searchParams.get("limitBytes") ?? 65536), url.searchParams.get("tail") === "true");
2236
2556
  return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
2237
2557
  }
2238
2558
  if (req.method === "GET" && url.pathname === "/api/state")
@@ -2263,6 +2583,19 @@ export async function handleWebRequest(req, res, router) {
2263
2583
  ? await workspaceRepl.deleteWorkspaceDirectory(body.path)
2264
2584
  : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
2265
2585
  }
2586
+ if (req.method === "POST" && url.pathname === "/api/framework-feedback/delete") {
2587
+ const body = await readJsonBody(req);
2588
+ return sendJson(res, await repl.deleteFrameworkFeedback(body?.id));
2589
+ }
2590
+ if (req.method === "GET" && url.pathname === "/api/framework-feedback") {
2591
+ res.setHeader("Cache-Control", "no-store");
2592
+ try {
2593
+ return sendJson(res, await repl.listFrameworkFeedback(Number(url.searchParams.get("page") ?? 1), Number(url.searchParams.get("pageSize") ?? 10)));
2594
+ }
2595
+ catch {
2596
+ return sendJson(res, { ok: false, error: "建议加载失败" }, 500);
2597
+ }
2598
+ }
2266
2599
  if (req.method === "GET" && url.pathname === "/api/tools")
2267
2600
  return sendJson(res, repl.globalTools());
2268
2601
  if (req.method === "POST" && url.pathname === "/api/tools/global") {
@@ -2321,6 +2654,8 @@ export async function handleWebRequest(req, res, router) {
2321
2654
  const body = await readJsonBody(req);
2322
2655
  return sendJson(res, await repl.setSessionModel(body.model, body.reasoning));
2323
2656
  }
2657
+ if (req.method === "POST" && url.pathname === "/api/demo")
2658
+ return sendJson(res, await repl.startDemo(url.searchParams.get("sessionId") ?? ""));
2324
2659
  if (req.method === "POST" && url.pathname === "/api/compact")
2325
2660
  return sendJson(res, await repl.compactSession());
2326
2661
  if (req.method === "POST" && url.pathname === "/api/fast-mode") {
@@ -2339,6 +2674,10 @@ export async function handleWebRequest(req, res, router) {
2339
2674
  const body = await readJsonBody(req);
2340
2675
  return sendJson(res, await repl.submitImmediately(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
2341
2676
  }
2677
+ if (req.method === "POST" && url.pathname === "/api/run/pause")
2678
+ return sendJson(res, repl.pauseRun());
2679
+ if (req.method === "POST" && url.pathname === "/api/run/continue")
2680
+ return sendJson(res, repl.continueRun());
2342
2681
  if (req.method === "POST" && url.pathname === "/api/interrupt")
2343
2682
  return sendJson(res, await repl.interrupt());
2344
2683
  if (req.method === "POST" && url.pathname === "/api/queue/cancel")
@@ -2479,7 +2818,7 @@ function restoredHistoryLines(runtime) {
2479
2818
  if (line.toolUseId && line.titleStatus && historyToolLineIndexes.has(line.toolUseId)) {
2480
2819
  const index = historyToolLineIndexes.get(line.toolUseId);
2481
2820
  const started = lines[index];
2482
- lines[index] = { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
2821
+ lines[index] = { ...line, activity: started.activity ? { ...started.activity, phase: line.titleStatus === "success" ? "completed" : "failed" } : undefined, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
2483
2822
  return index + 1;
2484
2823
  }
2485
2824
  lines.push(line);
@@ -2505,6 +2844,10 @@ function restoredHistoryLines(runtime) {
2505
2844
  }
2506
2845
  for (const [entryIndex, entry] of entries.entries()) {
2507
2846
  if (entry.type === "message") {
2847
+ if (entry.message.metadata?.loadedImages === true && typeof entry.message.metadata.sourceToolUseId === "string") {
2848
+ renderObservedImages(entry.message, append);
2849
+ continue;
2850
+ }
2508
2851
  renderMessage(entry.message, append, undefined, {
2509
2852
  includeToolUseBlocks: true,
2510
2853
  includeThinkingBlocks: true,
@@ -2520,6 +2863,8 @@ function restoredHistoryLines(runtime) {
2520
2863
  const matches = line.toolUseId ? toolTimings.get(line.toolUseId) : undefined;
2521
2864
  if (matches?.length === 1)
2522
2865
  line.timing = matches[0];
2866
+ if (line.activity && !line.titleStatus)
2867
+ line.activity = { ...line.activity, phase: "interrupted" };
2523
2868
  }
2524
2869
  return lines;
2525
2870
  }
@@ -2675,6 +3020,12 @@ function reduceStatus(status, event) {
2675
3020
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
2676
3021
  if (event.type === "usage")
2677
3022
  return { ...status, usage: event.usage, inputTokenUpdatedAt: event.usage.inputTokens !== undefined ? Date.now() : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.usage.outputTokens !== undefined ? Date.now() : status.outputTokenUpdatedAt, activityTick: status.activityTick + 1 };
3023
+ if (event.type === "assistant.output.done")
3024
+ return status.modelOutput?.kind === "text" ? { ...status, modelOutput: undefined, activityTick: status.activityTick + 1 } : status;
3025
+ if (event.type === "message" && event.message.role === "assistant" && event.message.blocks.some(block => block.type === "text") && status.modelOutput?.kind === "text")
3026
+ return { ...status, modelOutput: undefined, activityTick: status.activityTick + 1 };
3027
+ if (event.type === "assistant.delta" && !event.text)
3028
+ return status;
2678
3029
  if (event.type === "assistant.delta")
2679
3030
  return { ...status, phase: "calling_model", modelOutput: { kind: "text" }, streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text), outputTokenUpdatedAt: Date.now(), activityTick: status.activityTick + 1 };
2680
3031
  if (event.type === "thinking.delta")
@@ -3065,6 +3416,15 @@ function renderMessageImages(message, append, messageId = message.id) {
3065
3416
  }
3066
3417
  return rendered;
3067
3418
  }
3419
+ function renderObservedImages(message, append) {
3420
+ for (const [index, block] of message.blocks.entries()) {
3421
+ if (block.type !== "image")
3422
+ continue;
3423
+ const line = imageLineForBlock("tool_result", block, message.id, index);
3424
+ if (line)
3425
+ append({ ...line, observationOnly: true, messageId: message.id, parentToolName: "image_inspect", parentToolUseId: message.metadata?.sourceToolUseId });
3426
+ }
3427
+ }
3068
3428
  function imageLineForBlock(role, block, messageId, blockIndex) {
3069
3429
  const kind = kindForRole(role);
3070
3430
  if (kind === "meta")
@@ -3150,6 +3510,8 @@ function compactionLine(report, createdAt = new Date().toISOString(), current =
3150
3510
  return {
3151
3511
  kind: "meta",
3152
3512
  title: "Context compaction",
3513
+ presentationLevel: "primary",
3514
+ collapsible: false,
3153
3515
  text: formatCompactionReportSummary(report),
3154
3516
  format: "plain",
3155
3517
  compaction: { ...report, createdAt, current },
@@ -3190,14 +3552,15 @@ function thinkingLine(text, live = false) {
3190
3552
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
3191
3553
  }
3192
3554
  function formatToolUse(toolUse) {
3555
+ const activity = toolActivity(toolUse.input, "running");
3193
3556
  const toolPresentation = toolPresentationForSource(toolUse.name);
3194
3557
  const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
3195
3558
  if (toolUse.name === "plan_update" && isWebPlanPayload(toolUse.input))
3196
- return { kind: "tool", toolName: toolUse.name, toolPresentation, title: toolPresentation.label, bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
3559
+ return { kind: "tool", activity, toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
3197
3560
  if (toolUse.name === "image_create")
3198
- return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", presentationLevel: "primary", collapsible: true };
3561
+ return { kind: "tool", activity, toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", presentationLevel: "primary", collapsible: true };
3199
3562
  const summary = summarizeToolUse(toolUse.name, toolUse.input);
3200
- return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
3563
+ return { kind: "tool", activity, toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
3201
3564
  }
3202
3565
  function formatToolResultLine(toolName, output, ok, presentToolResult) {
3203
3566
  // Hidden fallback reports in historical agent payloads must not enter line.text
@@ -3212,6 +3575,7 @@ function formatToolResultLine(toolName, output, ok, presentToolResult) {
3212
3575
  : formatToolResult(toolName, toolName.startsWith("subagent_") ? redactToolDetail(output) : output, ok);
3213
3576
  return {
3214
3577
  kind: ok ? "tool" : "error",
3578
+ observation: toolObservationResult(toolName, output),
3215
3579
  toolError: ok ? undefined : toolErrorText(output, ok).slice(0, 2000),
3216
3580
  toolName,
3217
3581
  toolPresentation: toolPresentationForSource(toolName),