neoctl 0.2.36 → 0.2.38

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 (208) hide show
  1. package/dist/agents/agent-definition.d.ts +2 -0
  2. package/dist/agents/agent-definition.js +10 -0
  3. package/dist/agents/agent-definition.js.map +1 -1
  4. package/dist/agents/agent-report-security.test.d.ts +1 -0
  5. package/dist/agents/agent-report-security.test.js +301 -0
  6. package/dist/agents/agent-report-security.test.js.map +1 -0
  7. package/dist/agents/agent-tool-persistence.test.js +9 -3
  8. package/dist/agents/agent-tool-persistence.test.js.map +1 -1
  9. package/dist/agents/agent-tool.d.ts +3 -0
  10. package/dist/agents/agent-tool.js +131 -34
  11. package/dist/agents/agent-tool.js.map +1 -1
  12. package/dist/agents/coordination-boundary.test.d.ts +1 -0
  13. package/dist/agents/coordination-boundary.test.js +58 -0
  14. package/dist/agents/coordination-boundary.test.js.map +1 -0
  15. package/dist/agents/live-preview-redaction-security.test.d.ts +1 -0
  16. package/dist/agents/live-preview-redaction-security.test.js +286 -0
  17. package/dist/agents/live-preview-redaction-security.test.js.map +1 -0
  18. package/dist/agents/local-agent-task.d.ts +27 -1
  19. package/dist/agents/local-agent-task.js +75 -5
  20. package/dist/agents/local-agent-task.js.map +1 -1
  21. package/dist/agents/obs07-11-run-facts.test.d.ts +1 -0
  22. package/dist/agents/obs07-11-run-facts.test.js +459 -0
  23. package/dist/agents/obs07-11-run-facts.test.js.map +1 -0
  24. package/dist/agents/smoke-agent-lifecycle.js +8 -4
  25. package/dist/agents/smoke-agent-lifecycle.js.map +1 -1
  26. package/dist/agents/smoke-agents.js +7 -3
  27. package/dist/agents/smoke-agents.js.map +1 -1
  28. package/dist/context/context-manager.d.ts +5 -0
  29. package/dist/context/context-manager.js +14 -7
  30. package/dist/context/context-manager.js.map +1 -1
  31. package/dist/context/prompt-config.d.ts +35 -0
  32. package/dist/context/prompt-config.js +151 -0
  33. package/dist/context/prompt-config.js.map +1 -0
  34. package/dist/context/prompt-config.test.d.ts +1 -0
  35. package/dist/context/prompt-config.test.js +124 -0
  36. package/dist/context/prompt-config.test.js.map +1 -0
  37. package/dist/context/prompts.d.ts +1 -1
  38. package/dist/context/prompts.js +4 -27
  39. package/dist/context/prompts.js.map +1 -1
  40. package/dist/context/smoke-context.js +49 -4
  41. package/dist/context/smoke-context.js.map +1 -1
  42. package/dist/context/system.md +16 -0
  43. package/dist/core/image-registry.js +1 -1
  44. package/dist/core/image-registry.js.map +1 -1
  45. package/dist/core/obs04-visible-data.test.d.ts +1 -0
  46. package/dist/core/obs04-visible-data.test.js +147 -0
  47. package/dist/core/obs04-visible-data.test.js.map +1 -0
  48. package/dist/core/query-engine.d.ts +32 -1
  49. package/dist/core/query-engine.js +320 -95
  50. package/dist/core/query-engine.js.map +1 -1
  51. package/dist/core/query-session-settings.test.d.ts +1 -0
  52. package/dist/core/query-session-settings.test.js +252 -0
  53. package/dist/core/query-session-settings.test.js.map +1 -0
  54. package/dist/core/query.d.ts +18 -0
  55. package/dist/core/query.js +134 -15
  56. package/dist/core/query.js.map +1 -1
  57. package/dist/core/run-agent.d.ts +2 -0
  58. package/dist/core/run-agent.js +23 -15
  59. package/dist/core/run-agent.js.map +1 -1
  60. package/dist/core/session-settings-prompt.d.ts +35 -0
  61. package/dist/core/session-settings-prompt.js +61 -0
  62. package/dist/core/session-settings-prompt.js.map +1 -0
  63. package/dist/core/session-settings-prompt.test.d.ts +1 -0
  64. package/dist/core/session-settings-prompt.test.js +257 -0
  65. package/dist/core/session-settings-prompt.test.js.map +1 -0
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.js +2 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/model/model-gateway.d.ts +1 -0
  70. package/dist/model/model-metadata.json +97 -99
  71. package/dist/model/openai-native-image-guard.test.d.ts +1 -0
  72. package/dist/model/openai-native-image-guard.test.js +101 -0
  73. package/dist/model/openai-native-image-guard.test.js.map +1 -0
  74. package/dist/model/openai-responses-instructions.test.d.ts +1 -0
  75. package/dist/model/openai-responses-instructions.test.js +64 -0
  76. package/dist/model/openai-responses-instructions.test.js.map +1 -0
  77. package/dist/model/openai-responses-mapper.js +96 -32
  78. package/dist/model/openai-responses-mapper.js.map +1 -1
  79. package/dist/model/smoke-responses-mapper.js +8 -5
  80. package/dist/model/smoke-responses-mapper.js.map +1 -1
  81. package/dist/plugins/smoke-plugin-system.js +14 -14
  82. package/dist/repl/index.js +2 -2
  83. package/dist/repl/index.js.map +1 -1
  84. package/dist/secrets/secret-redaction.d.ts +6 -5
  85. package/dist/secrets/secret-redaction.js +7 -79
  86. package/dist/secrets/secret-redaction.js.map +1 -1
  87. package/dist/secrets/secret-types.d.ts +4 -1
  88. package/dist/secrets/secret-types.js.map +1 -1
  89. package/dist/session/session-export.d.ts +6 -0
  90. package/dist/session/session-export.js.map +1 -1
  91. package/dist/session/session-store.d.ts +15 -1
  92. package/dist/session/session-store.js +30 -2
  93. package/dist/session/session-store.js.map +1 -1
  94. package/dist/tasks/subagent-tools.test.js +1 -0
  95. package/dist/tasks/subagent-tools.test.js.map +1 -1
  96. package/dist/tasks/task-ack-size.test.js +6 -5
  97. package/dist/tasks/task-ack-size.test.js.map +1 -1
  98. package/dist/tasks/task-persistence.d.ts +1 -0
  99. package/dist/tasks/task-persistence.js +31 -9
  100. package/dist/tasks/task-persistence.js.map +1 -1
  101. package/dist/tasks/task-store.d.ts +6 -4
  102. package/dist/tasks/task-store.js +46 -20
  103. package/dist/tasks/task-store.js.map +1 -1
  104. package/dist/tools/builtins/exec-process-manager.d.ts +97 -2
  105. package/dist/tools/builtins/exec-process-manager.js +221 -19
  106. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  107. package/dist/tools/builtins/exec-tool.js +14 -1
  108. package/dist/tools/builtins/exec-tool.js.map +1 -1
  109. package/dist/tools/builtins/image-generation-tool.js +24 -24
  110. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  111. package/dist/tools/smoke-exec-process.js +2 -1
  112. package/dist/tools/smoke-exec-process.js.map +1 -1
  113. package/dist/tools/smoke-terminal-output-chain.d.ts +1 -0
  114. package/dist/tools/smoke-terminal-output-chain.js +245 -0
  115. package/dist/tools/smoke-terminal-output-chain.js.map +1 -0
  116. package/dist/tools/smoke-tool-system.js +6 -6
  117. package/dist/tools/smoke-tool-system.js.map +1 -1
  118. package/dist/tools/terminal-background-history.test.d.ts +1 -0
  119. package/dist/tools/terminal-background-history.test.js +77 -0
  120. package/dist/tools/terminal-background-history.test.js.map +1 -0
  121. package/dist/tools/terminal-output-persistence.test.d.ts +1 -0
  122. package/dist/tools/terminal-output-persistence.test.js +50 -0
  123. package/dist/tools/terminal-output-persistence.test.js.map +1 -0
  124. package/dist/tools/terminal-output-store.d.ts +154 -0
  125. package/dist/tools/terminal-output-store.js +580 -0
  126. package/dist/tools/terminal-output-store.js.map +1 -0
  127. package/dist/tools/terminal-output-store.test.d.ts +1 -0
  128. package/dist/tools/terminal-output-store.test.js +364 -0
  129. package/dist/tools/terminal-output-store.test.js.map +1 -0
  130. package/dist/types/events.d.ts +1 -0
  131. package/dist/types/messages.d.ts +1 -0
  132. package/dist/web/agent-content-detail.d.ts +78 -0
  133. package/dist/web/agent-content-detail.js +495 -0
  134. package/dist/web/agent-content-detail.js.map +1 -0
  135. package/dist/web/agent-content-detail.test.d.ts +1 -0
  136. package/dist/web/agent-content-detail.test.js +307 -0
  137. package/dist/web/agent-content-detail.test.js.map +1 -0
  138. package/dist/web/agent-parent-messages.test.d.ts +1 -0
  139. package/dist/web/agent-parent-messages.test.js +58 -0
  140. package/dist/web/agent-parent-messages.test.js.map +1 -0
  141. package/dist/web/agent-tool-payload-security.test.d.ts +1 -0
  142. package/dist/web/agent-tool-payload-security.test.js +120 -0
  143. package/dist/web/agent-tool-payload-security.test.js.map +1 -0
  144. package/dist/web/index.d.ts +136 -4
  145. package/dist/web/index.js +303 -78
  146. package/dist/web/index.js.map +1 -1
  147. package/dist/web/prompt-config-protocol.d.ts +4 -0
  148. package/dist/web/prompt-config-protocol.js +57 -0
  149. package/dist/web/prompt-config-protocol.js.map +1 -0
  150. package/dist/web/prompt-config-protocol.test.d.ts +1 -0
  151. package/dist/web/prompt-config-protocol.test.js +78 -0
  152. package/dist/web/prompt-config-protocol.test.js.map +1 -0
  153. package/dist/web/runtime-context-protocol.d.ts +1 -0
  154. package/dist/web/runtime-context-protocol.js +1 -0
  155. package/dist/web/runtime-context-protocol.js.map +1 -1
  156. package/dist/web/session-prompt-integration.test.d.ts +1 -0
  157. package/dist/web/session-prompt-integration.test.js +57 -0
  158. package/dist/web/session-prompt-integration.test.js.map +1 -0
  159. package/dist/web/session-prompt-protocol.d.ts +8 -0
  160. package/dist/web/session-prompt-protocol.js +11 -0
  161. package/dist/web/session-prompt-protocol.js.map +1 -0
  162. package/dist/web/session-settings.test.d.ts +1 -0
  163. package/dist/web/session-settings.test.js +254 -0
  164. package/dist/web/session-settings.test.js.map +1 -0
  165. package/dist/web/smoke-agent-content-http.d.ts +1 -0
  166. package/dist/web/smoke-agent-content-http.js +446 -0
  167. package/dist/web/smoke-agent-content-http.js.map +1 -0
  168. package/dist/web/smoke-status-semantics.d.ts +1 -0
  169. package/dist/web/smoke-status-semantics.js +90 -0
  170. package/dist/web/smoke-status-semantics.js.map +1 -0
  171. package/dist/web/smoke-task-session-entrypoints.js +22 -1
  172. package/dist/web/smoke-task-session-entrypoints.js.map +1 -1
  173. package/dist/web/smoke-terminal-output-http.d.ts +1 -0
  174. package/dist/web/smoke-terminal-output-http.js +377 -0
  175. package/dist/web/smoke-terminal-output-http.js.map +1 -0
  176. package/dist/web/smoke-tool-call-detail.d.ts +1 -0
  177. package/dist/web/smoke-tool-call-detail.js +106 -0
  178. package/dist/web/smoke-tool-call-detail.js.map +1 -0
  179. package/dist/web/smoke-tool-call-http.d.ts +1 -0
  180. package/dist/web/smoke-tool-call-http.js +100 -0
  181. package/dist/web/smoke-tool-call-http.js.map +1 -0
  182. package/dist/web/smoke-tool-detail-fields-http.d.ts +1 -0
  183. package/dist/web/smoke-tool-detail-fields-http.js +121 -0
  184. package/dist/web/smoke-tool-detail-fields-http.js.map +1 -0
  185. package/dist/web/smoke-web-terminal-tasks.js +223 -76
  186. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  187. package/dist/web/status-semantics.d.ts +11 -0
  188. package/dist/web/status-semantics.js +59 -0
  189. package/dist/web/status-semantics.js.map +1 -0
  190. package/dist/web/terminal-presentation.d.ts +15 -0
  191. package/dist/web/terminal-presentation.js +61 -0
  192. package/dist/web/terminal-presentation.js.map +1 -0
  193. package/dist/web/terminal-presentation.test.d.ts +1 -0
  194. package/dist/web/terminal-presentation.test.js +33 -0
  195. package/dist/web/terminal-presentation.test.js.map +1 -0
  196. package/dist/web/tool-call-detail.d.ts +40 -0
  197. package/dist/web/tool-call-detail.js +167 -0
  198. package/dist/web/tool-call-detail.js.map +1 -0
  199. package/dist/web/tool-detail-fields.d.ts +59 -0
  200. package/dist/web/tool-detail-fields.js +119 -0
  201. package/dist/web/tool-detail-fields.js.map +1 -0
  202. package/dist/web/tool-detail-fields.test.d.ts +1 -0
  203. package/dist/web/tool-detail-fields.test.js +121 -0
  204. package/dist/web/tool-detail-fields.test.js.map +1 -0
  205. package/docs/prompt-config.md +71 -0
  206. package/package.json +94 -93
  207. package/scripts/build-standalone.mjs +3 -0
  208. package/scripts/copy-prompt-resources.mjs +5 -0
