neoctl 0.2.30 → 0.2.32

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 (151) hide show
  1. package/README.md +37 -3
  2. package/dist/agents/agent-activity.d.ts +3 -0
  3. package/dist/agents/agent-activity.js +33 -6
  4. package/dist/agents/agent-activity.js.map +1 -1
  5. package/dist/agents/agent-definition.d.ts +2 -2
  6. package/dist/agents/agent-definition.js +6 -6
  7. package/dist/agents/agent-definition.js.map +1 -1
  8. package/dist/agents/agent-report-tool.d.ts +1 -1
  9. package/dist/agents/agent-report-tool.js +4 -4
  10. package/dist/agents/agent-report-tool.js.map +1 -1
  11. package/dist/agents/agent-tool.d.ts +1 -1
  12. package/dist/agents/agent-tool.js +97 -18
  13. package/dist/agents/agent-tool.js.map +1 -1
  14. package/dist/agents/local-agent-task.d.ts +12 -0
  15. package/dist/agents/local-agent-task.js +38 -1
  16. package/dist/agents/local-agent-task.js.map +1 -1
  17. package/dist/agents/smoke-agents.js +37 -26
  18. package/dist/agents/smoke-agents.js.map +1 -1
  19. package/dist/context/compaction.d.ts +20 -4
  20. package/dist/context/compaction.js +378 -187
  21. package/dist/context/compaction.js.map +1 -1
  22. package/dist/context/context-manager.d.ts +7 -1
  23. package/dist/context/context-manager.js +15 -11
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/prompts.js +8 -6
  26. package/dist/context/prompts.js.map +1 -1
  27. package/dist/context/smoke-context.js +273 -24
  28. package/dist/context/smoke-context.js.map +1 -1
  29. package/dist/core/assistant-output-filter.d.ts +1 -0
  30. package/dist/core/assistant-output-filter.js +54 -2
  31. package/dist/core/assistant-output-filter.js.map +1 -1
  32. package/dist/core/context-metrics.d.ts +1 -0
  33. package/dist/core/context-metrics.js +18 -12
  34. package/dist/core/context-metrics.js.map +1 -1
  35. package/dist/core/image-registry.d.ts +16 -3
  36. package/dist/core/image-registry.js +65 -30
  37. package/dist/core/image-registry.js.map +1 -1
  38. package/dist/core/image-storage.d.ts +15 -1
  39. package/dist/core/image-storage.js +140 -17
  40. package/dist/core/image-storage.js.map +1 -1
  41. package/dist/core/message-pipeline.js +18 -4
  42. package/dist/core/message-pipeline.js.map +1 -1
  43. package/dist/core/query-engine.d.ts +12 -2
  44. package/dist/core/query-engine.js +59 -8
  45. package/dist/core/query-engine.js.map +1 -1
  46. package/dist/core/query.d.ts +13 -2
  47. package/dist/core/query.js +144 -27
  48. package/dist/core/query.js.map +1 -1
  49. package/dist/core/run-agent.d.ts +13 -2
  50. package/dist/core/run-agent.js +16 -13
  51. package/dist/core/run-agent.js.map +1 -1
  52. package/dist/core/smoke-core-loop.js +121 -3
  53. package/dist/core/smoke-core-loop.js.map +1 -1
  54. package/dist/core/smoke-image-integrity.d.ts +1 -0
  55. package/dist/core/smoke-image-integrity.js +67 -0
  56. package/dist/core/smoke-image-integrity.js.map +1 -0
  57. package/dist/core/state.d.ts +1 -1
  58. package/dist/core/state.js.map +1 -1
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/model/model-metadata.json +4 -4
  63. package/dist/plugins/plugin-system.d.ts +61 -0
  64. package/dist/plugins/plugin-system.js +189 -0
  65. package/dist/plugins/plugin-system.js.map +1 -0
  66. package/dist/plugins/smoke-plugin-system.d.ts +1 -0
  67. package/dist/plugins/smoke-plugin-system.js +63 -0
  68. package/dist/plugins/smoke-plugin-system.js.map +1 -0
  69. package/dist/repl/index.js +22 -17
  70. package/dist/repl/index.js.map +1 -1
  71. package/dist/secrets/secret-redaction.d.ts +5 -0
  72. package/dist/secrets/secret-redaction.js +43 -0
  73. package/dist/secrets/secret-redaction.js.map +1 -1
  74. package/dist/secrets/secret-types.d.ts +4 -0
  75. package/dist/secrets/secret-types.js.map +1 -1
  76. package/dist/secrets/smoke-secrets.js +7 -3
  77. package/dist/secrets/smoke-secrets.js.map +1 -1
  78. package/dist/session/session-export.d.ts +2 -0
  79. package/dist/session/session-export.js +10 -0
  80. package/dist/session/session-export.js.map +1 -1
  81. package/dist/session/session-store.d.ts +41 -0
  82. package/dist/session/session-store.js +292 -59
  83. package/dist/session/session-store.js.map +1 -1
  84. package/dist/session/smoke-session.js +127 -7
  85. package/dist/session/smoke-session.js.map +1 -1
  86. package/dist/skills/smoke-skills.js +5 -5
  87. package/dist/skills/smoke-skills.js.map +1 -1
  88. package/dist/tasks/subagent-tools.d.ts +31 -0
  89. package/dist/tasks/{task-tools.js → subagent-tools.js} +26 -32
  90. package/dist/tasks/subagent-tools.js.map +1 -0
  91. package/dist/tools/builtins/edit-tool.js +24 -6
  92. package/dist/tools/builtins/edit-tool.js.map +1 -1
  93. package/dist/tools/builtins/exec-process-manager.d.ts +13 -0
  94. package/dist/tools/builtins/exec-process-manager.js +57 -7
  95. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  96. package/dist/tools/builtins/exec-tool.js +34 -10
  97. package/dist/tools/builtins/exec-tool.js.map +1 -1
  98. package/dist/tools/builtins/filesystem-tools.js +2 -4
  99. package/dist/tools/builtins/filesystem-tools.js.map +1 -1
  100. package/dist/tools/builtins/grep-tool.js +2 -3
  101. package/dist/tools/builtins/grep-tool.js.map +1 -1
  102. package/dist/tools/builtins/image-generation-tool.js +36 -46
  103. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  104. package/dist/tools/builtins/image-loader-tool.js +12 -7
  105. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  106. package/dist/tools/builtins/image-note-tool.js +8 -4
  107. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  108. package/dist/tools/builtins/plan-tool.js +1 -1
  109. package/dist/tools/builtins/plan-tool.js.map +1 -1
  110. package/dist/tools/builtins/search-tool.js +3 -3
  111. package/dist/tools/builtins/search-tool.js.map +1 -1
  112. package/dist/tools/registry.d.ts +8 -1
  113. package/dist/tools/registry.js +37 -13
  114. package/dist/tools/registry.js.map +1 -1
  115. package/dist/tools/run-tool-use.js +3 -2
  116. package/dist/tools/run-tool-use.js.map +1 -1
  117. package/dist/tools/smoke-exec-process.js +12 -0
  118. package/dist/tools/smoke-exec-process.js.map +1 -1
  119. package/dist/tools/smoke-tool-system.js +68 -31
  120. package/dist/tools/smoke-tool-system.js.map +1 -1
  121. package/dist/tools/tool-catalog.d.ts +36 -0
  122. package/dist/tools/tool-catalog.js +64 -0
  123. package/dist/tools/tool-catalog.js.map +1 -0
  124. package/dist/tools/tool-orchestration.d.ts +20 -3
  125. package/dist/tools/tool-orchestration.js +29 -4
  126. package/dist/tools/tool-orchestration.js.map +1 -1
  127. package/dist/tools/tool.d.ts +13 -0
  128. package/dist/tools/tool.js.map +1 -1
  129. package/dist/types/events.d.ts +28 -1
  130. package/dist/types/messages.d.ts +13 -5
  131. package/dist/types/messages.js +4 -0
  132. package/dist/types/messages.js.map +1 -1
  133. package/dist/ui/display-message.d.ts +10 -2
  134. package/dist/ui/display-message.js +18 -9
  135. package/dist/ui/display-message.js.map +1 -1
  136. package/dist/web/index.d.ts +198 -9
  137. package/dist/web/index.js +1208 -146
  138. package/dist/web/index.js.map +1 -1
  139. package/dist/web/runtime-context-protocol.d.ts +19 -1
  140. package/dist/web/runtime-context-protocol.js +19 -3
  141. package/dist/web/runtime-context-protocol.js.map +1 -1
  142. package/dist/web/smoke-runtime-context-protocol.js +13 -2
  143. package/dist/web/smoke-runtime-context-protocol.js.map +1 -1
  144. package/dist/web/smoke-web-history.d.ts +1 -0
  145. package/dist/web/smoke-web-history.js +179 -0
  146. package/dist/web/smoke-web-history.js.map +1 -0
  147. package/dist/web/smoke-web-terminal-tasks.js +35 -3
  148. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  149. package/package.json +4 -1
  150. package/dist/tasks/task-tools.d.ts +0 -31
  151. package/dist/tasks/task-tools.js.map +0 -1
