neoctl 0.2.35 → 0.2.37

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 (180) hide show
  1. package/dist/agents/agent-report-security.test.d.ts +1 -0
  2. package/dist/agents/agent-report-security.test.js +301 -0
  3. package/dist/agents/agent-report-security.test.js.map +1 -0
  4. package/dist/agents/agent-tool-persistence.test.d.ts +1 -0
  5. package/dist/agents/agent-tool-persistence.test.js +319 -0
  6. package/dist/agents/agent-tool-persistence.test.js.map +1 -0
  7. package/dist/agents/agent-tool.d.ts +3 -0
  8. package/dist/agents/agent-tool.js +246 -76
  9. package/dist/agents/agent-tool.js.map +1 -1
  10. package/dist/agents/live-preview-redaction-security.test.d.ts +1 -0
  11. package/dist/agents/live-preview-redaction-security.test.js +286 -0
  12. package/dist/agents/live-preview-redaction-security.test.js.map +1 -0
  13. package/dist/agents/local-agent-task.d.ts +68 -1
  14. package/dist/agents/local-agent-task.js +78 -5
  15. package/dist/agents/local-agent-task.js.map +1 -1
  16. package/dist/agents/no-nested-delegation.test.d.ts +1 -0
  17. package/dist/agents/no-nested-delegation.test.js +38 -0
  18. package/dist/agents/no-nested-delegation.test.js.map +1 -0
  19. package/dist/agents/obs07-11-run-facts.test.d.ts +1 -0
  20. package/dist/agents/obs07-11-run-facts.test.js +459 -0
  21. package/dist/agents/obs07-11-run-facts.test.js.map +1 -0
  22. package/dist/agents/smoke-agent-lifecycle.d.ts +1 -0
  23. package/dist/agents/smoke-agent-lifecycle.js +192 -0
  24. package/dist/agents/smoke-agent-lifecycle.js.map +1 -0
  25. package/dist/agents/smoke-agents.js +7 -3
  26. package/dist/agents/smoke-agents.js.map +1 -1
  27. package/dist/context/context-manager.d.ts +1 -0
  28. package/dist/context/context-manager.js +10 -6
  29. package/dist/context/context-manager.js.map +1 -1
  30. package/dist/context/smoke-context.js +46 -3
  31. package/dist/context/smoke-context.js.map +1 -1
  32. package/dist/core/image-registry.js +1 -1
  33. package/dist/core/image-registry.js.map +1 -1
  34. package/dist/core/obs04-visible-data.test.d.ts +1 -0
  35. package/dist/core/obs04-visible-data.test.js +147 -0
  36. package/dist/core/obs04-visible-data.test.js.map +1 -0
  37. package/dist/core/query-engine.d.ts +5 -0
  38. package/dist/core/query-engine.js +36 -3
  39. package/dist/core/query-engine.js.map +1 -1
  40. package/dist/core/query.d.ts +5 -1
  41. package/dist/core/query.js +120 -18
  42. package/dist/core/query.js.map +1 -1
  43. package/dist/core/run-agent-pause.test.d.ts +1 -0
  44. package/dist/core/run-agent-pause.test.js +52 -0
  45. package/dist/core/run-agent-pause.test.js.map +1 -0
  46. package/dist/core/run-agent-persistence.test.d.ts +1 -0
  47. package/dist/core/run-agent-persistence.test.js +222 -0
  48. package/dist/core/run-agent-persistence.test.js.map +1 -0
  49. package/dist/core/run-agent.d.ts +13 -0
  50. package/dist/core/run-agent.js +72 -17
  51. package/dist/core/run-agent.js.map +1 -1
  52. package/dist/core/smoke-core-loop.js +8 -7
  53. package/dist/core/smoke-core-loop.js.map +1 -1
  54. package/dist/model/model-gateway.d.ts +1 -0
  55. package/dist/model/openai-native-image-guard.test.d.ts +1 -0
  56. package/dist/model/openai-native-image-guard.test.js +101 -0
  57. package/dist/model/openai-native-image-guard.test.js.map +1 -0
  58. package/dist/model/openai-responses-mapper.js +91 -16
  59. package/dist/model/openai-responses-mapper.js.map +1 -1
  60. package/dist/plugins/smoke-plugin-system.js +14 -14
  61. package/dist/repl/index.js +22 -28
  62. package/dist/repl/index.js.map +1 -1
  63. package/dist/secrets/secret-redaction.d.ts +6 -5
  64. package/dist/secrets/secret-redaction.js +7 -79
  65. package/dist/secrets/secret-redaction.js.map +1 -1
  66. package/dist/secrets/secret-types.d.ts +4 -1
  67. package/dist/secrets/secret-types.js.map +1 -1
  68. package/dist/session/session-store-safety.test.d.ts +1 -0
  69. package/dist/session/session-store-safety.test.js +107 -0
  70. package/dist/session/session-store-safety.test.js.map +1 -0
  71. package/dist/session/session-store.d.ts +5 -1
  72. package/dist/session/session-store.js +69 -7
  73. package/dist/session/session-store.js.map +1 -1
  74. package/dist/session/tool-result-memory.d.ts +3 -1
  75. package/dist/session/tool-result-memory.js.map +1 -1
  76. package/dist/tasks/subagent-tools.d.ts +4 -1
  77. package/dist/tasks/subagent-tools.js +85 -24
  78. package/dist/tasks/subagent-tools.js.map +1 -1
  79. package/dist/tasks/subagent-tools.test.d.ts +1 -0
  80. package/dist/tasks/subagent-tools.test.js +159 -0
  81. package/dist/tasks/subagent-tools.test.js.map +1 -0
  82. package/dist/tasks/task-ack-size.test.d.ts +1 -0
  83. package/dist/tasks/task-ack-size.test.js +101 -0
  84. package/dist/tasks/task-ack-size.test.js.map +1 -0
  85. package/dist/tasks/task-persistence.d.ts +23 -0
  86. package/dist/tasks/task-persistence.js +216 -0
  87. package/dist/tasks/task-persistence.js.map +1 -0
  88. package/dist/tasks/task-persistence.test.d.ts +1 -0
  89. package/dist/tasks/task-persistence.test.js +331 -0
  90. package/dist/tasks/task-persistence.test.js.map +1 -0
  91. package/dist/tasks/task-store.d.ts +51 -7
  92. package/dist/tasks/task-store.js +333 -31
  93. package/dist/tasks/task-store.js.map +1 -1
  94. package/dist/tools/builtins/exec-process-manager.d.ts +97 -2
  95. package/dist/tools/builtins/exec-process-manager.js +218 -19
  96. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  97. package/dist/tools/builtins/exec-tool.js +14 -1
  98. package/dist/tools/builtins/exec-tool.js.map +1 -1
  99. package/dist/tools/builtins/image-generation-tool.js +24 -24
  100. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  101. package/dist/tools/smoke-exec-process.js +2 -1
  102. package/dist/tools/smoke-exec-process.js.map +1 -1
  103. package/dist/tools/smoke-terminal-output-chain.d.ts +1 -0
  104. package/dist/tools/smoke-terminal-output-chain.js +245 -0
  105. package/dist/tools/smoke-terminal-output-chain.js.map +1 -0
  106. package/dist/tools/smoke-tool-system.js +6 -6
  107. package/dist/tools/smoke-tool-system.js.map +1 -1
  108. package/dist/tools/terminal-output-persistence.test.d.ts +1 -0
  109. package/dist/tools/terminal-output-persistence.test.js +50 -0
  110. package/dist/tools/terminal-output-persistence.test.js.map +1 -0
  111. package/dist/tools/terminal-output-store.d.ts +150 -0
  112. package/dist/tools/terminal-output-store.js +562 -0
  113. package/dist/tools/terminal-output-store.js.map +1 -0
  114. package/dist/tools/terminal-output-store.test.d.ts +1 -0
  115. package/dist/tools/terminal-output-store.test.js +364 -0
  116. package/dist/tools/terminal-output-store.test.js.map +1 -0
  117. package/dist/tools/tool.d.ts +5 -0
  118. package/dist/tools/tool.js.map +1 -1
  119. package/dist/types/events.d.ts +1 -0
  120. package/dist/types/messages.d.ts +1 -0
  121. package/dist/web/agent-content-detail.d.ts +78 -0
  122. package/dist/web/agent-content-detail.js +495 -0
  123. package/dist/web/agent-content-detail.js.map +1 -0
  124. package/dist/web/agent-content-detail.test.d.ts +1 -0
  125. package/dist/web/agent-content-detail.test.js +307 -0
  126. package/dist/web/agent-content-detail.test.js.map +1 -0
  127. package/dist/web/agent-parent-messages.test.d.ts +1 -0
  128. package/dist/web/agent-parent-messages.test.js +58 -0
  129. package/dist/web/agent-parent-messages.test.js.map +1 -0
  130. package/dist/web/agent-tool-payload-security.test.d.ts +1 -0
  131. package/dist/web/agent-tool-payload-security.test.js +120 -0
  132. package/dist/web/agent-tool-payload-security.test.js.map +1 -0
  133. package/dist/web/index.d.ts +213 -9
  134. package/dist/web/index.js +260 -108
  135. package/dist/web/index.js.map +1 -1
  136. package/dist/web/smoke-agent-content-http.d.ts +1 -0
  137. package/dist/web/smoke-agent-content-http.js +446 -0
  138. package/dist/web/smoke-agent-content-http.js.map +1 -0
  139. package/dist/web/smoke-status-semantics.d.ts +1 -0
  140. package/dist/web/smoke-status-semantics.js +90 -0
  141. package/dist/web/smoke-status-semantics.js.map +1 -0
  142. package/dist/web/smoke-task-session-entrypoints.d.ts +1 -0
  143. package/dist/web/smoke-task-session-entrypoints.js +96 -0
  144. package/dist/web/smoke-task-session-entrypoints.js.map +1 -0
  145. package/dist/web/smoke-terminal-output-http.d.ts +1 -0
  146. package/dist/web/smoke-terminal-output-http.js +372 -0
  147. package/dist/web/smoke-terminal-output-http.js.map +1 -0
  148. package/dist/web/smoke-tool-call-detail.d.ts +1 -0
  149. package/dist/web/smoke-tool-call-detail.js +106 -0
  150. package/dist/web/smoke-tool-call-detail.js.map +1 -0
  151. package/dist/web/smoke-tool-call-http.d.ts +1 -0
  152. package/dist/web/smoke-tool-call-http.js +100 -0
  153. package/dist/web/smoke-tool-call-http.js.map +1 -0
  154. package/dist/web/smoke-tool-detail-fields-http.d.ts +1 -0
  155. package/dist/web/smoke-tool-detail-fields-http.js +121 -0
  156. package/dist/web/smoke-tool-detail-fields-http.js.map +1 -0
  157. package/dist/web/smoke-web-agent-tasks.d.ts +1 -0
  158. package/dist/web/smoke-web-agent-tasks.js +50 -0
  159. package/dist/web/smoke-web-agent-tasks.js.map +1 -0
  160. package/dist/web/smoke-web-terminal-tasks.js +126 -76
  161. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  162. package/dist/web/status-semantics.d.ts +11 -0
  163. package/dist/web/status-semantics.js +59 -0
  164. package/dist/web/status-semantics.js.map +1 -0
  165. package/dist/web/terminal-presentation.d.ts +15 -0
  166. package/dist/web/terminal-presentation.js +61 -0
  167. package/dist/web/terminal-presentation.js.map +1 -0
  168. package/dist/web/terminal-presentation.test.d.ts +1 -0
  169. package/dist/web/terminal-presentation.test.js +33 -0
  170. package/dist/web/terminal-presentation.test.js.map +1 -0
  171. package/dist/web/tool-call-detail.d.ts +40 -0
  172. package/dist/web/tool-call-detail.js +167 -0
  173. package/dist/web/tool-call-detail.js.map +1 -0
  174. package/dist/web/tool-detail-fields.d.ts +59 -0
  175. package/dist/web/tool-detail-fields.js +119 -0
  176. package/dist/web/tool-detail-fields.js.map +1 -0
  177. package/dist/web/tool-detail-fields.test.d.ts +1 -0
  178. package/dist/web/tool-detail-fields.test.js +121 -0
  179. package/dist/web/tool-detail-fields.test.js.map +1 -0
  180. package/package.json +90 -90