package/dist/web/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
3
+ import { handlePromptConfigRequest } from "./prompt-config-protocol.js";
2
4
  import http from "node:http";
3
5
  import fs from "node:fs/promises";
4
6
  import path from "node:path";
5
7
  import { existsSync, readFileSync } from "node:fs";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import { createRequire } from "node:module";
10
+ import { readSessionPrompt, updateSessionPrompt, SessionPromptError } from "./session-prompt-protocol.js";
8
11
  import { QueryEngine } from "../core/query-engine.js";
9
12
  import { InMemoryAppPromptStore } from "../app/app-prompt.js";
10
13
  import { InMemoryAppState } from "../app/app-state.js";
@@ -31,6 +34,10 @@ import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/comm
31
34
  import { writeSessionMarkdownExport } from "../session/session-export.js";
32
35
  import { DefaultContextManager } from "../context/context-manager.js";
33
36
  import { createImageId } from "../types/messages.js";
37
+ import { readToolCallDetail, toolErrorText, allowToolDetailRequest, sanitizeAgentToolPayload, redactToolDetail } from "./tool-call-detail.js";
38
+ import { terminalFacts, terminalPreviews, terminalResultText } from "./terminal-presentation.js";
39
+ import { createAgentContentDetailResolver } from "./agent-content-detail.js";
40
+ import { toolDetailFieldsFromDetail } from "./tool-detail-fields.js";
34
41
  import { WEB_HTML } from "./html.js";