package/dist/web/index.js CHANGED
@@ -14,6 +14,7 @@ import { findModelMetadata, loadModelCatalog, reasoningEffortsForModel, resolveC
14
14
  import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
15
15
  import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
16
16
  import { ToolRegistry } from "../tools/registry.js";
17
+ import { builtinToolPresentation, toolPresentationForSource } from "../tools/tool-catalog.js";
17
18
  import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
18
19
  import { createExecTool, createWriteStdinTool } from "../tools/builtins/exec-tool.js";
19
20
  import { ExecProcessManager } from "../tools/builtins/exec-process-manager.js";
@@ -24,17 +25,20 @@ import { planTool } from "../tools/builtins/plan-tool.js";
24
25
  import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
25
26
  import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
26
27
  import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
27
- import { createTaskTools } from "../tasks/task-tools.js";
28
+ import { createSubagentTools } from "../tasks/subagent-tools.js";
28
29
  import { TaskStore } from "../tasks/task-store.js";
29
30
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
30
31
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
32
  import { DefaultContextManager } from "../context/context-manager.js";
33
+ import { createImageId } from "../types/messages.js";
32
34
  import { WEB_HTML } from "./html.js";
33
35
  import { openDirectory } from "../open-directory.js";
34
- import { resolveImageBlockDataSync } from "../core/image-storage.js";
36
+ import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
35
37
  import { isWebPlanPayload, serializeWebPlanPayload, webPlanBodyTitle } from "./plan-payload.js";
36
38
  import { createWebRuntimeContextPayload } from "./runtime-context-protocol.js";
37
39
  const require = createRequire(import.meta.url);
40
+ const corePackage = require("../../package.json");
41
+ const CORE_VERSION = String(corePackage.version || "unknown");
38
42
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
39
43
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
40
44
  const markedAssetPath = path.join(markedPackageDir, "lib", "marked.esm.js");
@@ -127,26 +131,42 @@ export async function createWebRuntime(options = {}) {
127
131
  const pluginOverrides = normalizePluginOverrides(options.sessionPluginOverrides, pluginCatalog);
128
132
  const activePlugins = activeWebRuntimePlugins(pluginCatalog, pluginOverrides);
129
133
  const tools = new ToolRegistry();
134
+ const toolCatalog = [];
135
+ const catalogedToolNames = new Set();
136
+ const catalogTool = (tool, item) => {
137
+ if (catalogedToolNames.has(tool.name))
138
+ return;
139
+ catalogedToolNames.add(tool.name);
140
+ toolCatalog.push({ name: tool.name, presentation: toolPresentationForSource(tool.name, item.source), ...item });
141
+ };
142
+ const registerTool = (tool, item) => {
143
+ tools.register(tool);
144
+ catalogTool(tool, item);
145
+ };
130
146
  const execProcessManager = new ExecProcessManager();
131
- tools.register(editTool);
132
- tools.register(writeTool);
133
- tools.register(createExecTool({ processManager: execProcessManager }));
134
- tools.register(createWriteStdinTool(execProcessManager));
135
- tools.register(listDirectoryTool);
136
- tools.register(readFileTool);
137
- tools.register(grepTool);
138
- tools.register(searchTool);
139
- tools.register(createLoadImageTool());
147
+ registerTool(editTool, { source: "builtin" });
148
+ registerTool(writeTool, { source: "builtin" });
149
+ registerTool(createExecTool({ processManager: execProcessManager }), { source: "builtin" });
150
+ registerTool(createWriteStdinTool(execProcessManager), { source: "builtin" });
151
+ registerTool(listDirectoryTool, { source: "builtin" });
152
+ registerTool(readFileTool, { source: "builtin" });
153
+ registerTool(grepTool, { source: "builtin" });
154
+ registerTool(searchTool, { source: "builtin" });
155
+ registerTool(createLoadImageTool(), { source: "builtin" });
140
156
  if (modelConfig?.provider === "openai")
141
- tools.register(createOpenAIImageGenerationTool());
142
- tools.register(planTool);
157
+ registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
158
+ registerTool(planTool, { source: "builtin" });
143
159
  for (const tool of options.externalTools ?? [])
144
- tools.register(tool);
160
+ registerTool(tool, { source: "external" });
161
+ for (const plugin of pluginCatalog) {
162
+ for (const tool of plugin.tools)
163
+ catalogTool(tool, { source: "plugin", pluginId: plugin.id, pluginName: plugin.name });
164
+ }
145
165
  for (const plugin of activePlugins)
146
166
  for (const tool of plugin.tools)
147
167
  tools.register(tool);
148
168
  const agentRuntime = { modelGateway, tools, taskStore };
149
- tools.register(createAgentTool(agentRuntime));
169
+ registerTool(createAgentTool(agentRuntime), { source: "builtin" });
150
170
  const resumeHandler = async (taskId, directive) => {
151
171
  const dummyContext = {
152
172
  agentId: "main",
@@ -156,8 +176,11 @@ export async function createWebRuntime(options = {}) {
156
176
  };
157
177
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
158
178
  };
159
- for (const tool of createTaskTools(taskStore, resumeHandler))
160
- tools.register(tool);
179
+ for (const tool of createSubagentTools(taskStore, resumeHandler))
180
+ registerTool(tool, { source: "builtin" });
181
+ const globalToolOverrides = normalizeToolOverrides(options.globalToolOverrides, toolCatalog);
182
+ const sessionToolOverrides = normalizeToolOverrides(options.sessionToolOverrides, toolCatalog);
183
+ applyToolOverrides(tools, toolCatalog, globalToolOverrides, sessionToolOverrides);
161
184
  const appPromptStore = new InMemoryAppPromptStore();
162
185
  const engine = new QueryEngine({
163
186
  agentId: options.agentId ?? "main",
@@ -176,6 +199,8 @@ export async function createWebRuntime(options = {}) {
176
199
  taskNotificationSource: createTaskNotificationSource(taskStore),
177
200
  commands: replCommandDefinitions.map((command) => command.usage),
178
201
  plugins: [...(options.plugins ?? []), ...activePlugins.map((plugin) => plugin.id)],
202
+ cwdTransitionPaths: options.cwdTransitionPaths,
203
+ onCwdTransitionConsumed: options.onCwdTransitionConsumed,
179
204
  session: {
180
205
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
181
206
  sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
@@ -208,6 +233,14 @@ export async function createWebRuntime(options = {}) {
208
233
  persist: options.persistSessionPluginOverrides,
209
234
  resolve: options.resolveSessionPluginOverrides,
210
235
  } : undefined,
236
+ toolSupport: {
237
+ catalog: toolCatalog,
238
+ globalOverrides: globalToolOverrides,
239
+ sessionOverrides: sessionToolOverrides,
240
+ persistGlobal: options.persistGlobalToolOverrides,
241
+ persistSession: options.persistSessionToolOverrides,
242
+ resolveSession: options.resolveSessionToolOverrides,
243
+ },
211
244
  };
212
245
  }
213
246
  function normalizeWebRuntimePlugins(value) {
@@ -233,6 +266,14 @@ function normalizePluginOverrides(value, catalog) {
233
266
  function activeWebRuntimePlugins(catalog, overrides) {
234
267
  return catalog.filter((plugin) => plugin.globallyEnabled && overrides[plugin.id] !== false);
235
268
  }
269
+ function normalizeToolOverrides(value, catalog) {
270
+ const available = new Set(catalog.map((tool) => tool.name));
271
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([name, enabled]) => available.has(name) && typeof enabled === "boolean"));
272
+ }
273
+ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides) {
274
+ for (const tool of catalog)
275
+ registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
276
+ }
236
277
  function createTaskNotificationSource(taskStore) {
237
278
  return {
238
279
  collectUnnotifiedCompletions() {
@@ -250,7 +291,7 @@ function createTaskNotificationSource(taskStore) {
250
291
  };
251
292
  }
252
293
  function syncImageGenerationTool(runtime, provider) {
253
- runtime.tools.unregister("image2");
294
+ runtime.tools.unregister("image_create");
254
295
  if (provider === "openai")
255
296
  runtime.tools.register(createOpenAIImageGenerationTool());
256
297
  }
@@ -266,6 +307,8 @@ export class WebRuntimeRouter {
266
307
  repls = new Map();
267
308
  createRuntime;
268
309
  createRepl;
310
+ clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
311
+ coreVersion = process.env.NEO_CORE_VERSION?.trim() || CORE_VERSION;
269
312
  constructor(options = {}) {
270
313
  this.createRuntime = options.createRuntime ?? createWebRuntime;
271
314
  this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
@@ -286,6 +329,24 @@ export class WebRuntimeRouter {
286
329
  activeScopes() {
287
330
  return [...this.repls.keys()];
288
331
  }
332
+ clientInfo() {
333
+ return {
334
+ protocolVersion: 1,
335
+ revision: this.clientRevision,
336
+ coreVersion: this.coreVersion,
337
+ };
338
+ }
339
+ async requestClientReload(options = {}) {
340
+ const payload = {
341
+ ...this.clientInfo(),
342
+ requestId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
343
+ clearCache: options.clearCache !== false,
344
+ reason: options.reason?.trim() || "deployment",
345
+ };
346
+ const repls = await Promise.all([...this.repls.values()]);
347
+ const clients = repls.reduce((total, repl) => total + repl.requestClientReload(payload), 0);
348
+ return { ...payload, clients };
349
+ }
289
350
  }
290
351
  export async function createWebRuntimeRouter(options = {}) {
291
352
  const router = new WebRuntimeRouter(options);
@@ -308,6 +369,8 @@ export class WebRepl {
308
369
  pendingDeltaOperations = [];
309
370
  pendingDeltaStatus = false;
310
371
  pendingDeltaTimer;
372
+ pendingTerminalOutput = new Map();
373
+ terminalOutputTimer;
311
374
  lineId = 0;
312
375
  assistantLineId;
313
376
  thinkingLineId;
@@ -322,29 +385,29 @@ export class WebRepl {
322
385
  queuedAttachments;
323
386
  foregroundRun;
324
387
  foregroundRunToken = 0;
325
- pendingUserImageEchoLabels;
388
+ pendingUserImageEchoIds;
326
389
  pendingUserImageEchoMessageId;
327
390
  runtimeContextRevision = 0;
328
391
  runtimeContextPublishQueue = Promise.resolve();
329
392
  backgroundSessionRuns = new Map();
330
393
  suppressReattachedStreaming = new Set();
331
- backgroundTaskCount;
332
394
  constructor(runtime) {
333
395
  this.runtime = runtime;
334
- this.lines = initialLines(runtime, { current: 0 });
396
+ const lineId = { current: 0 };
397
+ this.lines = initialLines(runtime, lineId);
398
+ this.lineId = lineId.current;
335
399
  this.status = initialStatus(runtime);
336
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
337
400
  runtime.taskStore.subscribe(() => {
338
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
339
401
  this.broadcastSync();
340
402
  });
341
403
  runtime.execProcessManager.subscribe(() => {
342
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
404
+ this.dropInactiveTerminalOutput();
343
405
  this.broadcastSync();
344
406
  });
407
+ runtime.execProcessManager.subscribeOutput((delta) => this.queueTerminalOutput(delta));
345
408
  runtime.engine.onSessionTitleChange(() => this.broadcastSync());
346
409
  }
347
- subscribe(res) {
410
+ subscribe(res, clientInfo) {
348
411
  const subscriber = { response: res, blocked: false, needsSync: false, needsRuntimeContext: false };
349
412
  this.subscribers.add(subscriber);
350
413
  res.writeHead(200, {
@@ -353,19 +416,31 @@ export class WebRepl {
353
416
  Connection: "keep-alive",
354
417
  "X-Accel-Buffering": "no",
355
418
  });
419
+ if (clientInfo)
420
+ this.send(subscriber, "client.version", clientInfo);
356
421
  this.send(subscriber, "sync", this.snapshot(true));
357
422
  this.publishRuntimeContext([subscriber]);
358
423
  reqKeepAlive(res);
359
424
  res.on("close", () => this.subscribers.delete(subscriber));
360
425
  }
426
+ get backgroundTaskCount() {
427
+ return this.backgroundTasks().length;
428
+ }
429
+ requestClientReload(payload) {
430
+ const subscribers = [...this.subscribers].filter((subscriber) => !subscriber.response.destroyed);
431
+ for (const subscriber of subscribers)
432
+ this.send(subscriber, "client.reload", payload);
433
+ return subscribers.length;
434
+ }
361
435
  snapshot(includeCatalog = false) {
436
+ const backgroundTasks = this.backgroundTasks();
362
437
  return {
363
438
  lines: this.lines,
364
439
  status: this.status,
365
440
  busy: this.busy,
366
441
  queuedInput: this.queuedInput,
367
- backgroundTaskCount: this.backgroundTaskCount,
368
- backgroundTasks: this.backgroundTasks(),
442
+ backgroundTaskCount: backgroundTasks.length,
443
+ backgroundTasks,
369
444
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
370
445
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
371
446
  session: this.runtime.engine.snapshot().session,
@@ -401,6 +476,91 @@ export class WebRepl {
401
476
  }),
402
477
  };
403
478
  }
479
+ globalTools() {
480
+ const support = this.runtime.toolSupport;
481
+ if (!support)
482
+ return { items: [] };
483
+ return {
484
+ items: support.catalog.map((tool) => ({
485
+ ...tool,
486
+ configuredEnabled: support.globalOverrides[tool.name] ?? true,
487
+ })),
488
+ };
489
+ }
490
+ sessionTools() {
491
+ const support = this.runtime.toolSupport;
492
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
493
+ if (!support)
494
+ return { sessionId, busy: this.busy, items: [] };
495
+ const activePluginIds = new Set(this.runtime.pluginSupport
496
+ ? activeWebRuntimePlugins(this.runtime.pluginSupport.catalog, this.runtime.pluginSupport.overrides).map((plugin) => plugin.id)
497
+ : []);
498
+ return {
499
+ sessionId,
500
+ busy: this.busy,
501
+ items: support.catalog.map((tool) => {
502
+ const override = support.sessionOverrides[tool.name];
503
+ const globallyEnabled = support.globalOverrides[tool.name] ?? true;
504
+ const available = tool.source !== "plugin" || Boolean(tool.pluginId && activePluginIds.has(tool.pluginId));
505
+ return {
506
+ ...tool,
507
+ available,
508
+ globallyEnabled,
509
+ mode: override === undefined ? "inherit" : override ? "enabled" : "disabled",
510
+ effectiveEnabled: available && (override ?? globallyEnabled),
511
+ };
512
+ }),
513
+ };
514
+ }
515
+ async setGlobalTools(value) {
516
+ if (this.busy)
517
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change global tools while a response is running");
518
+ if (!isRecord(value))
519
+ return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
520
+ const support = this.runtime.toolSupport;
521
+ if (!support)
522
+ return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
523
+ const normalized = this.parseToolOverrides(value, support);
524
+ if (!normalized.ok)
525
+ return normalized;
526
+ try {
527
+ support.globalOverrides = normalized.value;
528
+ this.syncPluginToolRegistration();
529
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
530
+ if (support.persistGlobal)
531
+ await support.persistGlobal(support.globalOverrides);
532
+ await this.refreshToolConfiguration();
533
+ return { ok: true, state: this.globalTools() };
534
+ }
535
+ catch (error) {
536
+ return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
537
+ }
538
+ }
539
+ async setSessionTools(value) {
540
+ if (this.busy)
541
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
542
+ if (!isRecord(value))
543
+ return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
544
+ const support = this.runtime.toolSupport;
545
+ if (!support)
546
+ return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
547
+ const normalized = this.parseToolModes(value, support);
548
+ if (!normalized.ok)
549
+ return normalized;
550
+ try {
551
+ await this.applySessionToolOverrides(normalized.value, true);
552
+ return { ok: true, state: this.sessionTools() };
553
+ }
554
+ catch (error) {
555
+ return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
556
+ }
557
+ }
558
+ async loadSessionTools(sessionId) {
559
+ const support = this.runtime.toolSupport;
560
+ if (!support?.resolveSession || !sessionId)
561
+ return;
562
+ await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
563
+ }
404
564
  async setSessionPlugins(value) {
405
565
  if (this.busy)
406
566
  return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
@@ -435,6 +595,64 @@ export class WebRepl {
435
595
  return;
436
596
  await this.applySessionPluginOverrides(await support.resolve(sessionId), false);
437
597
  }
598
+ parseToolOverrides(value, support) {
599
+ const available = new Set(support.catalog.map((tool) => tool.name));
600
+ const overrides = {};
601
+ for (const [name, enabled] of Object.entries(value)) {
602
+ if (!available.has(name))
603
+ return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
604
+ if (typeof enabled !== "boolean")
605
+ return actionFailure("TOOL_INVALID", `invalid tool state: ${name}`);
606
+ overrides[name] = enabled;
607
+ }
608
+ return { ok: true, value: overrides };
609
+ }
610
+ parseToolModes(value, support) {
611
+ const available = new Set(support.catalog.map((tool) => tool.name));
612
+ const overrides = {};
613
+ for (const [name, mode] of Object.entries(value)) {
614
+ if (!available.has(name))
615
+ return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
616
+ if (mode === "enabled" || mode === true)
617
+ overrides[name] = true;
618
+ else if (mode === "disabled" || mode === false)
619
+ overrides[name] = false;
620
+ else if (mode !== "inherit" && mode !== null && mode !== undefined)
621
+ return actionFailure("TOOL_INVALID", `invalid tool mode: ${name}`);
622
+ }
623
+ return { ok: true, value: overrides };
624
+ }
625
+ async applySessionToolOverrides(overrides, persist) {
626
+ const support = this.runtime.toolSupport;
627
+ if (!support)
628
+ return;
629
+ support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
630
+ this.syncPluginToolRegistration();
631
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
632
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
633
+ if (persist && sessionId && support.persistSession)
634
+ await support.persistSession(sessionId, support.sessionOverrides);
635
+ await this.refreshToolConfiguration();
636
+ }
637
+ syncPluginToolRegistration() {
638
+ const pluginSupport = this.runtime.pluginSupport;
639
+ if (!pluginSupport)
640
+ return;
641
+ for (const plugin of pluginSupport.catalog)
642
+ for (const tool of plugin.tools)
643
+ this.runtime.tools.unregister(tool.name);
644
+ for (const plugin of activeWebRuntimePlugins(pluginSupport.catalog, pluginSupport.overrides)) {
645
+ for (const tool of plugin.tools)
646
+ this.runtime.tools.register(tool);
647
+ }
648
+ }
649
+ async refreshToolConfiguration() {
650
+ const metrics = await this.runtime.engine.contextMetrics();
651
+ this.runtime.initialMetrics = metrics;
652
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
653
+ this.broadcastSync();
654
+ this.publishRuntimeContext();
655
+ }
438
656
  async applySessionPluginOverrides(overrides, persist) {
439
657
  const support = this.runtime.pluginSupport;
440
658
  if (!support)
@@ -448,6 +666,9 @@ export class WebRepl {
448
666
  for (const tool of plugin.tools)
449
667
  this.runtime.tools.register(tool);
450
668
  support.overrides = normalized;
669
+ const toolSupport = this.runtime.toolSupport;
670
+ if (toolSupport)
671
+ applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
451
672
  this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
452
673
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
453
674
  if (persist && sessionId && support.persist)
@@ -474,6 +695,7 @@ export class WebRepl {
474
695
  this.broadcastSync();
475
696
  return { ok: true };
476
697
  }
698
+ this.publishRuntimeContext();
477
699
  this.startRun(text, attachments);
478
700
  return { ok: true };
479
701
  }
@@ -532,7 +754,7 @@ export class WebRepl {
532
754
  const snapshot = this.runtime.engine.snapshot().session;
533
755
  if (!snapshot)
534
756
  throw new Error("session transcripts are disabled");
535
- await this.loadSessionPlugins(snapshot.sessionId);
757
+ await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
536
758
  await this.refreshSessionView();
537
759
  return { ok: true };
538
760
  }
@@ -549,7 +771,7 @@ export class WebRepl {
549
771
  const snapshot = this.runtime.engine.snapshot().session;
550
772
  if (!snapshot)
551
773
  throw new Error("session transcripts are disabled");
552
- await this.loadSessionPlugins(snapshot.sessionId);
774
+ await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
553
775
  await this.refreshSessionView();
554
776
  return { ok: true };
555
777
  }
@@ -628,22 +850,28 @@ export class WebRepl {
628
850
  imagePayload(messageId, blockIndex) {
629
851
  if (!messageId || !Number.isInteger(blockIndex) || blockIndex < 0)
630
852
  return undefined;
631
- const message = this.runtime.engine.getHistoryMessages().find((item) => item.id === messageId);
632
- const block = message?.blocks[blockIndex];
633
- if (!block || block.type !== "image")
634
- return undefined;
635
- const resolved = resolveImageBlockDataSync(block)?.trim();
636
- if (!resolved)
637
- return undefined;
638
- const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/iu.exec(resolved);
639
- const mimeType = (dataUrl?.[1] ?? block.mimeType).toLowerCase();
640
- if (!mimeType.startsWith("image/"))
641
- return undefined;
642
- const base64 = (dataUrl?.[2] ?? resolved).replace(/\s+/gu, "");
643
- if (!base64 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
853
+ let block;
854
+ for (const entry of this.runtime.engine.getDisplayEntries()) {
855
+ if (entry.type !== "message" || entry.message.id !== messageId)
856
+ continue;
857
+ const candidate = entry.message.blocks[blockIndex];
858
+ if (candidate?.type === "image")
859
+ block = candidate;
860
+ break;
861
+ }
862
+ return block ? imagePayloadForBlock(block) : undefined;
863
+ }
864
+ imagePayloadById(imageId) {
865
+ if (!imageId)
644
866
  return undefined;
645
- const body = Buffer.from(base64, "base64");
646
- return body.length ? { body, mimeType } : undefined;
867
+ for (const entry of this.runtime.engine.getDisplayEntries()) {
868
+ if (entry.type !== "message")
869
+ continue;
870
+ const block = entry.message.blocks.find((candidate) => candidate.type === "image" && candidate.imageId === imageId);
871
+ if (block)
872
+ return imagePayloadForBlock(block);
873
+ }
874
+ return undefined;
647
875
  }
648
876
  async setFastMode(enabled) {
649
877
  try {
@@ -655,6 +883,25 @@ export class WebRepl {
655
883
  return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
656
884
  }
657
885
  }
886
+ async setContextWindowK(value) {
887
+ if (this.busy)
888
+ return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
889
+ if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
890
+ return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
891
+ }
892
+ const contextWindowK = Number(value);
893
+ const contextWindowTokens = contextWindowK * 1000;
894
+ try {
895
+ await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
896
+ const metrics = await this.runtime.engine.contextMetrics();
897
+ this.runtime.initialMetrics = metrics;
898
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
899
+ return { ok: true, contextWindowK, contextWindowTokens };
900
+ }
901
+ catch (error) {
902
+ return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
903
+ }
904
+ }
658
905
  loginForm(providerValue) {
659
906
  const provider = parseLoginProvider(providerValue);
660
907
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -711,6 +958,12 @@ export class WebRepl {
711
958
  this.broadcastSync();
712
959
  return id;
713
960
  }
961
+ appendCompaction(report, createdAt) {
962
+ this.lines = this.lines.map((line) => line.compaction
963
+ ? { ...line, compaction: { ...line.compaction, current: false } }
964
+ : line);
965
+ return this.append(compactionLine(report, createdAt, true));
966
+ }
714
967
  appendLineText(id, text) {
715
968
  this.lines = this.lines.map((line) => line.id === id ? { ...line, text: line.text + text } : line);
716
969
  this.broadcastDelta({ type: "line.text.append", id, text });
@@ -749,10 +1002,10 @@ export class WebRepl {
749
1002
  this.interruptArmed = false;
750
1003
  this.queuedInput = undefined;
751
1004
  this.queuedAttachments = undefined;
752
- this.pendingUserImageEchoLabels = undefined;
1005
+ this.pendingUserImageEchoIds = undefined;
753
1006
  this.pendingUserImageEchoMessageId = undefined;
754
1007
  this.finalizeForegroundView();
755
- this.finalizeLiveToolLines();
1008
+ this.cancelLiveToolLines(reason);
756
1009
  this.busy = false;
757
1010
  this.status = { ...this.status, phase: "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
758
1011
  this.broadcastSync();
@@ -765,10 +1018,15 @@ export class WebRepl {
765
1018
  kind: "agent",
766
1019
  taskId: task.taskId,
767
1020
  agentId: task.agentId,
1021
+ agentType: task.agentType,
768
1022
  type: task.type,
769
1023
  status: task.status,
770
1024
  description: task.description,
1025
+ prompt: task.prompt,
1026
+ progress: task.progress,
1027
+ outputFile: task.outputFile,
771
1028
  createdAt: task.createdAt,
1029
+ updatedAt: task.updatedAt,
772
1030
  }));
773
1031
  const terminalTasks = this.runtime.execProcessManager.list()
774
1032
  .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
@@ -786,8 +1044,26 @@ export class WebRepl {
786
1044
  shell: terminal.shell,
787
1045
  tty: terminal.tty,
788
1046
  durationMs: terminal.duration_ms,
1047
+ output: terminal.output,
1048
+ outputEnd: terminal.outputEnd,
1049
+ }));
1050
+ const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1051
+ kind: "session",
1052
+ taskId: `session:${run.sessionId}`,
1053
+ type: "会话",
1054
+ status: "running",
1055
+ description: run.title || "未命名会话",
1056
+ createdAt: run.startedAt,
1057
+ sessionId: run.sessionId,
789
1058
  }));
790
- return [...agentTasks, ...terminalTasks];
1059
+ const createdAtTime = (value) => {
1060
+ if (typeof value === "number")
1061
+ return value;
1062
+ const parsed = Date.parse(value);
1063
+ return Number.isFinite(parsed) ? parsed : 0;
1064
+ };
1065
+ return [...agentTasks, ...terminalTasks, ...sessionTasks]
1066
+ .sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
791
1067
  }
792
1068
  async detachRunningForeground(reason) {
793
1069
  if (!this.busy)
@@ -816,7 +1092,7 @@ export class WebRepl {
816
1092
  this.interruptArmed = false;
817
1093
  this.queuedInput = undefined;
818
1094
  this.queuedAttachments = undefined;
819
- this.pendingUserImageEchoLabels = undefined;
1095
+ this.pendingUserImageEchoIds = undefined;
820
1096
  this.pendingUserImageEchoMessageId = undefined;
821
1097
  this.busy = false;
822
1098
  this.status = { ...this.status, phase: "ready", detail: undefined };
@@ -858,11 +1134,145 @@ export class WebRepl {
858
1134
  this.finalizeLiveLine(id);
859
1135
  this.liveToolLineIds.clear();
860
1136
  }
1137
+ cancelLiveToolLines(reason) {
1138
+ for (const id of this.liveToolLineIds.values()) {
1139
+ const line = this.lines.find((item) => item.id === id);
1140
+ const steps = (line?.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "failed", phase: "cancelled" } : step);
1141
+ this.lines = this.lines.map((item) => item.id === id ? { ...item, live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...item.toolStream, phase: "cancelled", message: reason, steps } } : item);
1142
+ this.queueDeltaOperation({ type: "line.patch", id, patch: { live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...line?.toolStream, phase: "cancelled", message: reason, steps } } });
1143
+ }
1144
+ this.liveToolLineIds.clear();
1145
+ }
1146
+ patchLiveToolLine(toolUseId, patch) {
1147
+ const id = this.liveToolLineIds.get(toolUseId);
1148
+ if (id === undefined)
1149
+ return;
1150
+ const line = this.lines.find((item) => item.id === id);
1151
+ if (!line)
1152
+ return;
1153
+ this.lines = this.lines.map((item) => item.id === id ? { ...item, ...patch } : item);
1154
+ this.queueDeltaOperation({ type: "line.patch", id, patch });
1155
+ }
1156
+ applyToolProgress(event) {
1157
+ const id = this.liveToolLineIds.get(event.toolUse.id);
1158
+ if (id === undefined)
1159
+ return;
1160
+ const line = this.lines.find((item) => item.id === id);
1161
+ if (!line)
1162
+ return;
1163
+ const progress = event.progress;
1164
+ const previousStream = line.toolStream ?? {};
1165
+ if (progress.sequence !== undefined && previousStream.sequence !== undefined && progress.sequence <= previousStream.sequence)
1166
+ return;
1167
+ const channel = progress.channel ?? inferToolStreamChannel(progress.data);
1168
+ const operation = progress.operation ?? (channel === "stdout" || channel === "stderr" ? "append" : "replace");
1169
+ const stream = { ...previousStream };
1170
+ stream.channel = channel;
1171
+ stream.phase = progress.phase;
1172
+ stream.sequence = progress.sequence ?? stream.sequence;
1173
+ if (progress.progress) {
1174
+ stream.current = progress.progress.current;
1175
+ stream.total = progress.progress.total;
1176
+ stream.unit = progress.progress.unit;
1177
+ }
1178
+ const terminalDelta = terminalOutputDelta(progress.data);
1179
+ const textDelta = toolStreamTextDelta(progress.data);
1180
+ if (terminalDelta) {
1181
+ const key = terminalDelta.stream === "stderr" ? "stderr" : "stdout";
1182
+ stream[key] = operation === "replace"
1183
+ ? trimToolStreamText(terminalDelta.text, 40_000)
1184
+ : appendToolStreamText(stream[key], terminalDelta.text, 40_000);
1185
+ stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, key, terminalDelta.text, operation);
1186
+ }
1187
+ else if ((channel === "stdout" || channel === "stderr") && textDelta !== undefined) {
1188
+ stream[channel] = operation === "replace"
1189
+ ? trimToolStreamText(textDelta, 40_000)
1190
+ : appendToolStreamText(stream[channel], textDelta, 40_000);
1191
+ stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, channel, textDelta, operation);
1192
+ }
1193
+ else if ((channel === "patch" || channel === "artifact") && textDelta !== undefined) {
1194
+ stream.output = operation === "replace"
1195
+ ? trimToolStreamText(textDelta, 40_000)
1196
+ : appendToolStreamText(stream.output, textDelta, 40_000);
1197
+ stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, "output", textDelta, operation);
1198
+ stream.message = progress.message;
1199
+ }
1200
+ else if (channel === "item") {
1201
+ const key = progress.key || progress.phase || `step-${progress.sequence ?? Date.now()}`;
1202
+ const steps = [...(stream.steps ?? [])];
1203
+ const index = steps.findIndex((step) => step.key === key);
1204
+ if (operation === "remove") {
1205
+ if (index >= 0)
1206
+ steps.splice(index, 1);
1207
+ }
1208
+ else {
1209
+ const status = toolStreamStepStatus(progress.phase);
1210
+ const message = operation === "append" && index >= 0
1211
+ ? `${steps[index].message}${progress.message}`
1212
+ : progress.message;
1213
+ const stepToolName = childToolName(progress.data) ?? steps[index]?.toolName;
1214
+ const step = {
1215
+ key,
1216
+ message,
1217
+ toolName: stepToolName,
1218
+ toolLabel: stepToolName ? toolTitle(stepToolName, "running") : steps[index]?.toolLabel,
1219
+ status,
1220
+ phase: progress.phase,
1221
+ sequence: progress.sequence,
1222
+ };
1223
+ if (index >= 0)
1224
+ steps[index] = step;
1225
+ else
1226
+ steps.push(step);
1227
+ }
1228
+ stream.steps = steps.slice(-24);
1229
+ stream.message = progress.message;
1230
+ }
1231
+ else if (operation === "append") {
1232
+ stream.message = `${stream.message ?? ""}${progress.message}`;
1233
+ }
1234
+ else if (operation === "remove") {
1235
+ stream.message = undefined;
1236
+ }
1237
+ else {
1238
+ stream.message = progress.message;
1239
+ }
1240
+ this.patchLiveToolLine(event.toolUse.id, { toolStream: stream });
1241
+ }
1242
+ applyAvailableToolResult(event) {
1243
+ const liveLineId = this.liveToolLineIds.get(event.toolUse.id);
1244
+ if (liveLineId === undefined)
1245
+ return;
1246
+ const liveLine = this.lines.find((item) => item.id === liveLineId);
1247
+ if (!liveLine)
1248
+ return;
1249
+ let replacement;
1250
+ for (const message of event.messages) {
1251
+ renderToolResultMessage(message, (line, toolUseId) => {
1252
+ if (toolUseId === event.toolUse.id)
1253
+ replacement = line;
1254
+ return liveLineId;
1255
+ }, runtimeToolResultPresenter(this.runtime));
1256
+ }
1257
+ const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: event.ok ? "completed" : "failed" } : step);
1258
+ if (replacement) {
1259
+ this.replaceLine(liveLineId, {
1260
+ ...replacement,
1261
+ live: false,
1262
+ presentationLevel: mergePresentationLevel(replacement.presentationLevel, liveLine.presentationLevel),
1263
+ toolDisplay: mergeToolDisplays(replacement.toolDisplay, liveLine.toolDisplay),
1264
+ toolStream: { ...liveLine.toolStream, steps, message: liveLine.toolStream?.message },
1265
+ });
1266
+ }
1267
+ else {
1268
+ this.replaceLine(liveLineId, { live: false, titleStatus: event.ok ? "success" : "failure", toolStream: { ...liveLine.toolStream, steps } });
1269
+ }
1270
+ }
861
1271
  handleEvent(event) {
862
1272
  this.reduce(event);
863
1273
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
864
1274
  const messageId = this.pendingUserImageEchoMessageId;
865
- this.pendingUserImageEchoLabels = undefined;
1275
+ this.pendingUserImageEchoIds = undefined;
866
1276
  this.pendingUserImageEchoMessageId = undefined;
867
1277
  renderMessageImages(event.message, (line) => this.append(line), messageId);
868
1278
  this.broadcastSync();
@@ -872,6 +1282,10 @@ export class WebRepl {
872
1282
  this.queueDeltaStatus();
873
1283
  return;
874
1284
  }
1285
+ if (event.type === "context.compacted") {
1286
+ this.appendCompaction(event.compaction);
1287
+ return;
1288
+ }
875
1289
  if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
876
1290
  this.broadcastSync();
877
1291
  return;
@@ -890,6 +1304,18 @@ export class WebRepl {
890
1304
  this.appendLineText(id, event.text);
891
1305
  return;
892
1306
  }
1307
+ if (event.type === "tool.progress") {
1308
+ this.applyToolProgress(event);
1309
+ return;
1310
+ }
1311
+ if (event.type === "tool.result.available") {
1312
+ this.applyAvailableToolResult(event);
1313
+ return;
1314
+ }
1315
+ if (event.type === "tool.batch.completed") {
1316
+ this.broadcastSync();
1317
+ return;
1318
+ }
893
1319
  if (event.type === "message") {
894
1320
  let replacedStreamingContent = false;
895
1321
  if (event.message.role === "assistant" && this.assistantLineId !== undefined) {
@@ -920,12 +1346,19 @@ export class WebRepl {
920
1346
  if (event.message.role === "tool_result") {
921
1347
  renderToolResultMessage(event.message, (line, toolUseId) => {
922
1348
  const liveLineId = this.liveToolLineIds.get(toolUseId);
923
- if (liveLineId === undefined)
1349
+ if (liveLineId === undefined) {
1350
+ const existing = this.lines.find((item) => item.toolUseId === toolUseId);
1351
+ if (existing) {
1352
+ this.replaceLine(existing.id, { ...line, toolStream: existing.toolStream, presentationLevel: mergePresentationLevel(line.presentationLevel, existing.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, existing.toolDisplay) });
1353
+ return existing.id;
1354
+ }
924
1355
  return this.append(line);
925
- this.replaceLine(liveLineId, line);
1356
+ }
1357
+ const liveLine = this.lines.find((item) => item.id === liveLineId);
1358
+ this.replaceLine(liveLineId, { ...line, toolStream: liveLine?.toolStream, presentationLevel: mergePresentationLevel(line.presentationLevel, liveLine?.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, liveLine?.toolDisplay) });
926
1359
  this.liveToolLineIds.delete(toolUseId);
927
1360
  return liveLineId;
928
- });
1361
+ }, runtimeToolResultPresenter(this.runtime));
929
1362
  renderMessageImages(event.message, (line) => this.append(line));
930
1363
  return;
931
1364
  }
@@ -946,16 +1379,15 @@ export class WebRepl {
946
1379
  if (event.type === "tool.started") {
947
1380
  this.finalizeLiveLine(this.assistantLineId);
948
1381
  this.finalizeThinkingLine();
1382
+ this.assistantLineId = undefined;
949
1383
  const id = this.append({ ...formatToolUse(event.toolUse), live: true });
950
1384
  this.liveToolLineIds.set(event.toolUse.id, id);
951
1385
  return;
952
1386
  }
953
1387
  if (event.type === "tool.finished") {
954
1388
  const id = this.liveToolLineIds.get(event.toolUse.id);
955
- if (id !== undefined) {
1389
+ if (id !== undefined)
956
1390
  this.finalizeLiveLine(id);
957
- this.liveToolLineIds.delete(event.toolUse.id);
958
- }
959
1391
  return;
960
1392
  }
961
1393
  if (event.type === "terminal") {
@@ -970,13 +1402,13 @@ export class WebRepl {
970
1402
  this.append({ kind: "error", text: event.error.message });
971
1403
  }
972
1404
  matchesPendingUserImageEcho(message) {
973
- const pending = this.pendingUserImageEchoLabels;
1405
+ const pending = this.pendingUserImageEchoIds;
974
1406
  if (!pending?.length || message.role !== "user")
975
1407
  return false;
976
- const labels = message.blocks
977
- .filter((block) => block.type === "image" && typeof block.label === "string")
978
- .map((block) => block.label);
979
- return labels.length === pending.length && labels.every((label, index) => label === pending[index]);
1408
+ const imageIds = message.blocks
1409
+ .filter((block) => block.type === "image" && typeof block.imageId === "string")
1410
+ .map((block) => block.imageId);
1411
+ return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
980
1412
  }
981
1413
  async handleCommandOrPrompt(text, attachments = []) {
982
1414
  const command = parseReplCommand(text);
@@ -1078,11 +1510,11 @@ export class WebRepl {
1078
1510
  const promptPayload = buildWebPromptPayload(command.text, attachments);
1079
1511
  const optimisticMessageId = `web-user-${this.lineId + 1}`;
1080
1512
  this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
1081
- const optimisticImageLabels = (promptPayload.blocks ?? [])
1082
- .filter((block) => block.type === "image" && typeof block.label === "string")
1083
- .map((block) => block.label);
1084
- this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
1085
- this.pendingUserImageEchoMessageId = optimisticImageLabels.length ? optimisticMessageId : undefined;
1513
+ const optimisticImageIds = (promptPayload.blocks ?? [])
1514
+ .filter((block) => block.type === "image" && typeof block.imageId === "string")
1515
+ .map((block) => block.imageId);
1516
+ this.pendingUserImageEchoIds = optimisticImageIds.length ? optimisticImageIds : undefined;
1517
+ this.pendingUserImageEchoMessageId = optimisticImageIds.length ? optimisticMessageId : undefined;
1086
1518
  const runToken = ++this.foregroundRunToken;
1087
1519
  const abortController = new AbortController();
1088
1520
  this.activeAbortController = abortController;
@@ -1091,7 +1523,12 @@ export class WebRepl {
1091
1523
  this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
1092
1524
  const engine = this.runtime.engine;
1093
1525
  try {
1094
- for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: promptPayload.displayText })) {
1526
+ for await (const event of engine.sendUserText(promptPayload.text, {
1527
+ abortSignal: abortController.signal,
1528
+ blocks: promptPayload.blocks,
1529
+ displayText: promptPayload.displayText,
1530
+ stopAfterTurn: () => this.queuedInput !== undefined,
1531
+ })) {
1095
1532
  if (this.foregroundRunToken !== runToken)
1096
1533
  continue;
1097
1534
  if (this.runtime.engine !== engine)
@@ -1122,7 +1559,7 @@ export class WebRepl {
1122
1559
  this.activeAbortController = undefined;
1123
1560
  this.interruptArmed = false;
1124
1561
  this.finalizeForegroundView();
1125
- this.pendingUserImageEchoLabels = undefined;
1562
+ this.pendingUserImageEchoIds = undefined;
1126
1563
  this.pendingUserImageEchoMessageId = undefined;
1127
1564
  const queuedText = this.queuedInput;
1128
1565
  const queuedAttach = this.queuedAttachments;
@@ -1155,7 +1592,10 @@ export class WebRepl {
1155
1592
  const metrics = await this.runtime.engine.contextMetrics();
1156
1593
  if (this.foregroundRunToken !== runToken)
1157
1594
  return;
1158
- this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
1595
+ if (result.report)
1596
+ this.appendCompaction(result.report);
1597
+ else
1598
+ this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
1159
1599
  this.handleEvent({ type: "context.metrics", metrics });
1160
1600
  }
1161
1601
  catch (error) {
@@ -1174,7 +1614,9 @@ export class WebRepl {
1174
1614
  }
1175
1615
  send(subscriber, event, data) {
1176
1616
  if (subscriber.blocked) {
1177
- if (event === "runtime.context")
1617
+ if (event === "client.reload")
1618
+ subscriber.pendingClientReload = data;
1619
+ else if (event === "runtime.context")
1178
1620
  subscriber.needsRuntimeContext = true;
1179
1621
  else
1180
1622
  subscriber.needsSync = true;
@@ -1189,6 +1631,11 @@ export class WebRepl {
1189
1631
  subscriber.blocked = false;
1190
1632
  if (subscriber.response.destroyed)
1191
1633
  return;
1634
+ if (subscriber.pendingClientReload) {
1635
+ const payload = subscriber.pendingClientReload;
1636
+ subscriber.pendingClientReload = undefined;
1637
+ this.send(subscriber, "client.reload", payload);
1638
+ }
1192
1639
  if (subscriber.needsSync) {
1193
1640
  subscriber.needsSync = false;
1194
1641
  this.send(subscriber, "sync", this.snapshot(false));
@@ -1204,6 +1651,7 @@ export class WebRepl {
1204
1651
  return createWebRuntimeContextPayload(snapshot, {
1205
1652
  revision,
1206
1653
  sessionId: this.runtime.engine.snapshot().session?.sessionId,
1654
+ toolPresentations: Object.fromEntries((this.runtime.toolSupport?.catalog ?? []).map((tool) => [tool.name, tool.presentation])),
1207
1655
  });
1208
1656
  }
1209
1657
  publishRuntimeContext(targets) {
@@ -1264,6 +1712,39 @@ export class WebRepl {
1264
1712
  this.pendingDeltaOperations = [];
1265
1713
  this.pendingDeltaStatus = false;
1266
1714
  }
1715
+ dropInactiveTerminalOutput() {
1716
+ for (const sessionId of this.pendingTerminalOutput.keys()) {
1717
+ if (!this.runtime.execProcessManager.isBackgroundRunning(sessionId))
1718
+ this.pendingTerminalOutput.delete(sessionId);
1719
+ }
1720
+ }
1721
+ queueTerminalOutput(delta) {
1722
+ if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
1723
+ return;
1724
+ const outputStart = Number(delta.outputStart ?? 0);
1725
+ const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
1726
+ const pending = this.pendingTerminalOutput.get(delta.sessionId);
1727
+ if (pending && pending.outputEnd === outputStart) {
1728
+ pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
1729
+ pending.outputEnd = outputEnd;
1730
+ }
1731
+ else {
1732
+ this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
1733
+ }
1734
+ if (this.terminalOutputTimer)
1735
+ return;
1736
+ this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
1737
+ this.terminalOutputTimer.unref?.();
1738
+ }
1739
+ flushTerminalOutput() {
1740
+ this.terminalOutputTimer = undefined;
1741
+ if (!this.pendingTerminalOutput.size)
1742
+ return;
1743
+ const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
1744
+ this.pendingTerminalOutput.clear();
1745
+ for (const subscriber of this.subscribers)
1746
+ this.send(subscriber, "terminal.output", { updates });
1747
+ }
1267
1748
  broadcastSync() {
1268
1749
  this.discardPendingDeltaOperations();
1269
1750
  const payload = this.snapshot(false);
@@ -1271,6 +1752,10 @@ export class WebRepl {
1271
1752
  this.send(subscriber, "sync", payload);
1272
1753
  }
1273
1754
  }
1755
+ function appendBoundedText(current, incoming, limit) {
1756
+ const combined = current + incoming;
1757
+ return combined.length <= limit ? combined : combined.slice(-limit);
1758
+ }
1274
1759
  export function compactLineOperations(operations) {
1275
1760
  const compacted = [];
1276
1761
  for (const operation of operations) {
@@ -1303,20 +1788,67 @@ async function route(req, res, router) {
1303
1788
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1304
1789
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1305
1790
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1791
+ if (req.method === "GET" && url.pathname === "/api/client-info")
1792
+ return sendJson(res, router.clientInfo());
1793
+ if (req.method === "POST" && url.pathname === "/api/client-reload") {
1794
+ const body = await readJsonBody(req);
1795
+ return sendJson(res, await router.requestClientReload(body));
1796
+ }
1306
1797
  const scope = webRuntimeScopeFromUrl(url);
1307
1798
  const repl = await router.get(scope);
1308
1799
  if (req.method === "GET" && url.pathname === "/events")
1309
- return repl.subscribe(res);
1800
+ return repl.subscribe(res, router.clientInfo());
1310
1801
  if (req.method === "GET" && url.pathname === "/api/state")
1311
1802
  return sendJson(res, repl.snapshot(true));
1312
1803
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
1313
1804
  return sendJson(res, await repl.runtimeContext());
1805
+ const workspaceRepl = repl;
1806
+ if (req.method === "GET" && url.pathname === "/api/cwd") {
1807
+ return sendJson(res, workspaceRepl.browseWorkspace
1808
+ ? await workspaceRepl.browseWorkspace(url.searchParams.get("path") ?? undefined)
1809
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1810
+ }
1811
+ if (req.method === "POST" && url.pathname === "/api/cwd/change") {
1812
+ const body = await readJsonBody(req);
1813
+ return sendJson(res, workspaceRepl.changeWorkspace
1814
+ ? await workspaceRepl.changeWorkspace(body.path)
1815
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1816
+ }
1817
+ if (req.method === "POST" && url.pathname === "/api/cwd/create") {
1818
+ const body = await readJsonBody(req);
1819
+ return sendJson(res, workspaceRepl.createWorkspaceDirectory
1820
+ ? await workspaceRepl.createWorkspaceDirectory(body.path)
1821
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1822
+ }
1823
+ if (req.method === "POST" && url.pathname === "/api/cwd/delete") {
1824
+ const body = await readJsonBody(req);
1825
+ return sendJson(res, workspaceRepl.deleteWorkspaceDirectory
1826
+ ? await workspaceRepl.deleteWorkspaceDirectory(body.path)
1827
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1828
+ }
1829
+ if (req.method === "GET" && url.pathname === "/api/tools")
1830
+ return sendJson(res, repl.globalTools());
1831
+ if (req.method === "POST" && url.pathname === "/api/tools/global") {
1832
+ const body = await readJsonBody(req);
1833
+ return sendJson(res, await repl.setGlobalTools(body.overrides));
1834
+ }
1835
+ if (req.method === "GET" && url.pathname === "/api/session-tools")
1836
+ return sendJson(res, repl.sessionTools());
1837
+ if (req.method === "POST" && url.pathname === "/api/session-tools") {
1838
+ const body = await readJsonBody(req);
1839
+ return sendJson(res, await repl.setSessionTools(body.overrides));
1840
+ }
1314
1841
  if (req.method === "GET" && url.pathname === "/api/session-plugins")
1315
1842
  return sendJson(res, repl.sessionPlugins());
1316
1843
  if (req.method === "POST" && url.pathname === "/api/session-plugins") {
1317
1844
  const body = await readJsonBody(req);
1318
1845
  return sendJson(res, await repl.setSessionPlugins(body.overrides));
1319
1846
  }
1847
+ const imageIdMatch = /^\/api\/images\/by-id\/([^/]+)$/u.exec(url.pathname);
1848
+ if (req.method === "GET" && imageIdMatch) {
1849
+ const payload = repl.imagePayloadById(decodeURIComponent(imageIdMatch[1]));
1850
+ return payload ? sendImage(res, payload) : sendJson(res, { error: "image not found" }, 404);
1851
+ }
1320
1852
  const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
1321
1853
  if (req.method === "GET" && imageMatch) {
1322
1854
  const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
@@ -1332,6 +1864,10 @@ async function route(req, res, router) {
1332
1864
  const body = await readJsonBody(req);
1333
1865
  return sendJson(res, await repl.setFastMode(body.enabled === true));
1334
1866
  }
1867
+ if (req.method === "POST" && url.pathname === "/api/context-window") {
1868
+ const body = await readJsonBody(req);
1869
+ return sendJson(res, await repl.setContextWindowK(body.value));
1870
+ }
1335
1871
  if (req.method === "POST" && url.pathname === "/api/submit") {
1336
1872
  const body = await readJsonBody(req);
1337
1873
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
@@ -1393,6 +1929,20 @@ function sendJson(res, value, status = 200) {
1393
1929
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1394
1930
  res.end(JSON.stringify(value));
1395
1931
  }
1932
+ function imagePayloadForBlock(block) {
1933
+ const resolved = resolveImageBlockDataSync(block)?.trim();
1934
+ if (!resolved)
1935
+ return undefined;
1936
+ const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/iu.exec(resolved);
1937
+ const mimeType = (dataUrl?.[1] ?? block.mimeType).toLowerCase();
1938
+ if (!mimeType.startsWith("image/"))
1939
+ return undefined;
1940
+ const base64 = (dataUrl?.[2] ?? resolved).replace(/\s+/gu, "");
1941
+ if (!base64 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
1942
+ return undefined;
1943
+ const body = Buffer.from(base64, "base64");
1944
+ return body.length ? { body, mimeType } : undefined;
1945
+ }
1396
1946
  function sendImage(res, payload) {
1397
1947
  res.writeHead(200, {
1398
1948
  "Content-Type": payload.mimeType,
@@ -1416,7 +1966,8 @@ function sanitizeWebAttachments(value) {
1416
1966
  if (!isRecord(item))
1417
1967
  return [];
1418
1968
  if (item.kind === "image" && typeof item.label === "string" && /^\[img#\d+\]$/.test(item.label) && typeof item.mimeType === "string" && item.mimeType.startsWith("image/") && typeof item.data === "string") {
1419
- return [{ kind: "image", label: item.label, mimeType: item.mimeType, data: item.data }];
1969
+ const imageId = typeof item.imageId === "string" && /^image_[0-9a-f-]{36}$/iu.test(item.imageId) ? item.imageId : createImageId();
1970
+ return [{ kind: "image", imageId, label: item.label, mimeType: item.mimeType, data: item.data }];
1420
1971
  }
1421
1972
  if (item.kind === "file" && typeof item.name === "string" && typeof item.mimeType === "string" && typeof item.size === "number" && Number.isFinite(item.size) && typeof item.absolutePath === "string" && path.isAbsolute(item.absolutePath)) {
1422
1973
  return [{
@@ -1454,14 +2005,44 @@ function initialLines(runtime, lineId) {
1454
2005
  lines.push({ id: ++lineId.current, ...line });
1455
2006
  return lines;
1456
2007
  }
2008
+ export function restoreWebHistoryLines(runtime) {
2009
+ return restoredHistoryLines(runtime).map((line) => ({ ...line }));
2010
+ }
1457
2011
  function restoredHistoryLines(runtime) {
1458
2012
  const lines = [];
2013
+ const historyToolLineIndexes = new Map();
1459
2014
  const append = (line) => {
2015
+ if (line.toolUseId && line.titleStatus && historyToolLineIndexes.has(line.toolUseId)) {
2016
+ const index = historyToolLineIndexes.get(line.toolUseId);
2017
+ const started = lines[index];
2018
+ lines[index] = { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
2019
+ return index + 1;
2020
+ }
1460
2021
  lines.push(line);
2022
+ if (line.toolUseId && !line.titleStatus)
2023
+ historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
1461
2024
  return lines.length;
1462
2025
  };
1463
- for (const message of runtime.engine.getHistoryMessages())
1464
- renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: true });
2026
+ const entries = runtime.engine.getDisplayEntries();
2027
+ let lastCompactionIndex = -1;
2028
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
2029
+ if (entries[index].type === "compact") {
2030
+ lastCompactionIndex = index;
2031
+ break;
2032
+ }
2033
+ }
2034
+ for (const [entryIndex, entry] of entries.entries()) {
2035
+ if (entry.type === "message") {
2036
+ renderMessage(entry.message, append, undefined, {
2037
+ includeToolUseBlocks: true,
2038
+ includeThinkingBlocks: true,
2039
+ presentToolResult: runtimeToolResultPresenter(runtime),
2040
+ });
2041
+ continue;
2042
+ }
2043
+ const report = entry.report ?? legacyCompactionReport(entry.reason);
2044
+ append(compactionLine(report, entry.createdAt, entryIndex === lastCompactionIndex));
2045
+ }
1465
2046
  return lines;
1466
2047
  }
1467
2048
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
@@ -1471,7 +2052,18 @@ async function resetStatus(runtime) {
1471
2052
  return initialStatus(runtime, await runtime.engine.contextMetrics());
1472
2053
  }
1473
2054
  function buildWebPromptPayload(displayText, attachments) {
1474
- const activeAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
2055
+ const candidateAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
2056
+ const labels = new Set();
2057
+ const imageIds = new Set();
2058
+ for (const attachment of candidateAttachments) {
2059
+ if (labels.has(attachment.label))
2060
+ throw new Error(`Duplicate image label in one submission: ${attachment.label}`);
2061
+ if (imageIds.has(attachment.imageId))
2062
+ throw new Error(`Duplicate image identity in one submission: ${attachment.imageId}`);
2063
+ labels.add(attachment.label);
2064
+ imageIds.add(attachment.imageId);
2065
+ }
2066
+ const activeAttachments = candidateAttachments;
1475
2067
  const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
1476
2068
  if (activeAttachments.length === 0) {
1477
2069
  const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
@@ -1486,7 +2078,7 @@ function buildWebPromptPayload(displayText, attachments) {
1486
2078
  break;
1487
2079
  }
1488
2080
  pushTextBlock(blocks, displayText.slice(cursor, next.index));
1489
- blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
2081
+ blocks.push({ type: "image", imageId: next.attachment.imageId, mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
1490
2082
  cursor = next.index + next.attachment.label.length;
1491
2083
  }
1492
2084
  pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
@@ -1531,9 +2123,76 @@ function pushTextBlock(blocks, text) {
1531
2123
  else
1532
2124
  blocks.push({ type: "text", text });
1533
2125
  }
2126
+ function inferToolStreamChannel(data) {
2127
+ if (data && typeof data === "object" && !Array.isArray(data) && data.type === "terminal.output.delta") {
2128
+ return data.stream === "stderr" ? "stderr" : "stdout";
2129
+ }
2130
+ return "state";
2131
+ }
2132
+ function terminalOutputDelta(data) {
2133
+ if (!data || typeof data !== "object" || Array.isArray(data))
2134
+ return undefined;
2135
+ const record = data;
2136
+ if (record.type !== "terminal.output.delta" || typeof record.text !== "string")
2137
+ return undefined;
2138
+ return { stream: record.stream === "stderr" ? "stderr" : "stdout", text: record.text };
2139
+ }
2140
+ function toolStreamTextDelta(data) {
2141
+ if (typeof data === "string")
2142
+ return data;
2143
+ if (!data || typeof data !== "object" || Array.isArray(data))
2144
+ return undefined;
2145
+ const record = data;
2146
+ for (const value of [record.text, record.delta, record.patch, record.content]) {
2147
+ if (typeof value === "string")
2148
+ return value;
2149
+ }
2150
+ return undefined;
2151
+ }
2152
+ function childToolName(data) {
2153
+ if (!data || typeof data !== "object" || Array.isArray(data))
2154
+ return undefined;
2155
+ const childEvent = data.child_event;
2156
+ if (!childEvent || typeof childEvent !== "object" || Array.isArray(childEvent))
2157
+ return undefined;
2158
+ const toolUse = childEvent.toolUse;
2159
+ if (!toolUse || typeof toolUse !== "object" || Array.isArray(toolUse))
2160
+ return undefined;
2161
+ return stringValue(toolUse.name);
2162
+ }
2163
+ function appendToolStreamChunk(chunks, sequence, stream, text, operation) {
2164
+ const next = operation === "replace" ? [] : [...(chunks ?? [])];
2165
+ next.push({ sequence: sequence ?? (next.at(-1)?.sequence ?? 0) + 1, stream, text });
2166
+ let total = 0;
2167
+ const retained = [];
2168
+ for (let index = next.length - 1; index >= 0; index -= 1) {
2169
+ const chunk = next[index];
2170
+ if (total >= 40_000)
2171
+ break;
2172
+ retained.push(chunk);
2173
+ total += chunk.text.length;
2174
+ }
2175
+ return retained.reverse();
2176
+ }
2177
+ function trimToolStreamText(value, maxChars) {
2178
+ return value.length <= maxChars ? value : value.slice(value.length - maxChars);
2179
+ }
2180
+ function appendToolStreamText(current, delta, maxChars) {
2181
+ return trimToolStreamText(`${current ?? ""}${delta}`, maxChars);
2182
+ }
2183
+ function toolStreamStepStatus(phase) {
2184
+ const normalized = phase?.toLowerCase() ?? "";
2185
+ if (normalized.includes("fail") || normalized.includes("error") || normalized.includes("cancel") || normalized.includes("abort"))
2186
+ return "failed";
2187
+ if (normalized.includes("complete") || normalized.includes("finish") || normalized.includes("done") || normalized.includes("success"))
2188
+ return "completed";
2189
+ return "running";
2190
+ }
1534
2191
  function reduceStatus(status, event) {
1535
2192
  if (event.type === "state")
1536
2193
  return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
2194
+ if (event.type === "context.compacted")
2195
+ return { ...status, phase: "compacting", detail: formatCompactionReportSummary(event.compaction), activityTick: status.activityTick + 1 };
1537
2196
  if (event.type === "context.metrics")
1538
2197
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1539
2198
  if (event.type === "usage")
@@ -1548,8 +2207,10 @@ function reduceStatus(status, event) {
1548
2207
  return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
1549
2208
  if (event.type === "tool.started")
1550
2209
  return { ...status, phase: "running_tools", currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1551
- if (event.type === "tool.finished")
1552
- return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
2210
+ if (event.type === "tool.progress")
2211
+ return { ...status, phase: "running_tools", detail: event.progress.message || status.detail, currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: status.currentTool?.id === event.toolUse.id ? status.currentTool.startedAt : Date.now() }, activityTick: status.activityTick + 1 };
2212
+ if (event.type === "tool.result.available" || event.type === "tool.finished" || event.type === "tool.batch.completed")
2213
+ return { ...status, currentTool: "toolUse" in event && status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1553
2214
  if (event.type === "terminal")
1554
2215
  return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1555
2216
  if (event.type === "message" || event.type === "error")
@@ -1862,6 +2523,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1862
2523
  if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
1863
2524
  return true;
1864
2525
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
2526
+ const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
1865
2527
  let rendered = false;
1866
2528
  for (const [blockIndex, block] of message.blocks.entries()) {
1867
2529
  if (block.type === "text") {
@@ -1878,7 +2540,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1878
2540
  const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1879
2541
  if (!line)
1880
2542
  continue;
1881
- append({ ...line, messageId: message.id, parentToolName });
2543
+ append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
1882
2544
  rendered = true;
1883
2545
  }
1884
2546
  else if (block.type === "thinking") {
@@ -1888,22 +2550,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1888
2550
  rendered = true;
1889
2551
  }
1890
2552
  else if (block.type === "tool_use" && options.includeToolUseBlocks) {
1891
- append({ ...formatToolUse(block), messageId: message.id, live: false });
2553
+ append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
1892
2554
  rendered = true;
1893
2555
  }
1894
2556
  else if (block.type === "tool_result") {
1895
- append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
2557
+ append({ ...formatToolResultLine(block.name, block.output, block.ok, options.presentToolResult), messageId: message.id, toolUseId: block.toolUseId });
1896
2558
  rendered = true;
1897
2559
  }
1898
2560
  }
1899
2561
  return rendered;
1900
2562
  }
1901
- function renderToolResultMessage(message, append) {
2563
+ function renderToolResultMessage(message, append, presentToolResult) {
1902
2564
  let rendered = false;
1903
2565
  for (const block of message.blocks) {
1904
2566
  if (block.type !== "tool_result")
1905
2567
  continue;
1906
- append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
2568
+ append({ ...formatToolResultLine(block.name, block.output, block.ok, presentToolResult), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
1907
2569
  rendered = true;
1908
2570
  }
1909
2571
  return rendered;
@@ -1927,13 +2589,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
1927
2589
  const kind = kindForRole(role);
1928
2590
  if (kind === "meta")
1929
2591
  return undefined;
2592
+ const resolution = resolveImageBlockDataResultSync(block);
1930
2593
  return {
1931
2594
  kind,
1932
2595
  text: block.label ?? `[image ${block.mimeType}]`,
1933
2596
  image: {
1934
- src: `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`,
2597
+ src: resolution.available ? (block.imageId ? `/api/images/by-id/${encodeURIComponent(block.imageId)}` : `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`) : undefined,
2598
+ imageId: block.imageId,
1935
2599
  label: block.label,
1936
2600
  mimeType: block.mimeType,
2601
+ available: resolution.available,
2602
+ error: resolution.available ? undefined : resolution.error,
2603
+ blockIndex,
1937
2604
  },
1938
2605
  };
1939
2606
  }
@@ -1985,39 +2652,192 @@ function titleForKind(kind) {
1985
2652
  function systemLine(text, summaryMaxLines) {
1986
2653
  return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
1987
2654
  }
2655
+ function legacyCompactionReport(reason) {
2656
+ return {
2657
+ reason: reason && reason !== "none" ? reason : "manualcompact",
2658
+ summary: "此检查点由旧版 neoctl 持久化,未保存详细压缩报告。",
2659
+ continuationState: "该旧版检查点没有可用的详细续接上下文。",
2660
+ sourceMessages: 0,
2661
+ preservedUserMessages: 0,
2662
+ newWindowMessages: 0,
2663
+ charsFreed: 0,
2664
+ modelDriven: false,
2665
+ imageCount: 0,
2666
+ };
2667
+ }
2668
+ function compactionLine(report, createdAt = new Date().toISOString(), current = true) {
2669
+ return {
2670
+ kind: "meta",
2671
+ title: "Context compaction",
2672
+ text: formatCompactionReportSummary(report),
2673
+ format: "plain",
2674
+ compaction: { ...report, createdAt, current },
2675
+ };
2676
+ }
2677
+ function compactionReportFromBoundary(message) {
2678
+ if (!message?.metadata?.compactBoundary)
2679
+ return undefined;
2680
+ const stored = message.metadata.compactionReport;
2681
+ if (stored?.continuationState)
2682
+ return { ...stored };
2683
+ const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
2684
+ const continuationState = /<compact_state>\s*([\s\S]*?)\s*<\/compact_state>/i.exec(text)?.[1]?.trim();
2685
+ if (!continuationState)
2686
+ return undefined;
2687
+ const reasonValue = typeof message.metadata.compactionReason === "string" ? message.metadata.compactionReason : "autocompact";
2688
+ const reason = isCompactionReason(reasonValue) ? reasonValue : "autocompact";
2689
+ const imageRegistry = message.metadata.imageRegistry;
2690
+ return {
2691
+ reason,
2692
+ summary: continuationState,
2693
+ continuationState,
2694
+ sourceMessages: 0,
2695
+ preservedUserMessages: 0,
2696
+ newWindowMessages: 1,
2697
+ charsFreed: 0,
2698
+ modelDriven: message.metadata.modelDriven === true,
2699
+ imageCount: Array.isArray(imageRegistry?.images) ? imageRegistry.images.length : 0,
2700
+ };
2701
+ }
2702
+ function isCompactionReason(value) {
2703
+ return ["snip", "microcompact", "autocompact", "reactive_compact", "manualcompact", "purecompact"].includes(value);
2704
+ }
2705
+ function formatCompactionReportSummary(report) {
2706
+ return `${report.newWindowMessages} message(s) in new window, ${report.preservedUserMessages} user message(s) preserved`;
2707
+ }
1988
2708
  function thinkingLine(text, live = false) {
1989
2709
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1990
2710
  }
1991
2711
  function formatToolUse(toolUse) {
1992
- if (toolUse.name === "plan" && isWebPlanPayload(toolUse.input))
1993
- return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), collapsible: true };
1994
- if (toolUse.name === "image2")
1995
- return { kind: "tool", toolName: toolUse.name, toolUseId: toolUse.id, title: toolTitle(toolUse.name, "running"), bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", collapsible: true };
2712
+ const toolPresentation = toolPresentationForSource(toolUse.name);
2713
+ const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
2714
+ if (toolUse.name === "plan_update" && isWebPlanPayload(toolUse.input))
2715
+ return { kind: "tool", toolName: toolUse.name, toolPresentation, title: toolPresentation.label, bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
2716
+ if (toolUse.name === "image_create")
2717
+ return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", presentationLevel: "primary", collapsible: true };
1996
2718
  const summary = summarizeToolUse(toolUse.name, toolUse.input);
1997
- return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
2719
+ 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 };
2720
+ }
2721
+ function formatToolResultLine(toolName, output, ok, presentToolResult) {
2722
+ const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
2723
+ const resources = presentation?.resources ?? toolResources(output);
2724
+ const formatted = resources?.length
2725
+ ? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
2726
+ : formatToolResult(toolName, output, ok);
2727
+ return {
2728
+ kind: ok ? "tool" : "error",
2729
+ toolName,
2730
+ toolPresentation: toolPresentationForSource(toolName),
2731
+ title: presentation?.title ?? toolTitle(toolName, "finished"),
2732
+ bodyTitle: presentation?.bodyTitle ?? formatted.bodyTitle,
2733
+ titleStatus: ok ? "success" : "failure",
2734
+ text: presentation?.text ?? formatted.text,
2735
+ toolDisplay: resources?.length ? undefined : buildToolResultDisplay(toolName, output, ok),
2736
+ format: formatted.format,
2737
+ live: false,
2738
+ previewStyle: formatted.full ? undefined : "summary",
2739
+ summaryMaxLines: formatted.summaryMaxLines,
2740
+ presentationLevel: presentation?.presentationLevel ?? toolPresentationLevel(toolName, output),
2741
+ resources,
2742
+ collapsible: true,
2743
+ };
1998
2744
  }
1999
- function formatToolResultLine(toolName, output, ok) {
2000
- const formatted = formatToolResult(toolName, output, ok);
2001
- return { kind: ok ? "tool" : "error", toolName, title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true };
2745
+ function toolPresentationLevel(toolName, payload) {
2746
+ if (toolName === "image_create")
2747
+ return "primary";
2748
+ if (!isRecord(payload))
2749
+ return "process";
2750
+ const direct = payload.presentationLevel;
2751
+ const ui = isRecord(payload.ui) ? payload.ui.presentationLevel : undefined;
2752
+ const underscoredUi = isRecord(payload._ui) ? payload._ui.presentationLevel : undefined;
2753
+ return [direct, ui, underscoredUi].some((value) => value === "primary") ? "primary" : "process";
2754
+ }
2755
+ function toolUiMetadata(payload) {
2756
+ if (!isRecord(payload))
2757
+ return undefined;
2758
+ const containers = [payload, payload.output, payload.result].filter(isRecord);
2759
+ for (const container of containers) {
2760
+ if (isRecord(container._ui))
2761
+ return container._ui;
2762
+ if (isRecord(container.ui))
2763
+ return container.ui;
2764
+ }
2765
+ return undefined;
2002
2766
  }
2003
- function toolTitle(toolName, _phase) {
2004
- const labels = {
2005
- agent: "子任务",
2006
- edit: "编辑文件",
2007
- exec_command: "执行命令",
2008
- write_stdin: "终端交互",
2009
- expose_downloads: "文件下载",
2010
- grep: "搜索文本",
2011
- image2: "图片生成",
2012
- image_note: "记录图片说明",
2013
- list: "列出文件",
2014
- load_image: "读取图片",
2015
- plan: "任务计划",
2016
- read: "读取文件",
2017
- search: "网络搜索",
2018
- write: "写入文件",
2767
+ function toolResources(payload) {
2768
+ const ui = toolUiMetadata(payload);
2769
+ if (!ui || !Array.isArray(ui.resources))
2770
+ return undefined;
2771
+ return normalizeUiResources(ui.resources);
2772
+ }
2773
+ function normalizeUiResources(values) {
2774
+ if (!Array.isArray(values))
2775
+ return undefined;
2776
+ const resources = values.flatMap((value) => {
2777
+ if (!isRecord(value))
2778
+ return [];
2779
+ const kind = stringValue(value.kind);
2780
+ const url = stringValue(value.url);
2781
+ if (!kind || !url || !safeResourceUrl(url))
2782
+ return [];
2783
+ return [{
2784
+ kind,
2785
+ url,
2786
+ reference: stringValue(value.reference) ?? canonicalResourceReference(url),
2787
+ label: stringValue(value.label),
2788
+ downloadName: stringValue(value.downloadName),
2789
+ sizeBytes: numberValue(value.sizeBytes),
2790
+ expiresAt: numberValue(value.expiresAt),
2791
+ mimeType: stringValue(value.mimeType),
2792
+ height: clampResourceHeight(numberValue(value.height)),
2793
+ }];
2794
+ });
2795
+ return resources.length > 0 ? resources : undefined;
2796
+ }
2797
+ function canonicalResourceReference(url) {
2798
+ return url.startsWith("/") ? `sandbox:${url}` : url;
2799
+ }
2800
+ function clampResourceHeight(value) {
2801
+ return value === undefined ? undefined : Math.max(240, Math.min(1600, Math.round(value)));
2802
+ }
2803
+ function normalizeToolResultPresentation(value) {
2804
+ if (!isRecord(value))
2805
+ return undefined;
2806
+ const level = value.presentationLevel === "primary" || value.presentationLevel === "process" ? value.presentationLevel : undefined;
2807
+ return {
2808
+ title: stringValue(value.title),
2809
+ bodyTitle: stringValue(value.bodyTitle),
2810
+ text: stringValue(value.text),
2811
+ presentationLevel: level,
2812
+ resources: normalizeUiResources(value.resources),
2019
2813
  };
2020
- return labels[toolName] ?? toolName;
2814
+ }
2815
+ function runtimeToolResultPresenter(runtime) {
2816
+ return (toolName, output, ok) => {
2817
+ const support = runtime.pluginSupport;
2818
+ if (!support)
2819
+ return undefined;
2820
+ const sessionId = runtime.engine.snapshot().session?.sessionId;
2821
+ for (const plugin of activeWebRuntimePlugins(support.catalog, support.overrides)) {
2822
+ if (typeof plugin.presentToolResult !== "function")
2823
+ continue;
2824
+ try {
2825
+ const presentation = plugin.presentToolResult({ toolName, output, ok, sessionId });
2826
+ if (presentation !== undefined && presentation !== null)
2827
+ return presentation;
2828
+ }
2829
+ catch (error) {
2830
+ console.warn(`plugin ${plugin.id} failed to present ${toolName}:`, error);
2831
+ }
2832
+ }
2833
+ return undefined;
2834
+ };
2835
+ }
2836
+ function safeResourceUrl(value) {
2837
+ return /^(?:https?:\/\/|\/)/i.test(value) && !/^\/\//.test(value);
2838
+ }
2839
+ function toolTitle(toolName, _phase) {
2840
+ return builtinToolPresentation(toolName)?.label ?? toolName;
2021
2841
  }
2022
2842
  function summarizeToolUse(toolName, input) {
2023
2843
  const purpose = toolUsePurpose(toolName, input);
@@ -2032,65 +2852,304 @@ function toolUsePurpose(toolName, input) {
2032
2852
  const description = typeof input.description === "string" ? input.description.trim() : "";
2033
2853
  if (description)
2034
2854
  return description;
2855
+ const taskId = stringValue(input.task_id);
2856
+ if (toolName === "subagent_output")
2857
+ return taskId ? `读取子任务 ${taskId} 的输出` : "读取子任务输出";
2858
+ if (toolName === "subagent_list")
2859
+ return "查看子任务状态";
2860
+ if (toolName === "subagent_get")
2861
+ return taskId ? `查看子任务 ${taskId}` : "查看子任务";
2862
+ if (toolName === "subagent_stop")
2863
+ return taskId ? `停止子任务 ${taskId}` : "停止子任务";
2864
+ if (toolName === "subagent_resume")
2865
+ return taskId ? `恢复子任务 ${taskId}` : "恢复子任务";
2866
+ if (toolName === "subagent_message")
2867
+ return stringValue(input.target) ? `向 ${stringValue(input.target)} 发送消息` : "发送子任务消息";
2035
2868
  if (toolName === "expose_downloads")
2036
2869
  return "准备网页下载链接";
2037
- if (toolName === "read" && typeof input.path === "string")
2870
+ if (toolName === "file_read" && typeof input.path === "string")
2038
2871
  return `读取 ${path.basename(input.path)}`;
2039
- if (toolName === "list" && typeof input.path === "string")
2872
+ if (toolName === "file_list" && typeof input.path === "string")
2040
2873
  return `查看 ${input.path}`;
2041
- if (toolName === "grep" && typeof input.query === "string")
2874
+ if (toolName === "file_search" && typeof input.query === "string")
2042
2875
  return `搜索 ${input.query}`;
2043
- if (toolName === "write" && typeof input.path === "string")
2876
+ if (toolName === "file_write" && typeof input.path === "string")
2044
2877
  return `写入 ${path.basename(input.path)}`;
2045
- if (toolName === "edit" && typeof input.path === "string")
2878
+ if (toolName === "file_edit" && typeof input.path === "string")
2046
2879
  return `修改 ${path.basename(input.path)}`;
2047
2880
  return undefined;
2048
2881
  }
2882
+ function mergePresentationLevel(result, started) {
2883
+ return result === "primary" || started === "primary" ? "primary" : result ?? started;
2884
+ }
2885
+ function mergeToolDisplays(result, started) {
2886
+ if (!result)
2887
+ return started;
2888
+ if (!started)
2889
+ return result;
2890
+ const facts = [...started.facts];
2891
+ for (const fact of result.facts) {
2892
+ const existing = facts.findIndex((item) => item.label === fact.label);
2893
+ if (existing >= 0)
2894
+ facts[existing] = fact;
2895
+ else
2896
+ facts.push(fact);
2897
+ }
2898
+ const subject = result.subject || started.subject;
2899
+ const purpose = result.purpose || started.purpose;
2900
+ return normalizeToolDisplay({
2901
+ subject,
2902
+ purpose,
2903
+ facts,
2904
+ previews: result.previews.length > 0 ? result.previews : started.previews,
2905
+ });
2906
+ }
2907
+ function normalizeToolDisplay(display) {
2908
+ let subject = display.subject?.trim() || undefined;
2909
+ const purpose = display.purpose?.trim() || undefined;
2910
+ if (subject && purpose && normalizeDisplayText(subject) === normalizeDisplayText(purpose))
2911
+ subject = undefined;
2912
+ const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
2913
+ const facts = display.facts.filter((fact) => {
2914
+ const normalized = normalizeDisplayText(fact.value);
2915
+ if (!normalized || visibleText.has(normalized))
2916
+ return false;
2917
+ visibleText.add(normalized);
2918
+ return true;
2919
+ });
2920
+ const previews = display.previews.filter((preview) => {
2921
+ const normalized = normalizeDisplayText(preview.content);
2922
+ if (!normalized || visibleText.has(normalized))
2923
+ return false;
2924
+ visibleText.add(normalized);
2925
+ return true;
2926
+ });
2927
+ return { subject, purpose, facts, previews };
2928
+ }
2929
+ function normalizeDisplayText(value) {
2930
+ return value.replace(/\s+/gu, " ").trim().toLowerCase();
2931
+ }
2932
+ function buildToolUseDisplay(toolName, input) {
2933
+ const data = isRecord(input) ? input : {};
2934
+ const purpose = toolUsePurpose(toolName, input);
2935
+ const subject = toolName === "terminal_run" ? undefined : toolDisplaySubject(toolName, data);
2936
+ const facts = [];
2937
+ const previews = [];
2938
+ if (toolName === "terminal_run")
2939
+ pushToolPreview(previews, "命令", "code", data.cmd);
2940
+ else if (toolName === "terminal_control") {
2941
+ pushToolFact(facts, "会话", data.session_id, true);
2942
+ pushToolFact(facts, "输入", data.chars, true);
2943
+ }
2944
+ else if (toolName === "file_search")
2945
+ pushToolFact(facts, "范围", data.path, true);
2946
+ else if (toolName === "image_inspect")
2947
+ pushToolFact(facts, "图片", arrayLabel(data.imageRefs), true);
2948
+ else if (toolName === "image_create") {
2949
+ pushToolFact(facts, "模式", data.mode === "edit" ? "编辑" : "生成");
2950
+ pushToolFact(facts, "名称", data.semanticName);
2951
+ }
2952
+ else if (toolName === "expose_downloads")
2953
+ pushToolFact(facts, "文件", arrayLabel(data.paths), true);
2954
+ else if (toolName === "subagent_message")
2955
+ pushToolFact(facts, "接收方", data.target);
2956
+ else if (toolName.startsWith("subagent_"))
2957
+ pushToolFact(facts, "任务", data.task_id, true);
2958
+ else if (toolName.startsWith("secret_")) {
2959
+ pushToolFact(facts, "密钥", data.key, true);
2960
+ pushToolFact(facts, "用途", data.reason);
2961
+ }
2962
+ else if (toolName === "subagent_run") {
2963
+ pushToolFact(facts, "任务", data.description ?? data.prompt);
2964
+ pushToolFact(facts, "类型", data.subagent_type ?? data.mode);
2965
+ }
2966
+ return normalizeToolDisplay({ subject, purpose, facts, previews });
2967
+ }
2968
+ function buildToolResultDisplay(toolName, output, ok) {
2969
+ const data = isRecord(output) ? output : {};
2970
+ const facts = [];
2971
+ const previews = [];
2972
+ let subject = toolDisplaySubject(toolName, data);
2973
+ let purpose = stringValue(data.description);
2974
+ if ((toolName === "file_edit" || toolName === "file_write") && isEditToolOutput(data)) {
2975
+ subject = data.path;
2976
+ pushToolPreview(previews, undefined, "diff", compactEditDiff(data));
2977
+ }
2978
+ else if (isExecOutput(data)) {
2979
+ subject = undefined;
2980
+ purpose = stringValue(data.description);
2981
+ pushToolPreview(previews, "命令", "code", data.command);
2982
+ const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
2983
+ if (outputText)
2984
+ pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
2985
+ if (data.timed_out && !outputText)
2986
+ pushToolFact(facts, "状态", "命令执行超时", false, "warning");
2987
+ else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
2988
+ pushToolFact(facts, "状态", `退出码 ${data.exit_code ?? "未知"}`, false, "danger");
2989
+ }
2990
+ else if (toolName === "file_search") {
2991
+ subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
2992
+ pushToolPreview(previews, undefined, "list", grepPreview(data));
2993
+ }
2994
+ else if (toolName === "web_search") {
2995
+ subject = stringValue(data.query);
2996
+ pushToolPreview(previews, undefined, "list", searchPreview(data));
2997
+ }
2998
+ else if (toolName === "file_read") {
2999
+ subject = stringValue(data.path);
3000
+ if (numberValue(data.startLine) !== undefined && numberValue(data.endLine) !== undefined)
3001
+ pushToolFact(facts, "范围", `${data.startLine}–${data.endLine} 行`);
3002
+ pushToolPreview(previews, undefined, "code", data.content);
3003
+ }
3004
+ else if (toolName === "file_list") {
3005
+ subject = stringValue(data.path);
3006
+ pushToolPreview(previews, undefined, "list", listPreview(data));
3007
+ }
3008
+ else if (toolName === "image_create") {
3009
+ subject = stringValue(data.semanticName);
3010
+ pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
3011
+ pushToolFact(facts, "尺寸", data.size);
3012
+ }
3013
+ else if (toolName === "image_inspect") {
3014
+ subject = arrayLabel(data.imageRefs);
3015
+ }
3016
+ else if (toolResources(output)?.length) {
3017
+ subject = toolResources(output)?.map((item) => item.label || item.downloadName).filter(Boolean).join("、");
3018
+ }
3019
+ else if (toolName.startsWith("subagent_") || toolName === "subagent_run") {
3020
+ subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
3021
+ }
3022
+ else if (toolName === "subagent_message") {
3023
+ subject = stringValue(data.target);
3024
+ }
3025
+ else if (toolName.startsWith("secret_")) {
3026
+ subject = stringValue(data.key);
3027
+ pushToolFact(facts, "密钥", data.key, true);
3028
+ if (toolName === "secret_list")
3029
+ pushToolFact(facts, "数量", `${arrayLength(data.secrets)} 项`);
3030
+ }
3031
+ else if (toolName === "subagent_report") {
3032
+ }
3033
+ else {
3034
+ subject = subject || stringValue(data.path) || stringValue(data.id) || stringValue(data.name);
3035
+ }
3036
+ return normalizeToolDisplay({ subject, purpose, facts, previews });
3037
+ }
3038
+ function toolDisplaySubject(toolName, data) {
3039
+ if (["file_read", "file_write", "file_edit", "file_list"].includes(toolName))
3040
+ return stringValue(data.path);
3041
+ if (toolName === "terminal_run")
3042
+ return stringValue(data.cmd) || stringValue(data.command);
3043
+ if (toolName === "file_search" || toolName === "web_search")
3044
+ return stringValue(data.query);
3045
+ if (toolName === "image_create")
3046
+ return stringValue(data.semanticName);
3047
+ if (toolName === "subagent_message")
3048
+ return stringValue(data.target);
3049
+ if (toolName.startsWith("subagent_"))
3050
+ return stringValue(data.task_id);
3051
+ return undefined;
3052
+ }
3053
+ function pushToolPreview(previews, label, kind, value) {
3054
+ if (typeof value !== "string" || !value.trim())
3055
+ return;
3056
+ const content = value.replace(/\r\n/g, "\n").trim();
3057
+ previews.push({ label, kind, content: truncate(content, 1400) });
3058
+ }
3059
+ function compactEditDiff(output) {
3060
+ const lines = [];
3061
+ for (const hunk of output.patch.slice(0, 3)) {
3062
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
3063
+ lines.push(...hunk.lines.slice(0, 14));
3064
+ }
3065
+ return lines.join("\n");
3066
+ }
3067
+ function grepPreview(data) {
3068
+ if (!Array.isArray(data.matches))
3069
+ return "";
3070
+ return data.matches.slice(0, 5).filter(isRecord).map((match) => {
3071
+ const file = stringValue(match.file) || "";
3072
+ const line = numberValue(match.line);
3073
+ const text = stringValue(match.text) || "";
3074
+ return `${file}${line !== undefined ? `:${line}` : ""} ${text}`.trim();
3075
+ }).filter(Boolean).join("\n");
3076
+ }
3077
+ function searchPreview(data) {
3078
+ if (!Array.isArray(data.results))
3079
+ return "";
3080
+ return data.results.slice(0, 4).filter(isRecord).map((result) => {
3081
+ const title = stringValue(result.title) || stringValue(result.url) || "";
3082
+ return title;
3083
+ }).filter(Boolean).join("\n");
3084
+ }
3085
+ function listPreview(data) {
3086
+ if (!Array.isArray(data.entries))
3087
+ return "";
3088
+ return data.entries.slice(0, 8).filter(isRecord).map((entry) => stringValue(entry.path) || stringValue(entry.name) || "").filter(Boolean).join("\n");
3089
+ }
3090
+ function pushToolFact(facts, label, value, code = false, tone = "neutral") {
3091
+ const normalized = stringValue(value);
3092
+ if (!normalized || facts.some((fact) => fact.label === label && fact.value === normalized))
3093
+ return;
3094
+ facts.push({ label, value: truncate(normalized, 220), code, tone });
3095
+ }
3096
+ function stringValue(value) {
3097
+ if (typeof value === "string" && value.trim())
3098
+ return value.replace(/\s+/gu, " ").trim();
3099
+ if (typeof value === "number" || typeof value === "boolean")
3100
+ return String(value);
3101
+ return undefined;
3102
+ }
3103
+ function numberValue(value) {
3104
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3105
+ }
3106
+ function arrayLength(value) {
3107
+ return Array.isArray(value) ? value.length : 0;
3108
+ }
3109
+ function arrayLabel(value) {
3110
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0).join("、") : undefined;
3111
+ }
3112
+ function editOperationLabel(operation) {
3113
+ if (operation === "create")
3114
+ return "新建";
3115
+ if (operation === "edit")
3116
+ return "修改";
3117
+ if (operation === "write")
3118
+ return "写入";
3119
+ return operation;
3120
+ }
3121
+ function formatDuration(durationMs) {
3122
+ return durationMs >= 1000 ? `${(durationMs / 1000).toFixed(durationMs >= 10000 ? 0 : 1)} 秒` : `${durationMs} 毫秒`;
3123
+ }
3124
+ function taskStatusDisplay(value, ok) {
3125
+ const labels = { pending: "排队中", queued: "排队中", running: "运行中", completed: "已完成", failed: "失败", killed: "已停止", stopped: "已停止" };
3126
+ const status = stringValue(value);
3127
+ return status ? labels[status] ?? status : ok ? "已完成" : "失败";
3128
+ }
2049
3129
  function formatToolResult(toolName, output, ok) {
2050
- if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
3130
+ if ((toolName === "file_edit" || toolName === "file_write") && isRecord(output) && isEditToolOutput(output))
2051
3131
  return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
2052
3132
  if (isExecOutput(output))
2053
3133
  return { text: formatExecToolResult(output, ok), format: "plain", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2054
- if (toolName === "list" && isRecord(output))
3134
+ if (toolName === "file_list" && isRecord(output))
2055
3135
  return { text: formatListToolResult(output, ok) };
2056
- if (toolName === "read" && isRecord(output))
3136
+ if (toolName === "file_read" && isRecord(output))
2057
3137
  return { text: formatReadToolResult(output, ok) };
2058
- if (toolName === "grep" && isRecord(output))
3138
+ if (toolName === "file_search" && isRecord(output))
2059
3139
  return { text: formatGrepToolResult(output, ok) };
2060
- if (toolName === "search" && isRecord(output))
3140
+ if (toolName === "web_search" && isRecord(output))
2061
3141
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2062
- if (toolName === "image2" && isRecord(output))
3142
+ if (toolName === "image_create" && isRecord(output))
2063
3143
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
2064
- if (toolName === "expose_downloads")
2065
- return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
2066
- if (toolName === "plan" && isWebPlanPayload(output))
3144
+ const resources = toolResources(output);
3145
+ if (resources?.length)
3146
+ return { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" };
3147
+ if (toolName === "plan_update" && isWebPlanPayload(output))
2067
3148
  return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
2068
3149
  if (typeof output === "string")
2069
3150
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2070
3151
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2071
3152
  }
2072
- function formatExposeDownloadsToolResult(output, ok) {
2073
- const downloads = extractDownloadEntries(output);
2074
- if (!ok)
2075
- return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
2076
- if (!downloads.length)
2077
- return "文件已准备好,请点击下载。";
2078
- return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
2079
- }
2080
- function formatDownloadEntries(downloads) {
2081
- return downloads.map((entry) => {
2082
- const filename = entry.filename || entry.name || "下载文件";
2083
- const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
2084
- return url ? `- ${filename}: ${url}` : `- ${filename}`;
2085
- }).join("\n");
2086
- }
2087
- function extractDownloadEntries(output) {
2088
- if (!isRecord(output))
2089
- return [];
2090
- const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
2091
- const downloads = candidates.find(Array.isArray);
2092
- return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
2093
- }
2094
3153
  function isEditToolOutput(value) {
2095
3154
  return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
2096
3155
  }
@@ -2395,8 +3454,9 @@ function formatUsageTotals(totals) {
2395
3454
  }
2396
3455
  function formatManualCompaction(result) {
2397
3456
  if (!result.changed)
2398
- return "No context compaction was needed.";
2399
- return `context compacted: ${result.messages.length} message(s) retained, ${formatNumber(result.charsFreed ?? result.tokensFreed ?? 0)} chars removed`;
3457
+ return "No conversation context available to compact.";
3458
+ const preservedUsers = result.report?.preservedUserMessages ?? result.messages.filter((message) => message.metadata?.compactPreservedUser === true).length;
3459
+ return `context compacted: ${result.messages.length} message(s) in new window, ${preservedUsers} user message(s) preserved`;
2400
3460
  }
2401
3461
  function formatPureCompaction(result) {
2402
3462
  if (!result.changed)
@@ -2406,6 +3466,8 @@ function formatPureCompaction(result) {
2406
3466
  function formatNumber(value) {
2407
3467
  return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
2408
3468
  }
3469
+ const TERMINAL_OUTPUT_FLUSH_MS = 100;
3470
+ const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
2409
3471
  const THINKING_SUMMARY_MAX_LINES = 1000;
2410
3472
  const EXPANDED_SUMMARY_MAX_LINES = 1000;
2411
3473
  const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;