package/dist/web/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
2
3
  import http from "node:http";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
@@ -31,6 +32,10 @@ import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/comm
31
32
  import { writeSessionMarkdownExport } from "../session/session-export.js";
32
33
  import { DefaultContextManager } from "../context/context-manager.js";
33
34
  import { createImageId } from "../types/messages.js";
35
+ import { readToolCallDetail, toolErrorText, allowToolDetailRequest, sanitizeAgentToolPayload, redactToolDetail } from "./tool-call-detail.js";
36
+ import { terminalFacts, terminalPreviews, terminalResultText } from "./terminal-presentation.js";
37
+ import { createAgentContentDetailResolver } from "./agent-content-detail.js";
38
+ import { toolDetailFieldsFromDetail } from "./tool-detail-fields.js";
34
39
  import { WEB_HTML } from "./html.js";
35
40
  import { openDirectory } from "../open-directory.js";
36
41
  import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
@@ -167,14 +172,10 @@ export async function createWebRuntime(options = {}) {
167
172
  tools.register(tool);
168
173
  const agentRuntime = { modelGateway, tools, taskStore };
169
174
  registerTool(createAgentTool(agentRuntime), { source: "builtin" });
175
+ let runtime;
170
176
  const resumeHandler = async (taskId, directive) => {
171
- const dummyContext = {
172
- agentId: "main",
173
- tools,
174
- appState: new InMemoryAppState("main", options.cwd),
175
- emit: () => undefined,
176
- };
177
- return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
177
+ const parentContext = createResumeParentContext(runtime.engine, tools, options.cwd);
178
+ return resumeAgentTask(taskId, directive, agentRuntime, taskStore, parentContext);
178
179
  };
179
180
  for (const tool of createSubagentTools(taskStore, resumeHandler))
180
181
  registerTool(tool, { source: "builtin" });
@@ -210,8 +211,9 @@ export async function createWebRuntime(options = {}) {
210
211
  },
211
212
  });