35
42
  import { openDirectory } from "../open-directory.js";
36
43
  import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
@@ -274,7 +281,15 @@ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides
274
281
  }
275
282
  /** Bind only the foreground view; never stop work owned by another session. */
276
283
  export function activateSession(runtime) {
277
- runtime.taskStore.bindSession?.(runtime.engine.snapshot().session?.sessionDir || undefined);
284
+ const sessionDir = runtime.engine.snapshot().session?.sessionDir || undefined;
285
+ const summary = runtime.taskStore.bindSession?.(sessionDir);
286
+ const interruptedTasks = summary?.loaded ? runtime.taskStore.recoverableInterruptedTasks?.(sessionDir) ?? [] : [];
287
+ if (interruptedTasks.length) {
288
+ runtime.engine.noteRecoverableSubagents(() => {
289
+ for (const task of interruptedTasks)
290
+ runtime.taskStore.markNotified(task.taskId);
291
+ });
292
+ }
278
293
  }
279
294
  export function sessionAgentTasks(runtime) {
280
295
  const dir = runtime.engine.snapshot().session?.sessionDir || undefined;
@@ -390,6 +405,7 @@ function webRuntimeScopeKey(scope) {
390
405
  }
391
406
  export class WebRepl {
392
407
  runtime;
408
+ agentContentReader = createAgentContentDetailResolver();
393
409
  subscribers = new Set();
394
410
  eventSequence = 0;
395
411
  pendingDeltaOperations = [];
@@ -458,19 +474,60 @@ export class WebRepl {
458
474
  this.send(subscriber, "client.reload", payload);
459
475
  return subscribers.length;
460
476
  }
477
+ async toolCallDetail(sessionId, toolUseId, messageId) {
478
+ const session = this.runtime.engine.snapshot().session;
479
+ 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() });
480
+ return detail ? this.runtime.engine.redactDisplayValue({ ...detail, fields: toolDetailFieldsFromDetail(detail) }) : undefined;
481
+ }
482
+ async agentContent(sessionId, view, request) {
483
+ const session = this.runtime.engine.snapshot().session;
484
+ if (!session?.sessionDir || sessionId !== session.sessionId || !["timeline", "delegation", "report", "messages"].includes(view))
485
+ return undefined;
486
+ // Loading/recovery belongs to activateSession, never this read-only route.
487
+ if (!this.runtime.taskStore.getInSession(request.taskId, session.sessionDir))
488
+ return undefined;
489
+ return this.agentContentReader[view]({
490
+ ownerSessionId: sessionId, ownerSessionDir: session.sessionDir, taskStore: this.runtime.taskStore,
491
+ redact: value => this.runtime.engine.redactDisplayValue(value),
492
+ }, request);
493
+ }
494
+ terminalOutput(sessionId, runId, stream, offset, limitBytes) {
495
+ const session = this.runtime.engine.snapshot().session;
496
+ if (!session?.sessionDir || !sessionId || sessionId !== session.sessionId || !runId || !["stdout", "stderr"].includes(stream))
497
+ return undefined;
498
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limitBytes) || limitBytes < 4 || limitBytes > 262144)
499
+ return undefined;
500
+ const manager = this.runtime.execProcessManager;
501
+ const registered = manager.registerOwnerSession(sessionId, session.sessionDir);
502
+ if (!registered.ok)
503
+ return undefined;
504
+ const page = manager.readOutput(sessionId, runId, { stream: stream, offset, limitBytes });
505
+ // Cursor addresses stored bytes, not the possibly additionally redacted response length.
506
+ return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
507
+ }
461
508
  snapshot(includeCatalog = false) {
462
509
  const backgroundTasks = this.backgroundTasks();
510
+ const engineSnapshot = this.runtime.engine.snapshot();
511
+ const pendingSessionSettings = this.runtime.engine.getPendingSessionSettings?.() ?? {};
512
+ // Include retained reasoning alongside a queued model. Explicit undefined is
513
+ // the default reset and is intentionally omitted by JSON serialization.
514
+ if (Object.hasOwn(pendingSessionSettings, "model") && !Object.hasOwn(pendingSessionSettings, "reasoning")) {
515
+ pendingSessionSettings.reasoning = engineSnapshot.reasoning;
516
+ }
463
517
  return {
464
- lines: this.lines,
518
+ lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
465
519
  status: this.status,
466
520
  busy: this.busy,
467
521
  queuedInput: this.queuedInput,
468
522
  backgroundTaskCount: backgroundTasks.length,
469
523
  backgroundTasks,
470
- agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 20).map((task) => this.agentTaskSnapshot(task)),
524
+ terminalTaskHistory: this.terminalTaskHistory(),
525
+ agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((task) => this.agentTaskSnapshot(task)),
471
526
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
472
527
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
473
- session: this.runtime.engine.snapshot().session,
528
+ session: engineSnapshot.session,
529
+ modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
530
+ pendingSessionSettings,
474
531
  fastMode: this.runtime.engine.isFastMode(),
475
532
  appPrompt: this.runtime.engine.getAppPrompt(),
476
533
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
@@ -902,35 +959,105 @@ export class WebRepl {
902
959
  }
903
960
  return undefined;
904
961
  }
962
+ async refreshSessionSettingMetrics(engine) {
963
+ const metrics = await engine.contextMetrics();
964
+ // A request can finish after its owner session was detached/switched.
965
+ if (this.runtime.engine !== engine)
966
+ return;
967
+ this.runtime.initialMetrics = metrics;
968
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
969
+ }
905
970
  async setFastMode(enabled) {
971
+ const engine = this.runtime.engine;
906
972
  try {
907
- const fastMode = await this.runtime.engine.setFastMode(enabled);
908
- this.broadcastSync();
909
- return { ok: true, fastMode };
973
+ const { deferred } = await engine.updateSessionSettings({ fastMode: enabled });
974
+ if (this.runtime.engine === engine)
975
+ this.broadcastSync();
976
+ return { ok: true, fastMode: enabled, deferred };
910
977
  }
911
978
  catch (error) {
912
979
  return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
913
980
  }
914
981
  }
915
982
  async setContextWindowK(value) {
916
- if (this.busy)
917
- return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
918
- if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
983
+ if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0 || !Number.isSafeInteger(Number(value) * 1000)) {
919
984
  return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
920
985
  }
921
986
  const contextWindowK = Number(value);
922
987
  const contextWindowTokens = contextWindowK * 1000;
988
+ const engine = this.runtime.engine;
923
989
  try {
924
- await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
925
- const metrics = await this.runtime.engine.contextMetrics();
926
- this.runtime.initialMetrics = metrics;
927
- this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
928
- return { ok: true, contextWindowK, contextWindowTokens };
990
+ const { deferred } = await engine.updateSessionSettings({ contextWindowTokens });
991
+ if (!deferred)
992
+ await this.refreshSessionSettingMetrics(engine);
993
+ if (this.runtime.engine === engine)
994
+ this.broadcastSync();
995
+ return { ok: true, contextWindowK, contextWindowTokens, deferred };
929
996
  }
930
997
  catch (error) {
931
998
  return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
932
999
  }
933
1000
  }