212
213
  await engine.initialize();
214
+ activateSession({ engine, taskStore });
213
215
  const initialMetrics = await engine.contextMetrics();
214
- return {
216
+ runtime = {
215
217
  engine,
216
218
  appPromptStore,
217
219
  communicationLogger,
@@ -242,6 +244,7 @@ export async function createWebRuntime(options = {}) {
242
244
  resolveSession: options.resolveSessionToolOverrides,
243
245
  },
244
246
  };
247
+ return runtime;
245
248
  }
246
249
  function normalizeWebRuntimePlugins(value) {
247
250
  if (!Array.isArray(value))
@@ -274,10 +277,46 @@ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides
274
277
  for (const tool of catalog)
275
278
  registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
276
279
  }
277
- function createTaskNotificationSource(taskStore) {
280
+ /** Bind only the foreground view; never stop work owned by another session. */
281
+ export function activateSession(runtime) {
282
+ const sessionDir = runtime.engine.snapshot().session?.sessionDir || undefined;
283
+ const summary = runtime.taskStore.bindSession?.(sessionDir);
284
+ const interruptedTasks = summary?.loaded ? runtime.taskStore.recoverableInterruptedTasks?.(sessionDir) ?? [] : [];
285
+ if (interruptedTasks.length) {
286
+ runtime.engine.noteRecoverableSubagents(() => {
287
+ for (const task of interruptedTasks)
288
+ runtime.taskStore.markNotified(task.taskId);
289
+ });
290
+ }
291
+ }
292
+ export function sessionAgentTasks(runtime) {
293
+ const dir = runtime.engine.snapshot().session?.sessionDir || undefined;
294
+ // Older embedders provide minimal TaskStore mocks with no persistence API.
295
+ return runtime.taskStore.list().filter((task) => !runtime.taskStore.bindSession
296
+ || task.ownerSessionDir === (dir ? path.resolve(dir) : undefined));
297
+ }
298
+ export function createResumeParentContext(engine, tools, cwd) {
299
+ const snapshot = engine.snapshot();
300
+ const session = snapshot.session;
278
301
  return {
279
- collectUnnotifiedCompletions() {
280
- return taskStore.collectUnnotifiedCompletions().map((task) => ({
302
+ agentId: snapshot.agentId ?? "main",
303
+ tools,
304
+ appState: new InMemoryAppState(snapshot.agentId ?? "main", cwd),
305
+ session: session?.sessionDir ? {
306
+ sessionId: session.sessionId,
307
+ sessionDir: session.sessionDir,
308
+ rootDir: path.dirname(session.sessionDir),
309
+ } : undefined,
310
+ options: { mainLoopModel: snapshot.model, reasoning: snapshot.reasoning,
311
+ contextWindowTokensOverride: snapshot.contextWindowTokensOverride },
312
+ emit: () => undefined,
313
+ };
314
+ }
315
+ export function createTaskNotificationSource(taskStore) {
316
+ return {
317
+ collectUnnotifiedCompletions(sessionDir) {
318
+ return taskStore.collectUnnotifiedCompletions(sessionDir).filter((task) => !taskStore.bindSession
319
+ || task.ownerSessionDir === (sessionDir ? path.resolve(sessionDir) : undefined)).map((task) => ({
281
320
  taskId: task.taskId,
282
321
  agentId: task.agentId,
283
322
  status: task.status,
@@ -364,6 +403,7 @@ function webRuntimeScopeKey(scope) {
364
403
  }
365
404
  export class WebRepl {
366
405
  runtime;
406
+ agentContentReader = createAgentContentDetailResolver();
367
407
  subscribers = new Set();
368
408
  eventSequence = 0;
369
409
  pendingDeltaOperations = [];
@@ -389,7 +429,6 @@ export class WebRepl {
389
429
  pendingUserImageEchoMessageId;
390
430
  runtimeContextRevision = 0;
391
431
  runtimeContextPublishQueue = Promise.resolve();
392
- pendingRuntimeConfiguration = false;
393
432
  backgroundSessionRuns = new Map();
394
433
  suppressReattachedStreaming = new Set();
395
434
  constructor(runtime) {
@@ -433,18 +472,53 @@ export class WebRepl {
433
472
  this.send(subscriber, "client.reload", payload);
434
473
  return subscribers.length;
435
474
  }
475
+ async toolCallDetail(sessionId, toolUseId, messageId) {
476
+ const session = this.runtime.engine.snapshot().session;
477
+ const detail = await readToolCallDetail({ sessionId: session?.sessionId ?? "", expectedSessionId: sessionId, sessionDir: session?.sessionDir, toolUseId, messageId, redact: value => this.runtime.engine.redactDisplayValue(value), entries: this.runtime.engine.getDisplayEntries() });
478
+ return detail ? this.runtime.engine.redactDisplayValue({ ...detail, fields: toolDetailFieldsFromDetail(detail) }) : undefined;
479
+ }
480
+ async agentContent(sessionId, view, request) {
481
+ const session = this.runtime.engine.snapshot().session;
482
+ if (!session?.sessionDir || sessionId !== session.sessionId || !["timeline", "delegation", "report", "messages"].includes(view))
483
+ return undefined;
484
+ // Loading/recovery belongs to activateSession, never this read-only route.
485
+ if (!this.runtime.taskStore.getInSession(request.taskId, session.sessionDir))
486
+ return undefined;
487
+ return this.agentContentReader[view]({
488
+ ownerSessionId: sessionId, ownerSessionDir: session.sessionDir, taskStore: this.runtime.taskStore,
489
+ redact: value => this.runtime.engine.redactDisplayValue(value),
490
+ }, request);
491
+ }
492
+ terminalOutput(sessionId, runId, stream, offset, limitBytes) {
493
+ const session = this.runtime.engine.snapshot().session;
494
+ if (!session?.sessionDir || !sessionId || sessionId !== session.sessionId || !runId || !["stdout", "stderr"].includes(stream))
495
+ return undefined;
496
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limitBytes) || limitBytes < 4 || limitBytes > 262144)
497
+ return undefined;
498
+ const manager = this.runtime.execProcessManager;
499
+ const registered = manager.registerOwnerSession(sessionId, session.sessionDir);
500
+ if (!registered.ok)
501
+ return undefined;
502
+ const page = manager.readOutput(sessionId, runId, { stream: stream, offset, limitBytes });
503
+ // Cursor addresses stored bytes, not the possibly additionally redacted response length.
504
+ return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
505
+ }
436
506
  snapshot(includeCatalog = false) {
437
507
  const backgroundTasks = this.backgroundTasks();
508
+ const engineSnapshot = this.runtime.engine.snapshot();
438
509
  return {
439
- lines: this.lines,
510
+ lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
440
511
  status: this.status,
441
512
  busy: this.busy,
442
513
  queuedInput: this.queuedInput,
443
514
  backgroundTaskCount: backgroundTasks.length,
444
515
  backgroundTasks,
516
+ terminalTaskHistory: this.terminalTaskHistory(),
517
+ agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((task) => this.agentTaskSnapshot(task)),
445
518
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
446
519
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
447
- session: this.runtime.engine.snapshot().session,
520
+ session: engineSnapshot.session,
521
+ modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
448
522
  fastMode: this.runtime.engine.isFastMode(),
449
523
  appPrompt: this.runtime.engine.getAppPrompt(),
450
524
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
@@ -514,6 +588,8 @@ export class WebRepl {
514
588
  };
515
589
  }
516
590
  async setGlobalTools(value) {
591
+ if (this.busy)
592
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change global tools while a response is running");
517
593
  if (!isRecord(value))
518
594
  return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
519
595
  const support = this.runtime.toolSupport;
@@ -524,12 +600,10 @@ export class WebRepl {
524
600
  return normalized;
525
601
  try {
526
602
  support.globalOverrides = normalized.value;
603
+ this.syncPluginToolRegistration();
604
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
527
605
  if (support.persistGlobal)
528
606
  await support.persistGlobal(support.globalOverrides);
529
- if (this.busy)
530
- this.pendingRuntimeConfiguration = true;
531
- else
532
- await this.applyRuntimeConfiguration();
533
607
  await this.refreshToolConfiguration();
534
608
  return { ok: true, state: this.globalTools() };
535
609
  }
@@ -538,6 +612,8 @@ export class WebRepl {
538
612
  }
539
613
  }
540
614
  async setSessionTools(value) {
615
+ if (this.busy)
616
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
541
617
  if (!isRecord(value))
542
618
  return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
543
619
  const support = this.runtime.toolSupport;
@@ -561,6 +637,8 @@ export class WebRepl {
561
637
  await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
562
638
  }
563
639
  async setSessionPlugins(value) {
640
+ if (this.busy)
641
+ return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
564
642
  if (!isRecord(value))
565
643
  return actionFailure("INVALID_REQUEST", "plugin overrides must be an object");
566
644
  const support = this.runtime.pluginSupport;
@@ -624,13 +702,11 @@ export class WebRepl {
624
702
  if (!support)
625
703
  return;
626
704
  support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
705
+ this.syncPluginToolRegistration();
706
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
627
707
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
628
708
  if (persist && sessionId && support.persistSession)
629
709
  await support.persistSession(sessionId, support.sessionOverrides);
630
- if (this.busy)
631
- this.pendingRuntimeConfiguration = true;
632
- else
633
- await this.applyRuntimeConfiguration();
634
710
  await this.refreshToolConfiguration();
635
711
  }
636
712
  syncPluginToolRegistration() {
@@ -645,18 +721,6 @@ export class WebRepl {
645
721
  this.runtime.tools.register(tool);
646
722
  }
647
723
  }
648
- async applyRuntimeConfiguration() {
649
- this.syncPluginToolRegistration();
650
- const pluginSupport = this.runtime.pluginSupport;
651
- if (pluginSupport) {
652
- const activePlugins = activeWebRuntimePlugins(pluginSupport.catalog, pluginSupport.overrides);
653
- this.runtime.engine.setRuntimePlugins([...pluginSupport.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...pluginSupport.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
654
- }
655
- const toolSupport = this.runtime.toolSupport;
656
- if (toolSupport)
657
- applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
658
- this.pendingRuntimeConfiguration = false;
659
- }
660
724
  async refreshToolConfiguration() {
661
725
  const metrics = await this.runtime.engine.contextMetrics();
662
726
  this.runtime.initialMetrics = metrics;
@@ -669,14 +733,21 @@ export class WebRepl {
669
733
  if (!support)
670
734
  return;
671
735
  const normalized = normalizePluginOverrides(overrides, support.catalog);
736
+ for (const plugin of support.catalog)
737
+ for (const tool of plugin.tools)
738
+ this.runtime.tools.unregister(tool.name);
739
+ const activePlugins = activeWebRuntimePlugins(support.catalog, normalized);
740
+ for (const plugin of activePlugins)
741
+ for (const tool of plugin.tools)
742
+ this.runtime.tools.register(tool);
672
743
  support.overrides = normalized;
744
+ const toolSupport = this.runtime.toolSupport;
745
+ if (toolSupport)
746
+ applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
747
+ this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
673
748
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
674
749
  if (persist && sessionId && support.persist)
675
750
  await support.persist(sessionId, normalized);
676
- if (this.busy)
677
- this.pendingRuntimeConfiguration = true;
678
- else
679
- await this.applyRuntimeConfiguration();
680
751
  const metrics = await this.runtime.engine.contextMetrics();
681
752
  this.runtime.initialMetrics = metrics;
682
753
  this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
@@ -727,11 +798,7 @@ export class WebRepl {
727
798
  this.queuedAttachments = [...(this.queuedAttachments ?? []), ...attachments].slice(0, 20);
728
799
  }
729
800
  startRun(text, attachments = []) {
730
- const run = (async () => {
731
- if (this.pendingRuntimeConfiguration)
732
- await this.applyRuntimeConfiguration();
733
- await this.handleCommandOrPrompt(text, attachments);
734
- })().catch((error) => {
801
+ const run = this.handleCommandOrPrompt(text, attachments).catch((error) => {
735
802
  this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
736
803
  this.setBusy(false);
737
804
  this.setStatus({ ...this.status, phase: "ready", detail: undefined });
@@ -759,6 +826,7 @@ export class WebRepl {
759
826
  await this.detachRunningForeground("session switch");
760
827
  this.runtime.engine = this.runtime.engine.forkForSession(sessionId, true);
761
828
  await this.runtime.engine.initialize();
829
+ activateSession(this.runtime);
762
830
  const snapshot = this.runtime.engine.snapshot().session;
763
831
  if (!snapshot)
764
832
  throw new Error("session transcripts are disabled");
@@ -776,6 +844,7 @@ export class WebRepl {
776
844
  await this.detachRunningForeground("new session");
777
845
  this.runtime.engine = this.runtime.engine.forkForSession(undefined, false);
778
846
  await this.runtime.engine.initialize();
847
+ activateSession(this.runtime);
779
848
  const snapshot = this.runtime.engine.snapshot().session;
780
849
  if (!snapshot)
781
850
  throw new Error("session transcripts are disabled");
@@ -892,6 +961,8 @@ export class WebRepl {
892
961
  }
893
962
  }
894
963
  async setContextWindowK(value) {
964
+ if (this.busy)
965
+ return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
895
966
  if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
896
967
  return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
897
968
  }
@@ -1017,24 +1088,77 @@ export class WebRepl {
1017
1088
  this.broadcastSync();
1018
1089
  return runWasActive;
1019
1090
  }
1091
+ agentTaskSnapshot(task) {
1092
+ const terminal = this.runtime.taskStore.isTerminal(task);
1093
+ // Redact complete values before any public preview boundary: clipping first can expose
1094
+ // a credential prefix which no longer matches the runtime registry's full value.
1095
+ const safeText = (value) => value === undefined ? undefined : this.runtime.engine.redactDisplayValue(value);
1096
+ const result = (value, limit = 1500) => {
1097
+ if (!value)
1098
+ return undefined;
1099
+ if (value.displaySource !== "agent_report" && value.displaySource !== "visible_text") {
1100
+ return { content: "", truncated: false, status: value.status, availability: "unavailable", reason: "旧报告缺少获准展示来源;不会公开未验证的文本" };
1101
+ }
1102
+ const content = safeText(value.content);
1103
+ return { content: content.slice(0, limit), truncated: content.length > limit, status: value.status };
1104
+ };
1105
+ const error = safeText(task.error);
1106
+ return this.runtime.engine.redactDisplayValue({
1107
+ kind: "agent", taskId: task.taskId, agentId: task.agentId,
1108
+ agentType: task.agentType, type: task.type, status: task.status,
1109
+ description: task.description, runGeneration: task.runGeneration ?? 1,
1110
+ progress: { visibleText: task.progress.visibleText?.channel === "visible" && task.progress.visibleText.redactionVersion === 1 && task.progress.visibleText.runGeneration === task.runGeneration ? this.runtime.engine.redactDisplayValue(task.progress.visibleText) : undefined, lastActivity: task.progress.lastActivity, currentAction: task.progress.currentAction, totalToolUseCount: task.progress.totalToolUseCount, steps: task.progress.steps?.slice(-8).map((step) => ({ id: step.id, title: step.title, status: step.status })) },
1111
+ pendingMessageCount: task.pendingMessages.length,
1112
+ deliveredRetainedThisRun: task.messageReceipts?.filter((receipt) => receipt.status === "delivered" && receipt.runGeneration === task.runGeneration).length,
1113
+ messageReceipts: (task.messageReceipts ?? []).slice(-20).map(({ id, status, queuedAt, deliveredAt, runGeneration }) => ({ id, status, queuedAt, deliveredAt, runGeneration })),
1114
+ result: terminal ? result(task.result) : undefined,
1115
+ error: terminal ? error?.slice(0, 1500) : undefined,
1116
+ errorTruncated: terminal && (error?.length ?? 0) > 1500,
1117
+ runHistory: (task.runHistory ?? []).map((run) => {
1118
+ const archivedError = safeText(run.error);
1119
+ return { runGeneration: run.runGeneration, status: run.status, startedAt: run.startedAt, durationMs: run.durationMs, completedAt: run.completedAt, result: result(run.result, 600), error: archivedError?.slice(0, 600), errorTruncated: (archivedError?.length ?? 0) > 600 };
1120
+ }),
1121
+ createdAt: task.createdAt, updatedAt: task.updatedAt, startedAt: task.startedAt, completedAt: task.completedAt, durationMs: task.durationMs,
1122
+ });
1123
+ }
1124
+ terminalTaskHistory() {
1125
+ const session = this.runtime.engine.snapshot().session;
1126
+ if (!session?.sessionId || !session.sessionDir)
1127
+ return [];
1128
+ const manager = this.runtime.execProcessManager;
1129
+ if (!manager.registerOwnerSession?.(session.sessionId, session.sessionDir).ok)
1130
+ return [];
1131
+ const records = [];
1132
+ let offset = 0;
1133
+ do {
1134
+ const history = manager.listHistory(session.sessionId, { offset, limit: 200 });
1135
+ if (!history.ok)
1136
+ break;
1137
+ records.push(...history.value.records);
1138
+ if (history.value.nextOffset === null)
1139
+ break;
1140
+ offset = history.value.nextOffset;
1141
+ } while (true);
1142
+ return records.reverse().map(({ record }) => ({
1143
+ kind: "terminal", taskId: `terminal:${record.runId}`, type: "终端",
1144
+ ownerSessionId: session.sessionId, sessionId: record.runId,
1145
+ status: record.lifecycle === "lost" ? "lost" : record.exit?.status ?? "unknown",
1146
+ description: record.metadata.description || record.metadata.command || "终端运行",
1147
+ command: record.metadata.command, cwd: record.metadata.cwd, shell: record.metadata.shell,
1148
+ tty: record.metadata.tty, processId: record.metadata.processId,
1149
+ createdAt: record.metadata.startedAt, completedAt: record.exit?.finishedAt,
1150
+ durationMs: record.exit?.durationMs, exitCode: record.exit?.exitCode,
1151
+ signal: record.exit?.signal, terminationReason: record.exit?.terminationReason,
1152
+ outputAvailability: record.availability, outputTruncated: record.truncated,
1153
+ metadataTruncated: record.metadata.truncatedFields,
1154
+ })).map(task => this.runtime.engine.redactDisplayValue(task));
1155
+ }
1020
1156
  backgroundTasks() {
1021
- const agentTasks = this.runtime.taskStore.list()
1157
+ const agentTasks = sessionAgentTasks(this.runtime)
1022
1158
  .filter((task) => !this.runtime.taskStore.isTerminal(task))
1023
- .map((task) => ({
1024
- kind: "agent",
1025
- taskId: task.taskId,
1026
- agentId: task.agentId,
1027
- agentType: task.agentType,
1028
- type: task.type,
1029
- status: task.status,
1030
- description: task.description,
1031
- prompt: task.prompt,
1032
- progress: task.progress,
1033
- outputFile: task.outputFile,
1034
- createdAt: task.createdAt,
1035
- updatedAt: task.updatedAt,
1036
- }));
1037
- const terminalTasks = this.runtime.execProcessManager.list()
1159
+ .map((task) => this.agentTaskSnapshot(task));
1160
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1161
+ const terminalTasks = (ownerSessionId ? this.runtime.execProcessManager.list(ownerSessionId) : [])
1038
1162
  .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
1039
1163
  .map((terminal) => ({
1040
1164
  kind: "terminal",
@@ -1042,7 +1166,8 @@ export class WebRepl {
1042
1166
  type: "终端",
1043
1167
  status: terminal.status,
1044
1168
  description: terminal.description || terminal.command,
1045
- createdAt: Date.now() - terminal.duration_ms,
1169
+ createdAt: terminal.started_at,
1170
+ ownerSessionId,
1046
1171
  sessionId: terminal.session_id,
1047
1172
  processId: terminal.process_id,
1048
1173
  command: terminal.command,
@@ -1050,8 +1175,11 @@ export class WebRepl {
1050
1175
  shell: terminal.shell,
1051
1176
  tty: terminal.tty,
1052
1177
  durationMs: terminal.duration_ms,
1053
- output: terminal.output,
1054
- outputEnd: terminal.outputEnd,
1178
+ // Output is loaded from the owner-bound reader, never copied into state snapshots.
1179
+ exitCode: terminal.exit_code,
1180
+ signal: terminal.signal,
1181
+ terminationReason: terminal.termination_reason,
1182
+ outputAvailability: terminal.output_ref.availability,
1055
1183
  }));
1056
1184
  const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1057
1185
  kind: "session",
@@ -1108,6 +1236,7 @@ export class WebRepl {
1108
1236
  await this.detachRunningForeground("session switch");
1109
1237
  this.backgroundSessionRuns.delete(run.sessionId);
1110
1238
  this.runtime.engine = run.engine;
1239
+ activateSession(this.runtime);
1111
1240
  this.activeAbortController = run.abortController;
1112
1241
  this.interruptArmed = false;
1113
1242
  this.foregroundRun = run.promise;
@@ -1260,7 +1389,7 @@ export class WebRepl {
1260
1389
  return liveLineId;
1261
1390
  }, runtimeToolResultPresenter(this.runtime));
1262
1391
  }
1263
- const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: event.ok ? "completed" : "failed" } : step);
1392
+ const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "unknown" } : step);
1264
1393
  if (replacement) {
1265
1394
  this.replaceLine(liveLineId, {
1266
1395
  ...replacement,
@@ -1430,6 +1559,7 @@ export class WebRepl {
1430
1559
  return void this.append({ kind: "system", text: formatUsageTotals(this.runtime.usage.snapshot()), previewStyle: "summary" });
1431
1560
  if (command.type === "reset") {
1432
1561
  this.runtime.engine.reset();
1562
+ activateSession(this.runtime);
1433
1563
  this.runtime.usage.reset();
1434
1564
  this.status = await resetStatus(this.runtime);
1435
1565
  this.append(systemLine("transcript reset"));
@@ -1684,6 +1814,10 @@ export class WebRepl {
1684
1814
  this.queueDeltaOperation(operation);
1685
1815
  }
1686
1816
  queueDeltaOperation(operation) {
1817
+ if (operation.type === "line.append" && operation.line.toolError)
1818
+ operation = { ...operation, line: { ...operation.line, toolError: this.runtime.engine.redactDisplayValue?.(operation.line.toolError) ?? operation.line.toolError } };
1819
+ if (operation.type === "line.patch" && operation.patch.toolError)
1820
+ operation = { ...operation, patch: { ...operation.patch, toolError: this.runtime.engine.redactDisplayValue?.(operation.patch.toolError) ?? operation.patch.toolError } };
1687
1821
  this.pendingDeltaOperations.push(operation);
1688
1822
  this.scheduleDeltaFlush();
1689
1823
  }
@@ -1727,18 +1861,11 @@ export class WebRepl {
1727
1861
  }
1728
1862
  }
1729
1863
  queueTerminalOutput(delta) {
1730
- if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
1864
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1865
+ if (!this.subscribers.size || !ownerSessionId || delta.ownerSessionId !== ownerSessionId)
1731
1866
  return;
1732
- const outputStart = Number(delta.outputStart ?? 0);
1733
- const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
1734
- const pending = this.pendingTerminalOutput.get(delta.sessionId);
1735
- if (pending && pending.outputEnd === outputStart) {
1736
- pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
1737
- pending.outputEnd = outputEnd;
1738
- }
1739
- else {
1740
- this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
1741
- }
1867
+ // Invalidation only. The reader owns independent stream cursors; SSE is not a full-output source.
1868
+ this.pendingTerminalOutput.set(delta.sessionId, { ownerSessionId });
1742
1869
  if (this.terminalOutputTimer)
1743
1870
  return;
1744
1871
  this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
@@ -1746,12 +1873,12 @@ export class WebRepl {
1746
1873
  }
1747
1874
  flushTerminalOutput() {
1748
1875
  this.terminalOutputTimer = undefined;
1749
- if (!this.pendingTerminalOutput.size)
1750
- return;
1751
- const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
1876
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1877
+ const updates = [...this.pendingTerminalOutput.entries()].filter(([, output]) => output.ownerSessionId === ownerSessionId).map(([sessionId, output]) => ({ sessionId, ...output, invalidated: true }));
1752
1878
  this.pendingTerminalOutput.clear();
1753
- for (const subscriber of this.subscribers)
1754
- this.send(subscriber, "terminal.output", { updates });
1879
+ if (updates.length)
1880
+ for (const subscriber of this.subscribers)
1881
+ this.send(subscriber, "terminal.output", { updates });
1755
1882
  }
1756
1883
  broadcastSync() {
1757
1884
  this.discardPendingDeltaOperations();
@@ -1802,10 +1929,38 @@ async function route(req, res, router) {
1802
1929
  const body = await readJsonBody(req);
1803
1930
  return sendJson(res, await router.requestClientReload(body));
1804
1931
  }
1932
+ if (["/api/tool-call-detail", "/api/terminal-output", "/api/agent-content"].includes(url.pathname) && !allowToolDetailRequest(req.headers.origin, req.headers.host, req.headers["sec-fetch-site"])) {
1933
+ return sendJson(res, { error: "详情访问被拒绝" }, 403);
1934
+ }
1805
1935
  const scope = webRuntimeScopeFromUrl(url);
1806
1936
  const repl = await router.get(scope);
1807
1937
  if (req.method === "GET" && url.pathname === "/events")
1808
1938
  return repl.subscribe(res, router.clientInfo());
1939
+ if (req.method === "GET" && url.pathname === "/api/tool-call-detail") {
1940
+ res.setHeader("Cache-Control", "no-store");
1941
+ const detail = await repl.toolCallDetail(url.searchParams.get("sessionId") ?? "", url.searchParams.get("toolUseId") ?? "", url.searchParams.get("messageId") ?? undefined);
1942
+ if (!detail)
1943
+ return sendJson(res, { error: "调用详情不可获取或无权访问" }, 404);
1944
+ return sendJson(res, detail);
1945
+ }
1946
+ if (req.method === "GET" && url.pathname === "/api/agent-content") {
1947
+ res.setHeader("Cache-Control", "no-store");
1948
+ const refresh = url.searchParams.get("refresh");
1949
+ if (refresh !== null && refresh !== "true" && refresh !== "false")
1950
+ return sendJson(res, { error: "代理详情参数无效" }, 404);
1951
+ const page = await repl.agentContent(url.searchParams.get("sessionId") ?? "", url.searchParams.get("view") ?? "timeline", {
1952
+ taskId: url.searchParams.get("taskId") ?? "", runGeneration: Number(url.searchParams.get("runGeneration")),
1953
+ ...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
1954
+ ...(url.searchParams.has("pageChars") ? { pageChars: Number(url.searchParams.get("pageChars")) } : {}),
1955
+ ...(url.searchParams.has("refresh") ? { refresh: url.searchParams.get("refresh") === "true" } : {}),
1956
+ });
1957
+ return page ? sendJson(res, page) : sendJson(res, { error: "代理内容不可获取或无权访问" }, 404);
1958
+ }
1959
+ if (req.method === "GET" && url.pathname === "/api/terminal-output") {
1960
+ res.setHeader("Cache-Control", "no-store");
1961
+ 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));
1962
+ return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
1963
+ }
1809
1964
  if (req.method === "GET" && url.pathname === "/api/state")
1810
1965
  return sendJson(res, repl.snapshot(true));
1811
1966
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
@@ -2727,13 +2882,19 @@ function formatToolUse(toolUse) {
2727
2882
  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 };
2728
2883
  }
2729
2884
  function formatToolResultLine(toolName, output, ok, presentToolResult) {
2885
+ // Hidden fallback reports in historical agent payloads must not enter line.text
2886
+ // even when the frontend suppresses the ordinary result preview.
2887
+ const authorized = sanitizeAgentToolPayload(toolName, output);
2888
+ if (toolName.startsWith("subagent_"))
2889
+ output = authorized.value;
2730
2890
  const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
2731
2891
  const resources = presentation?.resources ?? toolResources(output);
2732
2892
  const formatted = resources?.length
2733
2893
  ? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
2734
- : formatToolResult(toolName, output, ok);
2894
+ : formatToolResult(toolName, toolName.startsWith("subagent_") ? redactToolDetail(output) : output, ok);
2735
2895
  return {
2736
2896
  kind: ok ? "tool" : "error",
2897
+ toolError: ok ? undefined : toolErrorText(output, ok).slice(0, 2000),
2737
2898
  toolName,
2738
2899
  toolPresentation: toolPresentationForSource(toolName),
2739
2900
  title: presentation?.title ?? toolTitle(toolName, "finished"),
@@ -2920,12 +3081,16 @@ function normalizeToolDisplay(display) {
2920
3081
  const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
2921
3082
  const facts = display.facts.filter((fact) => {
2922
3083
  const normalized = normalizeDisplayText(fact.value);
2923
- if (!normalized || visibleText.has(normalized))
3084
+ const identity = `${fact.label}\0${normalized}`;
3085
+ if (!normalized || visibleText.has(identity))
2924
3086
  return false;
2925
- visibleText.add(normalized);
3087
+ visibleText.add(identity);
2926
3088
  return true;
2927
3089
  });
2928
3090
  const previews = display.previews.filter((preview) => {
3091
+ // Keep identical nonempty streams, but never render empty output panels.
3092
+ if (/^(stdout|stderr)(?:$|[( ·])/.test(preview.label ?? ""))
3093
+ return !!preview.content.trim();
2929
3094
  const normalized = normalizeDisplayText(preview.content);
2930
3095
  if (!normalized || visibleText.has(normalized))
2931
3096
  return false;
@@ -2959,8 +3124,10 @@ function buildToolUseDisplay(toolName, input) {
2959
3124
  }
2960
3125
  else if (toolName === "expose_downloads")
2961
3126
  pushToolFact(facts, "文件", arrayLabel(data.paths), true);
2962
- else if (toolName === "subagent_message")
2963
- pushToolFact(facts, "接收方", data.target);
3127
+ else if (toolName === "subagent_message") {
3128
+ if (typeof data.message === "string" && data.message.trim())
3129
+ previews.push({ kind: "text", content: data.message });
3130
+ }
2964
3131
  else if (toolName.startsWith("subagent_"))
2965
3132
  pushToolFact(facts, "任务", data.task_id, true);
2966
3133
  else if (toolName.startsWith("secret_")) {
@@ -2974,7 +3141,7 @@ function buildToolUseDisplay(toolName, input) {
2974
3141
  return normalizeToolDisplay({ subject, purpose, facts, previews });
2975
3142
  }
2976
3143
  function buildToolResultDisplay(toolName, output, ok) {
2977
- const data = isRecord(output) ? output : {};
3144
+ const data = toolName.startsWith("subagent_") ? subagentHeader(output) : isRecord(output) ? output : {};
2978
3145
  const facts = [];
2979
3146
  const previews = [];
2980
3147
  let subject = toolDisplaySubject(toolName, data);
@@ -2987,13 +3154,8 @@ function buildToolResultDisplay(toolName, output, ok) {
2987
3154
  subject = undefined;
2988
3155
  purpose = stringValue(data.description);
2989
3156
  pushToolPreview(previews, "命令", "code", data.command);
2990
- const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
2991
- if (outputText)
2992
- pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
2993
- if (data.timed_out && !outputText)
2994
- pushToolFact(facts, "状态", "命令执行超时", false, "warning");
2995
- else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
2996
- pushToolFact(facts, "状态", `退出码 ${data.exit_code ?? "未知"}`, false, "danger");
3157
+ previews.push(...terminalPreviews(data));
3158
+ facts.push(...terminalFacts(data));
2997
3159
  }
2998
3160
  else if (toolName === "file_search") {
2999
3161
  subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
@@ -3016,7 +3178,7 @@ function buildToolResultDisplay(toolName, output, ok) {
3016
3178
  else if (toolName === "image_create") {
3017
3179
  subject = stringValue(data.semanticName);
3018
3180
  pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
3019
- pushToolFact(facts, "尺寸", data.size);
3181
+ pushToolFact(facts, "请求尺寸", data.size === "auto" ? "auto(自动策略)" : data.size);
3020
3182
  }
3021
3183
  else if (toolName === "image_inspect") {
3022
3184
  subject = arrayLabel(data.imageRefs);
@@ -3026,6 +3188,9 @@ function buildToolResultDisplay(toolName, output, ok) {
3026
3188
  }
3027
3189
  else if (toolName.startsWith("subagent_") || toolName === "subagent_run") {
3028
3190
  subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
3191
+ facts.push(...subagentStatusFacts(toolName, output, ok));
3192
+ pushToolFact(facts, "轮次", data.run_generation);
3193
+ pushToolFact(facts, "待交付", data.pending_messages);
3029
3194
  }
3030
3195
  else if (toolName === "subagent_message") {
3031
3196
  subject = stringValue(data.target);
@@ -3209,21 +3374,8 @@ function diffLineMarker(line) {
3209
3374
  function isExecOutput(value) {
3210
3375
  return isRecord(value) && typeof value.command === "string" && typeof value.duration_ms === "number";
3211
3376
  }
3212
- function formatExecToolResult(output, ok) {
3213
- const status = output.status === "running" ? "运行中" : output.timed_out ? "已超时" : ok ? "已完成" : "执行失败";
3214
- const description = typeof output.description === "string" ? output.description.trim() : "";
3215
- const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.duration_ms}ms`];
3216
- if (output.status === "running" && typeof output.session_id === "string")
3217
- lines.push(`会话:${output.session_id}`);
3218
- const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
3219
- const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
3220
- if (stdout)
3221
- lines.push("输出:", stdout);
3222
- if (stderr)
3223
- lines.push("错误:", stderr);
3224
- if (!stdout && !stderr)
3225
- lines.push(ok ? "无输出。" : "没有捕获到输出。");
3226
- return lines.join("\n");
3377
+ function formatExecToolResult(output, _ok) {
3378
+ return terminalResultText(output);
3227
3379
  }
3228
3380
  function formatImageGenerationToolResult(output, ok) {
3229
3381
  const error = typeof output.error === "string" ? output.error : undefined;