1001
+ async setSessionModel(model, reasoning) {
1002
+ if (typeof model !== "string" || !model.trim() || model.length > 256 || /[\s\x00-\x1f]/u.test(model.trim())) {
1003
+ return actionFailure("MODEL_INVALID", "invalid model");
1004
+ }
1005
+ if (reasoning !== undefined && (typeof reasoning !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "default", "off"].includes(reasoning))) {
1006
+ return actionFailure("MODEL_INVALID", "invalid reasoning effort");
1007
+ }
1008
+ const engine = this.runtime.engine;
1009
+ const nextModel = model.trim();
1010
+ const current = { ...engine.getModelSettings(), ...engine.getPendingSessionSettings() };
1011
+ const argument = reasoning;
1012
+ const validationError = validateModelReasoningArgument(nextModel, argument);
1013
+ if (validationError)
1014
+ return actionFailure("MODEL_INVALID", validationError);
1015
+ const update = resolveModelReasoningUpdate(argument, current.reasoning, nextModel, nextModel !== current.model);
1016
+ try {
1017
+ // Current engine only: no env write, provider replacement, transcript message,
1018
+ // runtime-context injection or prompt-cache prefix change for UI settings.
1019
+ const { deferred } = await engine.updateSessionSettings({ model: nextModel, ...(update.update ? { reasoning: update.reasoning } : {}) });
1020
+ if (!deferred)
1021
+ await this.refreshSessionSettingMetrics(engine);
1022
+ if (this.runtime.engine === engine)
1023
+ this.broadcastSync();
1024
+ return { ok: true, deferred, modelSettings: { model: nextModel, reasoning: update.reasoning } };
1025
+ }
1026
+ catch (error) {
1027
+ return actionFailure("MODEL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
1028
+ }
1029
+ }
1030
+ sessionPrompt() {
1031
+ return readSessionPrompt(this.runtime.engine);
1032
+ }
1033
+ async saveSessionPrompt(body) {
1034
+ const engine = this.runtime.engine;
1035
+ const result = await updateSessionPrompt(engine, body);
1036
+ if (this.runtime.engine === engine) {
1037
+ if (!result.deferred)
1038
+ await this.refreshSessionSettingMetrics(engine);
1039
+ this.publishRuntimeContext();
1040
+ this.broadcastSync();
1041
+ }
1042
+ return result;
1043
+ }
1044
+ async compactSession() {
1045
+ const engine = this.runtime.engine;
1046
+ try {
1047
+ const result = await engine.updateSessionSettings({ compact: true });
1048
+ if (this.runtime.engine === engine) {
1049
+ if (result.compaction?.report)
1050
+ this.appendCompaction(result.compaction.report);
1051
+ if (!result.deferred)
1052
+ await this.refreshSessionSettingMetrics(engine);
1053
+ this.broadcastSync();
1054
+ }
1055
+ return { ok: true, deferred: result.deferred };
1056
+ }
1057
+ catch (error) {
1058
+ return actionFailure("COMPACTION_FAILED", error instanceof Error ? error.message : String(error));
1059
+ }
1060
+ }
934
1061
  loginForm(providerValue) {
935
1062
  const provider = parseLoginProvider(providerValue);
936
1063
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -1042,27 +1169,77 @@ export class WebRepl {
1042
1169
  }
1043
1170
  agentTaskSnapshot(task) {
1044
1171
  const terminal = this.runtime.taskStore.isTerminal(task);
1045
- const result = (value, limit = 1500) => value ? { content: value.content.slice(0, limit), truncated: value.content.length > limit, status: value.status } : undefined;
1046
- return {
1172
+ // Redact complete values before any public preview boundary: clipping first can expose
1173
+ // a credential prefix which no longer matches the runtime registry's full value.
1174
+ const safeText = (value) => value === undefined ? undefined : this.runtime.engine.redactDisplayValue(value);
1175
+ const result = (value, limit = 1500) => {
1176
+ if (!value)
1177
+ return undefined;
1178
+ if (value.displaySource !== "agent_report" && value.displaySource !== "visible_text") {
1179
+ return { content: "", truncated: false, status: value.status, availability: "unavailable", reason: "旧报告缺少获准展示来源;不会公开未验证的文本" };
1180
+ }
1181
+ const content = safeText(value.content);
1182
+ return { content: content.slice(0, limit), truncated: content.length > limit, status: value.status };
1183
+ };
1184
+ const error = safeText(task.error);
1185
+ return this.runtime.engine.redactDisplayValue({
1047
1186
  kind: "agent", taskId: task.taskId, agentId: task.agentId,
1048
1187
  agentType: task.agentType, type: task.type, status: task.status,
1049
1188
  description: task.description, runGeneration: task.runGeneration ?? 1,
1050
- progress: { 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 })) },
1189
+ 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 })) },
1051
1190
  pendingMessageCount: task.pendingMessages.length,
1052
1191
  deliveredRetainedThisRun: task.messageReceipts?.filter((receipt) => receipt.status === "delivered" && receipt.runGeneration === task.runGeneration).length,
1053
1192
  messageReceipts: (task.messageReceipts ?? []).slice(-20).map(({ id, status, queuedAt, deliveredAt, runGeneration }) => ({ id, status, queuedAt, deliveredAt, runGeneration })),
1054
1193
  result: terminal ? result(task.result) : undefined,
1055
- error: terminal ? task.error?.slice(0, 1500) : undefined,
1056
- errorTruncated: terminal && (task.error?.length ?? 0) > 1500,
1057
- runHistory: (task.runHistory ?? []).slice(-3).map((run) => ({ runGeneration: run.runGeneration, status: run.status, completedAt: run.completedAt, result: result(run.result, 600), error: run.error?.slice(0, 600), errorTruncated: (run.error?.length ?? 0) > 600 })),
1058
- createdAt: task.createdAt, updatedAt: task.updatedAt, completedAt: task.completedAt,
1059
- };
1194
+ error: terminal ? error?.slice(0, 1500) : undefined,
1195
+ errorTruncated: terminal && (error?.length ?? 0) > 1500,
1196
+ runHistory: (task.runHistory ?? []).map((run) => {
1197
+ const archivedError = safeText(run.error);
1198
+ 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 };
1199
+ }),
1200
+ createdAt: task.createdAt, updatedAt: task.updatedAt, startedAt: task.startedAt, completedAt: task.completedAt, durationMs: task.durationMs,
1201
+ });
1202
+ }
1203
+ terminalTaskHistory() {
1204
+ const session = this.runtime.engine.snapshot().session;
1205
+ if (!session?.sessionId || !session.sessionDir)
1206
+ return [];
1207
+ const manager = this.runtime.execProcessManager;
1208
+ if (!manager.registerOwnerSession?.(session.sessionId, session.sessionDir).ok)
1209
+ return [];
1210
+ const records = [];
1211
+ let offset = 0;
1212
+ do {
1213
+ const history = manager.listHistory(session.sessionId, { offset, limit: 200 });
1214
+ if (!history.ok)
1215
+ break;
1216
+ records.push(...history.value.records);
1217
+ if (history.value.nextOffset === null)
1218
+ break;
1219
+ offset = history.value.nextOffset;
1220
+ } while (true);
1221
+ // Output history includes foreground commands too. Only an explicit durable
1222
+ // background transition belongs in this UI; legacy records are not inferred.
1223
+ return records.filter(({ record }) => record.metadata.backgrounded === true).reverse().map(({ record }) => ({
1224
+ kind: "terminal", taskId: `terminal:${record.runId}`, type: "终端",
1225
+ ownerSessionId: session.sessionId, sessionId: record.runId,
1226
+ status: record.lifecycle === "lost" ? "lost" : record.exit?.status ?? "unknown",
1227
+ description: record.metadata.description || record.metadata.command || "终端运行",
1228
+ command: record.metadata.command, cwd: record.metadata.cwd, shell: record.metadata.shell,
1229
+ tty: record.metadata.tty, processId: record.metadata.processId,
1230
+ createdAt: record.metadata.startedAt, completedAt: record.exit?.finishedAt,
1231
+ durationMs: record.exit?.durationMs, exitCode: record.exit?.exitCode,
1232
+ signal: record.exit?.signal, terminationReason: record.exit?.terminationReason,
1233
+ outputAvailability: record.availability, outputTruncated: record.truncated,
1234
+ metadataTruncated: record.metadata.truncatedFields,
1235
+ })).map(task => this.runtime.engine.redactDisplayValue(task));
1060
1236
  }
1061
1237
  backgroundTasks() {
1062
1238
  const agentTasks = sessionAgentTasks(this.runtime)
1063
1239
  .filter((task) => !this.runtime.taskStore.isTerminal(task))
1064
1240
  .map((task) => this.agentTaskSnapshot(task));
1065
- const terminalTasks = this.runtime.execProcessManager.list()
1241
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1242
+ const terminalTasks = (ownerSessionId ? this.runtime.execProcessManager.list(ownerSessionId) : [])
1066
1243
  .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
1067
1244
  .map((terminal) => ({
1068
1245
  kind: "terminal",
@@ -1070,7 +1247,8 @@ export class WebRepl {
1070
1247
  type: "终端",
1071
1248
  status: terminal.status,
1072
1249
  description: terminal.description || terminal.command,
1073
- createdAt: Date.now() - terminal.duration_ms,
1250
+ createdAt: terminal.started_at,
1251
+ ownerSessionId,
1074
1252
  sessionId: terminal.session_id,
1075
1253
  processId: terminal.process_id,
1076
1254
  command: terminal.command,
@@ -1078,8 +1256,11 @@ export class WebRepl {
1078
1256
  shell: terminal.shell,
1079
1257
  tty: terminal.tty,
1080
1258
  durationMs: terminal.duration_ms,
1081
- output: terminal.output,
1082
- outputEnd: terminal.outputEnd,
1259
+ // Output is loaded from the owner-bound reader, never copied into state snapshots.
1260
+ exitCode: terminal.exit_code,
1261
+ signal: terminal.signal,
1262
+ terminationReason: terminal.termination_reason,
1263
+ outputAvailability: terminal.output_ref.availability,
1083
1264
  }));
1084
1265
  const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1085
1266
  kind: "session",
@@ -1289,7 +1470,7 @@ export class WebRepl {
1289
1470
  return liveLineId;
1290
1471
  }, runtimeToolResultPresenter(this.runtime));
1291
1472
  }
1292
- const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: event.ok ? "completed" : "failed" } : step);
1473
+ const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "unknown" } : step);
1293
1474
  if (replacement) {
1294
1475
  this.replaceLine(liveLineId, {
1295
1476
  ...replacement,
@@ -1714,6 +1895,10 @@ export class WebRepl {
1714
1895
  this.queueDeltaOperation(operation);
1715
1896
  }
1716
1897
  queueDeltaOperation(operation) {
1898
+ if (operation.type === "line.append" && operation.line.toolError)
1899
+ operation = { ...operation, line: { ...operation.line, toolError: this.runtime.engine.redactDisplayValue?.(operation.line.toolError) ?? operation.line.toolError } };
1900
+ if (operation.type === "line.patch" && operation.patch.toolError)
1901
+ operation = { ...operation, patch: { ...operation.patch, toolError: this.runtime.engine.redactDisplayValue?.(operation.patch.toolError) ?? operation.patch.toolError } };
1717
1902
  this.pendingDeltaOperations.push(operation);
1718
1903
  this.scheduleDeltaFlush();
1719
1904
  }
@@ -1757,18 +1942,11 @@ export class WebRepl {
1757
1942
  }
1758
1943
  }
1759
1944
  queueTerminalOutput(delta) {
1760
- if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
1945
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1946
+ if (!this.subscribers.size || !ownerSessionId || delta.ownerSessionId !== ownerSessionId)
1761
1947
  return;
1762
- const outputStart = Number(delta.outputStart ?? 0);
1763
- const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
1764
- const pending = this.pendingTerminalOutput.get(delta.sessionId);
1765
- if (pending && pending.outputEnd === outputStart) {
1766
- pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
1767
- pending.outputEnd = outputEnd;
1768
- }
1769
- else {
1770
- this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
1771
- }
1948
+ // Invalidation only. The reader owns independent stream cursors; SSE is not a full-output source.
1949
+ this.pendingTerminalOutput.set(delta.sessionId, { ownerSessionId });
1772
1950
  if (this.terminalOutputTimer)
1773
1951
  return;
1774
1952
  this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
@@ -1776,12 +1954,12 @@ export class WebRepl {
1776
1954
  }
1777
1955
  flushTerminalOutput() {
1778
1956
  this.terminalOutputTimer = undefined;
1779
- if (!this.pendingTerminalOutput.size)
1780
- return;
1781
- const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
1957
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1958
+ const updates = [...this.pendingTerminalOutput.entries()].filter(([, output]) => output.ownerSessionId === ownerSessionId).map(([sessionId, output]) => ({ sessionId, ...output, invalidated: true }));
1782
1959
  this.pendingTerminalOutput.clear();
1783
- for (const subscriber of this.subscribers)
1784
- this.send(subscriber, "terminal.output", { updates });
1960
+ if (updates.length)
1961
+ for (const subscriber of this.subscribers)
1962
+ this.send(subscriber, "terminal.output", { updates });
1785
1963
  }
1786
1964
  broadcastSync() {
1787
1965
  this.discardPendingDeltaOperations();
@@ -1826,16 +2004,46 @@ async function route(req, res, router) {
1826
2004
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1827
2005
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1828
2006
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
2007
+ if (url.pathname === "/api/prompt-config")
2008
+ return handlePromptConfigRequest(req, res);
1829
2009
  if (req.method === "GET" && url.pathname === "/api/client-info")
1830
2010
  return sendJson(res, router.clientInfo());
1831
2011
  if (req.method === "POST" && url.pathname === "/api/client-reload") {
1832
2012
  const body = await readJsonBody(req);
1833
2013
  return sendJson(res, await router.requestClientReload(body));
1834
2014
  }
2015
+ 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"])) {
2016
+ return sendJson(res, { error: "详情访问被拒绝" }, 403);
2017
+ }
1835
2018
  const scope = webRuntimeScopeFromUrl(url);
1836
2019
  const repl = await router.get(scope);
1837
2020
  if (req.method === "GET" && url.pathname === "/events")
1838
2021
  return repl.subscribe(res, router.clientInfo());
2022
+ if (req.method === "GET" && url.pathname === "/api/tool-call-detail") {
2023
+ res.setHeader("Cache-Control", "no-store");
2024
+ const detail = await repl.toolCallDetail(url.searchParams.get("sessionId") ?? "", url.searchParams.get("toolUseId") ?? "", url.searchParams.get("messageId") ?? undefined);
2025
+ if (!detail)
2026
+ return sendJson(res, { error: "调用详情不可获取或无权访问" }, 404);
2027
+ return sendJson(res, detail);
2028
+ }
2029
+ if (req.method === "GET" && url.pathname === "/api/agent-content") {
2030
+ res.setHeader("Cache-Control", "no-store");
2031
+ const refresh = url.searchParams.get("refresh");
2032
+ if (refresh !== null && refresh !== "true" && refresh !== "false")
2033
+ return sendJson(res, { error: "代理详情参数无效" }, 404);
2034
+ const page = await repl.agentContent(url.searchParams.get("sessionId") ?? "", url.searchParams.get("view") ?? "timeline", {
2035
+ taskId: url.searchParams.get("taskId") ?? "", runGeneration: Number(url.searchParams.get("runGeneration")),
2036
+ ...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
2037
+ ...(url.searchParams.has("pageChars") ? { pageChars: Number(url.searchParams.get("pageChars")) } : {}),
2038
+ ...(url.searchParams.has("refresh") ? { refresh: url.searchParams.get("refresh") === "true" } : {}),
2039
+ });
2040
+ return page ? sendJson(res, page) : sendJson(res, { error: "代理内容不可获取或无权访问" }, 404);
2041
+ }
2042
+ if (req.method === "GET" && url.pathname === "/api/terminal-output") {
2043
+ res.setHeader("Cache-Control", "no-store");
2044
+ 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));
2045
+ return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
2046
+ }
1839
2047
  if (req.method === "GET" && url.pathname === "/api/state")
1840
2048
  return sendJson(res, repl.snapshot(true));
1841
2049
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
@@ -1898,6 +2106,32 @@ async function route(req, res, router) {
1898
2106
  const body = await readJsonBody(req);
1899
2107
  return sendJson(res, repl.setAppPrompt(body));
1900
2108
  }
2109
+ if (url.pathname === "/api/session-prompt" && (req.method === "GET" || req.method === "POST")) {
2110
+ res.setHeader("Cache-Control", "no-store");
2111
+ try {
2112
+ if (req.method === "GET")
2113
+ return sendJson(res, await repl.sessionPrompt());
2114
+ let body;
2115
+ try {
2116
+ body = await readJsonBody(req);
2117
+ }
2118
+ catch {
2119
+ return sendJson(res, actionFailure("SESSION_PROMPT_INVALID", "invalid JSON body"), 400);
2120
+ }
2121
+ return sendJson(res, await repl.saveSessionPrompt(body));
2122
+ }
2123
+ catch (error) {
2124
+ const status = error instanceof SessionPromptError ? error.statusCode : 500;
2125
+ const code = status === 409 ? "SESSION_PROMPT_CONFLICT" : status === 400 ? "SESSION_PROMPT_INVALID" : "SESSION_PROMPT_FAILED";
2126
+ return sendJson(res, actionFailure(code, error instanceof Error ? error.message : String(error)), status);
2127
+ }
2128
+ }
2129
+ if (req.method === "POST" && url.pathname === "/api/session-model") {
2130
+ const body = await readJsonBody(req);
2131
+ return sendJson(res, await repl.setSessionModel(body.model, body.reasoning));
2132
+ }
2133
+ if (req.method === "POST" && url.pathname === "/api/compact")
2134
+ return sendJson(res, await repl.compactSession());
1901
2135
  if (req.method === "POST" && url.pathname === "/api/fast-mode") {
1902
2136
  const body = await readJsonBody(req);
1903
2137
  return sendJson(res, await repl.setFastMode(body.enabled === true));
@@ -2024,6 +2258,7 @@ function webCatalog(runtime) {
2024
2258
  return {
2025
2259
  commands: replCommandDefinitions,
2026
2260
  modelIds,
2261
+ modelReasoning: Object.fromEntries(modelIds.map(model => [model, reasoningEffortsForModel(model) ?? []])),
2027
2262
  reasoning: ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default", "off"],
2028
2263
  envPath: runtime.envPath,
2029
2264
  };
@@ -2757,13 +2992,19 @@ function formatToolUse(toolUse) {
2757
2992
  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 };
2758
2993
  }
2759
2994
  function formatToolResultLine(toolName, output, ok, presentToolResult) {
2995
+ // Hidden fallback reports in historical agent payloads must not enter line.text
2996
+ // even when the frontend suppresses the ordinary result preview.
2997
+ const authorized = sanitizeAgentToolPayload(toolName, output);
2998
+ if (toolName.startsWith("subagent_"))
2999
+ output = authorized.value;
2760
3000
  const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
2761
3001
  const resources = presentation?.resources ?? toolResources(output);
2762
3002
  const formatted = resources?.length
2763
3003
  ? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
2764
- : formatToolResult(toolName, output, ok);
3004
+ : formatToolResult(toolName, toolName.startsWith("subagent_") ? redactToolDetail(output) : output, ok);
2765
3005
  return {
2766
3006
  kind: ok ? "tool" : "error",
3007
+ toolError: ok ? undefined : toolErrorText(output, ok).slice(0, 2000),
2767
3008
  toolName,
2768
3009
  toolPresentation: toolPresentationForSource(toolName),
2769
3010
  title: presentation?.title ?? toolTitle(toolName, "finished"),
@@ -2950,12 +3191,16 @@ function normalizeToolDisplay(display) {
2950
3191
  const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
2951
3192
  const facts = display.facts.filter((fact) => {
2952
3193
  const normalized = normalizeDisplayText(fact.value);
2953
- if (!normalized || visibleText.has(normalized))
3194
+ const identity = `${fact.label}\0${normalized}`;
3195
+ if (!normalized || visibleText.has(identity))
2954
3196
  return false;
2955
- visibleText.add(normalized);
3197
+ visibleText.add(identity);
2956
3198
  return true;
2957
3199
  });
2958
3200
  const previews = display.previews.filter((preview) => {
3201
+ // Keep identical nonempty streams, but never render empty output panels.
3202
+ if (/^(stdout|stderr)(?:$|[( ·])/.test(preview.label ?? ""))
3203
+ return !!preview.content.trim();
2959
3204
  const normalized = normalizeDisplayText(preview.content);
2960
3205
  if (!normalized || visibleText.has(normalized))
2961
3206
  return false;
@@ -2989,8 +3234,10 @@ function buildToolUseDisplay(toolName, input) {
2989
3234
  }
2990
3235
  else if (toolName === "expose_downloads")
2991
3236
  pushToolFact(facts, "文件", arrayLabel(data.paths), true);
2992
- else if (toolName === "subagent_message")
2993
- pushToolFact(facts, "接收方", data.target);
3237
+ else if (toolName === "subagent_message") {
3238
+ if (typeof data.message === "string" && data.message.trim())
3239
+ previews.push({ kind: "text", content: data.message });
3240
+ }
2994
3241
  else if (toolName.startsWith("subagent_"))
2995
3242
  pushToolFact(facts, "任务", data.task_id, true);
2996
3243
  else if (toolName.startsWith("secret_")) {
@@ -3004,7 +3251,7 @@ function buildToolUseDisplay(toolName, input) {
3004
3251
  return normalizeToolDisplay({ subject, purpose, facts, previews });
3005
3252
  }
3006
3253
  function buildToolResultDisplay(toolName, output, ok) {
3007
- const data = isRecord(output) ? output : {};
3254
+ const data = toolName.startsWith("subagent_") ? subagentHeader(output) : isRecord(output) ? output : {};
3008
3255
  const facts = [];
3009
3256
  const previews = [];
3010
3257
  let subject = toolDisplaySubject(toolName, data);
@@ -3017,13 +3264,8 @@ function buildToolResultDisplay(toolName, output, ok) {
3017
3264
  subject = undefined;
3018
3265
  purpose = stringValue(data.description);
3019
3266
  pushToolPreview(previews, "命令", "code", data.command);
3020
- const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
3021
- if (outputText)
3022
- pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
3023
- if (data.timed_out && !outputText)
3024
- pushToolFact(facts, "状态", "命令执行超时", false, "warning");
3025
- else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
3026
- pushToolFact(facts, "状态", `退出码 ${data.exit_code ?? "未知"}`, false, "danger");
3267
+ previews.push(...terminalPreviews(data));
3268
+ facts.push(...terminalFacts(data));
3027
3269
  }
3028
3270
  else if (toolName === "file_search") {
3029
3271
  subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
@@ -3046,7 +3288,7 @@ function buildToolResultDisplay(toolName, output, ok) {
3046
3288
  else if (toolName === "image_create") {
3047
3289
  subject = stringValue(data.semanticName);
3048
3290
  pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
3049
- pushToolFact(facts, "尺寸", data.size);
3291
+ pushToolFact(facts, "请求尺寸", data.size === "auto" ? "auto(自动策略)" : data.size);
3050
3292
  }
3051
3293
  else if (toolName === "image_inspect") {
3052
3294
  subject = arrayLabel(data.imageRefs);
@@ -3056,13 +3298,9 @@ function buildToolResultDisplay(toolName, output, ok) {
3056
3298
  }
3057
3299
  else if (toolName.startsWith("subagent_") || toolName === "subagent_run") {
3058
3300
  subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
3059
- pushToolFact(facts, "任务状态", data.status);
3301
+ facts.push(...subagentStatusFacts(toolName, output, ok));
3060
3302
  pushToolFact(facts, "轮次", data.run_generation);
3061
3303
  pushToolFact(facts, "待交付", data.pending_messages);
3062
- if (toolName === "subagent_message")
3063
- pushToolFact(facts, "交付说明", "仅入队,不代表已交付或完成");
3064
- if (data.requires_resume === true)
3065
- pushToolFact(facts, "后续", "需要显式续跑");
3066
3304
  }
3067
3305
  else if (toolName === "subagent_message") {
3068
3306
  subject = stringValue(data.target);
@@ -3246,21 +3484,8 @@ function diffLineMarker(line) {
3246
3484
  function isExecOutput(value) {
3247
3485
  return isRecord(value) && typeof value.command === "string" && typeof value.duration_ms === "number";
3248
3486
  }
3249
- function formatExecToolResult(output, ok) {
3250
- const status = output.status === "running" ? "运行中" : output.timed_out ? "已超时" : ok ? "已完成" : "执行失败";
3251
- const description = typeof output.description === "string" ? output.description.trim() : "";
3252
- const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.duration_ms}ms`];
3253
- if (output.status === "running" && typeof output.session_id === "string")
3254
- lines.push(`会话:${output.session_id}`);
3255
- const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
3256
- const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
3257
- if (stdout)
3258
- lines.push("输出:", stdout);
3259
- if (stderr)
3260
- lines.push("错误:", stderr);
3261
- if (!stdout && !stderr)
3262
- lines.push(ok ? "无输出。" : "没有捕获到输出。");
3263
- return lines.join("\n");
3487
+ function formatExecToolResult(output, _ok) {
3488
+ return terminalResultText(output);
3264
3489
  }
3265
3490
  function formatImageGenerationToolResult(output, ok) {
3266
3491
  const error = typeof output.error === "string" ? output.error : undefined;