neoctl 0.2.30 → 0.2.31

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 (97) hide show
  1. package/README.md +37 -3
  2. package/dist/context/compaction.d.ts +20 -4
  3. package/dist/context/compaction.js +377 -186
  4. package/dist/context/compaction.js.map +1 -1
  5. package/dist/context/context-manager.d.ts +7 -1
  6. package/dist/context/context-manager.js +15 -11
  7. package/dist/context/context-manager.js.map +1 -1
  8. package/dist/context/prompts.js +2 -0
  9. package/dist/context/prompts.js.map +1 -1
  10. package/dist/context/smoke-context.js +266 -17
  11. package/dist/context/smoke-context.js.map +1 -1
  12. package/dist/core/assistant-output-filter.d.ts +1 -0
  13. package/dist/core/assistant-output-filter.js +54 -2
  14. package/dist/core/assistant-output-filter.js.map +1 -1
  15. package/dist/core/context-metrics.d.ts +1 -0
  16. package/dist/core/context-metrics.js +18 -12
  17. package/dist/core/context-metrics.js.map +1 -1
  18. package/dist/core/image-registry.d.ts +16 -3
  19. package/dist/core/image-registry.js +65 -30
  20. package/dist/core/image-registry.js.map +1 -1
  21. package/dist/core/image-storage.d.ts +15 -1
  22. package/dist/core/image-storage.js +140 -17
  23. package/dist/core/image-storage.js.map +1 -1
  24. package/dist/core/message-pipeline.js +18 -4
  25. package/dist/core/message-pipeline.js.map +1 -1
  26. package/dist/core/query-engine.d.ts +12 -2
  27. package/dist/core/query-engine.js +57 -8
  28. package/dist/core/query-engine.js.map +1 -1
  29. package/dist/core/query.d.ts +13 -2
  30. package/dist/core/query.js +56 -21
  31. package/dist/core/query.js.map +1 -1
  32. package/dist/core/smoke-core-loop.js +68 -3
  33. package/dist/core/smoke-core-loop.js.map +1 -1
  34. package/dist/core/smoke-image-integrity.d.ts +1 -0
  35. package/dist/core/smoke-image-integrity.js +67 -0
  36. package/dist/core/smoke-image-integrity.js.map +1 -0
  37. package/dist/core/state.d.ts +1 -1
  38. package/dist/core/state.js.map +1 -1
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/model/model-metadata.json +4 -4
  43. package/dist/plugins/plugin-system.d.ts +61 -0
  44. package/dist/plugins/plugin-system.js +189 -0
  45. package/dist/plugins/plugin-system.js.map +1 -0
  46. package/dist/plugins/smoke-plugin-system.d.ts +1 -0
  47. package/dist/plugins/smoke-plugin-system.js +63 -0
  48. package/dist/plugins/smoke-plugin-system.js.map +1 -0
  49. package/dist/repl/index.js +8 -3
  50. package/dist/repl/index.js.map +1 -1
  51. package/dist/session/session-export.d.ts +2 -0
  52. package/dist/session/session-export.js +10 -0
  53. package/dist/session/session-export.js.map +1 -1
  54. package/dist/session/session-store.d.ts +41 -0
  55. package/dist/session/session-store.js +292 -59
  56. package/dist/session/session-store.js.map +1 -1
  57. package/dist/session/smoke-session.js +127 -7
  58. package/dist/session/smoke-session.js.map +1 -1
  59. package/dist/tools/builtins/exec-process-manager.d.ts +9 -0
  60. package/dist/tools/builtins/exec-process-manager.js +45 -4
  61. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  62. package/dist/tools/builtins/exec-tool.js +10 -2
  63. package/dist/tools/builtins/exec-tool.js.map +1 -1
  64. package/dist/tools/builtins/image-generation-tool.js +32 -42
  65. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  66. package/dist/tools/builtins/image-loader-tool.js +10 -4
  67. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  68. package/dist/tools/builtins/image-note-tool.js +8 -3
  69. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  70. package/dist/tools/registry.d.ts +8 -1
  71. package/dist/tools/registry.js +37 -13
  72. package/dist/tools/registry.js.map +1 -1
  73. package/dist/tools/smoke-exec-process.js +12 -0
  74. package/dist/tools/smoke-exec-process.js.map +1 -1
  75. package/dist/tools/smoke-tool-system.js +24 -6
  76. package/dist/tools/smoke-tool-system.js.map +1 -1
  77. package/dist/types/events.d.ts +5 -1
  78. package/dist/types/messages.d.ts +13 -5
  79. package/dist/types/messages.js +4 -0
  80. package/dist/types/messages.js.map +1 -1
  81. package/dist/ui/display-message.d.ts +10 -2
  82. package/dist/ui/display-message.js +18 -9
  83. package/dist/ui/display-message.js.map +1 -1
  84. package/dist/web/index.d.ts +159 -9
  85. package/dist/web/index.js +967 -106
  86. package/dist/web/index.js.map +1 -1
  87. package/dist/web/runtime-context-protocol.d.ts +12 -1
  88. package/dist/web/runtime-context-protocol.js +16 -1
  89. package/dist/web/runtime-context-protocol.js.map +1 -1
  90. package/dist/web/smoke-runtime-context-protocol.js +3 -0
  91. package/dist/web/smoke-runtime-context-protocol.js.map +1 -1
  92. package/dist/web/smoke-web-history.d.ts +1 -0
  93. package/dist/web/smoke-web-history.js +121 -0
  94. package/dist/web/smoke-web-history.js.map +1 -0
  95. package/dist/web/smoke-web-terminal-tasks.js +34 -3
  96. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  97. package/package.json +4 -1
package/dist/web/index.js CHANGED
@@ -29,12 +29,15 @@ import { TaskStore } from "../tasks/task-store.js";
29
29
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
30
30
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
31
  import { DefaultContextManager } from "../context/context-manager.js";
32
+ import { createImageId } from "../types/messages.js";
32
33
  import { WEB_HTML } from "./html.js";
33
34
  import { openDirectory } from "../open-directory.js";
34
- import { resolveImageBlockDataSync } from "../core/image-storage.js";
35
+ import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
35
36
  import { isWebPlanPayload, serializeWebPlanPayload, webPlanBodyTitle } from "./plan-payload.js";
36
37
  import { createWebRuntimeContextPayload } from "./runtime-context-protocol.js";
37
38
  const require = createRequire(import.meta.url);
39
+ const corePackage = require("../../package.json");
40
+ const CORE_VERSION = String(corePackage.version || "unknown");
38
41
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
39
42
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
40
43
  const markedAssetPath = path.join(markedPackageDir, "lib", "marked.esm.js");
@@ -127,26 +130,42 @@ export async function createWebRuntime(options = {}) {
127
130
  const pluginOverrides = normalizePluginOverrides(options.sessionPluginOverrides, pluginCatalog);
128
131
  const activePlugins = activeWebRuntimePlugins(pluginCatalog, pluginOverrides);
129
132
  const tools = new ToolRegistry();
133
+ const toolCatalog = [];
134
+ const catalogedToolNames = new Set();
135
+ const catalogTool = (tool, item) => {
136
+ if (catalogedToolNames.has(tool.name))
137
+ return;
138
+ catalogedToolNames.add(tool.name);
139
+ toolCatalog.push({ name: tool.name, ...item });
140
+ };
141
+ const registerTool = (tool, item) => {
142
+ tools.register(tool);
143
+ catalogTool(tool, item);
144
+ };
130
145
  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());
146
+ registerTool(editTool, { source: "builtin" });
147
+ registerTool(writeTool, { source: "builtin" });
148
+ registerTool(createExecTool({ processManager: execProcessManager }), { source: "builtin" });
149
+ registerTool(createWriteStdinTool(execProcessManager), { source: "builtin" });
150
+ registerTool(listDirectoryTool, { source: "builtin" });
151
+ registerTool(readFileTool, { source: "builtin" });
152
+ registerTool(grepTool, { source: "builtin" });
153
+ registerTool(searchTool, { source: "builtin" });
154
+ registerTool(createLoadImageTool(), { source: "builtin" });
140
155
  if (modelConfig?.provider === "openai")
141
- tools.register(createOpenAIImageGenerationTool());
142
- tools.register(planTool);
156
+ registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
157
+ registerTool(planTool, { source: "builtin" });
143
158
  for (const tool of options.externalTools ?? [])
144
- tools.register(tool);
159
+ registerTool(tool, { source: "external" });
160
+ for (const plugin of pluginCatalog) {
161
+ for (const tool of plugin.tools)
162
+ catalogTool(tool, { source: "plugin", pluginId: plugin.id, pluginName: plugin.name });
163
+ }
145
164
  for (const plugin of activePlugins)
146
165
  for (const tool of plugin.tools)
147
166
  tools.register(tool);
148
167
  const agentRuntime = { modelGateway, tools, taskStore };
149
- tools.register(createAgentTool(agentRuntime));
168
+ registerTool(createAgentTool(agentRuntime), { source: "builtin" });
150
169
  const resumeHandler = async (taskId, directive) => {
151
170
  const dummyContext = {
152
171
  agentId: "main",
@@ -157,7 +176,10 @@ export async function createWebRuntime(options = {}) {
157
176
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
158
177
  };
159
178
  for (const tool of createTaskTools(taskStore, resumeHandler))
160
- tools.register(tool);
179
+ registerTool(tool, { source: "builtin" });
180
+ const globalToolOverrides = normalizeToolOverrides(options.globalToolOverrides, toolCatalog);
181
+ const sessionToolOverrides = normalizeToolOverrides(options.sessionToolOverrides, toolCatalog);
182
+ applyToolOverrides(tools, toolCatalog, globalToolOverrides, sessionToolOverrides);
161
183
  const appPromptStore = new InMemoryAppPromptStore();
162
184
  const engine = new QueryEngine({
163
185
  agentId: options.agentId ?? "main",
@@ -176,6 +198,8 @@ export async function createWebRuntime(options = {}) {
176
198
  taskNotificationSource: createTaskNotificationSource(taskStore),
177
199
  commands: replCommandDefinitions.map((command) => command.usage),
178
200
  plugins: [...(options.plugins ?? []), ...activePlugins.map((plugin) => plugin.id)],
201
+ cwdTransitionPaths: options.cwdTransitionPaths,
202
+ onCwdTransitionConsumed: options.onCwdTransitionConsumed,
179
203
  session: {
180
204
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
181
205
  sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
@@ -208,6 +232,14 @@ export async function createWebRuntime(options = {}) {
208
232
  persist: options.persistSessionPluginOverrides,
209
233
  resolve: options.resolveSessionPluginOverrides,
210
234
  } : undefined,
235
+ toolSupport: {
236
+ catalog: toolCatalog,
237
+ globalOverrides: globalToolOverrides,
238
+ sessionOverrides: sessionToolOverrides,
239
+ persistGlobal: options.persistGlobalToolOverrides,
240
+ persistSession: options.persistSessionToolOverrides,
241
+ resolveSession: options.resolveSessionToolOverrides,
242
+ },
211
243
  };
212
244
  }
213
245
  function normalizeWebRuntimePlugins(value) {
@@ -233,6 +265,14 @@ function normalizePluginOverrides(value, catalog) {
233
265
  function activeWebRuntimePlugins(catalog, overrides) {
234
266
  return catalog.filter((plugin) => plugin.globallyEnabled && overrides[plugin.id] !== false);
235
267
  }
268
+ function normalizeToolOverrides(value, catalog) {
269
+ const available = new Set(catalog.map((tool) => tool.name));
270
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([name, enabled]) => available.has(name) && typeof enabled === "boolean"));
271
+ }
272
+ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides) {
273
+ for (const tool of catalog)
274
+ registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
275
+ }
236
276
  function createTaskNotificationSource(taskStore) {
237
277
  return {
238
278
  collectUnnotifiedCompletions() {
@@ -266,6 +306,8 @@ export class WebRuntimeRouter {
266
306
  repls = new Map();
267
307
  createRuntime;
268
308
  createRepl;
309
+ clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
310
+ coreVersion = process.env.NEO_CORE_VERSION?.trim() || CORE_VERSION;
269
311
  constructor(options = {}) {
270
312
  this.createRuntime = options.createRuntime ?? createWebRuntime;
271
313
  this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
@@ -286,6 +328,24 @@ export class WebRuntimeRouter {
286
328
  activeScopes() {
287
329
  return [...this.repls.keys()];
288
330
  }
331
+ clientInfo() {
332
+ return {
333
+ protocolVersion: 1,
334
+ revision: this.clientRevision,
335
+ coreVersion: this.coreVersion,
336
+ };
337
+ }
338
+ async requestClientReload(options = {}) {
339
+ const payload = {
340
+ ...this.clientInfo(),
341
+ requestId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
342
+ clearCache: options.clearCache !== false,
343
+ reason: options.reason?.trim() || "deployment",
344
+ };
345
+ const repls = await Promise.all([...this.repls.values()]);
346
+ const clients = repls.reduce((total, repl) => total + repl.requestClientReload(payload), 0);
347
+ return { ...payload, clients };
348
+ }
289
349
  }
290
350
  export async function createWebRuntimeRouter(options = {}) {
291
351
  const router = new WebRuntimeRouter(options);
@@ -308,6 +368,8 @@ export class WebRepl {
308
368
  pendingDeltaOperations = [];
309
369
  pendingDeltaStatus = false;
310
370
  pendingDeltaTimer;
371
+ pendingTerminalOutput = new Map();
372
+ terminalOutputTimer;
311
373
  lineId = 0;
312
374
  assistantLineId;
313
375
  thinkingLineId;
@@ -322,29 +384,29 @@ export class WebRepl {
322
384
  queuedAttachments;
323
385
  foregroundRun;
324
386
  foregroundRunToken = 0;
325
- pendingUserImageEchoLabels;
387
+ pendingUserImageEchoIds;
326
388
  pendingUserImageEchoMessageId;
327
389
  runtimeContextRevision = 0;
328
390
  runtimeContextPublishQueue = Promise.resolve();
329
391
  backgroundSessionRuns = new Map();
330
392
  suppressReattachedStreaming = new Set();
331
- backgroundTaskCount;
332
393
  constructor(runtime) {
333
394
  this.runtime = runtime;
334
- this.lines = initialLines(runtime, { current: 0 });
395
+ const lineId = { current: 0 };
396
+ this.lines = initialLines(runtime, lineId);
397
+ this.lineId = lineId.current;
335
398
  this.status = initialStatus(runtime);
336
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
337
399
  runtime.taskStore.subscribe(() => {
338
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
339
400
  this.broadcastSync();
340
401
  });
341
402
  runtime.execProcessManager.subscribe(() => {
342
- this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
403
+ this.dropInactiveTerminalOutput();
343
404
  this.broadcastSync();
344
405
  });
406
+ runtime.execProcessManager.subscribeOutput((delta) => this.queueTerminalOutput(delta));
345
407
  runtime.engine.onSessionTitleChange(() => this.broadcastSync());
346
408
  }
347
- subscribe(res) {
409
+ subscribe(res, clientInfo) {
348
410
  const subscriber = { response: res, blocked: false, needsSync: false, needsRuntimeContext: false };
349
411
  this.subscribers.add(subscriber);
350
412
  res.writeHead(200, {
@@ -353,19 +415,31 @@ export class WebRepl {
353
415
  Connection: "keep-alive",
354
416
  "X-Accel-Buffering": "no",
355
417
  });
418
+ if (clientInfo)
419
+ this.send(subscriber, "client.version", clientInfo);
356
420
  this.send(subscriber, "sync", this.snapshot(true));
357
421
  this.publishRuntimeContext([subscriber]);
358
422
  reqKeepAlive(res);
359
423
  res.on("close", () => this.subscribers.delete(subscriber));
360
424
  }
425
+ get backgroundTaskCount() {
426
+ return this.backgroundTasks().length;
427
+ }
428
+ requestClientReload(payload) {
429
+ const subscribers = [...this.subscribers].filter((subscriber) => !subscriber.response.destroyed);
430
+ for (const subscriber of subscribers)
431
+ this.send(subscriber, "client.reload", payload);
432
+ return subscribers.length;
433
+ }
361
434
  snapshot(includeCatalog = false) {
435
+ const backgroundTasks = this.backgroundTasks();
362
436
  return {
363
437
  lines: this.lines,
364
438
  status: this.status,
365
439
  busy: this.busy,
366
440
  queuedInput: this.queuedInput,
367
- backgroundTaskCount: this.backgroundTaskCount,
368
- backgroundTasks: this.backgroundTasks(),
441
+ backgroundTaskCount: backgroundTasks.length,
442
+ backgroundTasks,
369
443
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
370
444
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
371
445
  session: this.runtime.engine.snapshot().session,
@@ -401,6 +475,91 @@ export class WebRepl {
401
475
  }),
402
476
  };
403
477
  }
478
+ globalTools() {
479
+ const support = this.runtime.toolSupport;
480
+ if (!support)
481
+ return { items: [] };
482
+ return {
483
+ items: support.catalog.map((tool) => ({
484
+ ...tool,
485
+ configuredEnabled: support.globalOverrides[tool.name] ?? true,
486
+ })),
487
+ };
488
+ }
489
+ sessionTools() {
490
+ const support = this.runtime.toolSupport;
491
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
492
+ if (!support)
493
+ return { sessionId, busy: this.busy, items: [] };
494
+ const activePluginIds = new Set(this.runtime.pluginSupport
495
+ ? activeWebRuntimePlugins(this.runtime.pluginSupport.catalog, this.runtime.pluginSupport.overrides).map((plugin) => plugin.id)
496
+ : []);
497
+ return {
498
+ sessionId,
499
+ busy: this.busy,
500
+ items: support.catalog.map((tool) => {
501
+ const override = support.sessionOverrides[tool.name];
502
+ const globallyEnabled = support.globalOverrides[tool.name] ?? true;
503
+ const available = tool.source !== "plugin" || Boolean(tool.pluginId && activePluginIds.has(tool.pluginId));
504
+ return {
505
+ ...tool,
506
+ available,
507
+ globallyEnabled,
508
+ mode: override === undefined ? "inherit" : override ? "enabled" : "disabled",
509
+ effectiveEnabled: available && (override ?? globallyEnabled),
510
+ };
511
+ }),
512
+ };
513
+ }
514
+ async setGlobalTools(value) {
515
+ if (this.busy)
516
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change global tools while a response is running");
517
+ if (!isRecord(value))
518
+ return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
519
+ const support = this.runtime.toolSupport;
520
+ if (!support)
521
+ return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
522
+ const normalized = this.parseToolOverrides(value, support);
523
+ if (!normalized.ok)
524
+ return normalized;
525
+ try {
526
+ support.globalOverrides = normalized.value;
527
+ this.syncPluginToolRegistration();
528
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
529
+ if (support.persistGlobal)
530
+ await support.persistGlobal(support.globalOverrides);
531
+ await this.refreshToolConfiguration();
532
+ return { ok: true, state: this.globalTools() };
533
+ }
534
+ catch (error) {
535
+ return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
536
+ }
537
+ }
538
+ async setSessionTools(value) {
539
+ if (this.busy)
540
+ return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
541
+ if (!isRecord(value))
542
+ return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
543
+ const support = this.runtime.toolSupport;
544
+ if (!support)
545
+ return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
546
+ const normalized = this.parseToolModes(value, support);
547
+ if (!normalized.ok)
548
+ return normalized;
549
+ try {
550
+ await this.applySessionToolOverrides(normalized.value, true);
551
+ return { ok: true, state: this.sessionTools() };
552
+ }
553
+ catch (error) {
554
+ return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
555
+ }
556
+ }
557
+ async loadSessionTools(sessionId) {
558
+ const support = this.runtime.toolSupport;
559
+ if (!support?.resolveSession || !sessionId)
560
+ return;
561
+ await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
562
+ }
404
563
  async setSessionPlugins(value) {
405
564
  if (this.busy)
406
565
  return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
@@ -435,6 +594,64 @@ export class WebRepl {
435
594
  return;
436
595
  await this.applySessionPluginOverrides(await support.resolve(sessionId), false);
437
596
  }
597
+ parseToolOverrides(value, support) {
598
+ const available = new Set(support.catalog.map((tool) => tool.name));
599
+ const overrides = {};
600
+ for (const [name, enabled] of Object.entries(value)) {
601
+ if (!available.has(name))
602
+ return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
603
+ if (typeof enabled !== "boolean")
604
+ return actionFailure("TOOL_INVALID", `invalid tool state: ${name}`);
605
+ overrides[name] = enabled;
606
+ }
607
+ return { ok: true, value: overrides };
608
+ }
609
+ parseToolModes(value, support) {
610
+ const available = new Set(support.catalog.map((tool) => tool.name));
611
+ const overrides = {};
612
+ for (const [name, mode] of Object.entries(value)) {
613
+ if (!available.has(name))
614
+ return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
615
+ if (mode === "enabled" || mode === true)
616
+ overrides[name] = true;
617
+ else if (mode === "disabled" || mode === false)
618
+ overrides[name] = false;
619
+ else if (mode !== "inherit" && mode !== null && mode !== undefined)
620
+ return actionFailure("TOOL_INVALID", `invalid tool mode: ${name}`);
621
+ }
622
+ return { ok: true, value: overrides };
623
+ }
624
+ async applySessionToolOverrides(overrides, persist) {
625
+ const support = this.runtime.toolSupport;
626
+ if (!support)
627
+ return;
628
+ support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
629
+ this.syncPluginToolRegistration();
630
+ applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
631
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
632
+ if (persist && sessionId && support.persistSession)
633
+ await support.persistSession(sessionId, support.sessionOverrides);
634
+ await this.refreshToolConfiguration();
635
+ }
636
+ syncPluginToolRegistration() {
637
+ const pluginSupport = this.runtime.pluginSupport;
638
+ if (!pluginSupport)
639
+ return;
640
+ for (const plugin of pluginSupport.catalog)
641
+ for (const tool of plugin.tools)
642
+ this.runtime.tools.unregister(tool.name);
643
+ for (const plugin of activeWebRuntimePlugins(pluginSupport.catalog, pluginSupport.overrides)) {
644
+ for (const tool of plugin.tools)
645
+ this.runtime.tools.register(tool);
646
+ }
647
+ }
648
+ async refreshToolConfiguration() {
649
+ const metrics = await this.runtime.engine.contextMetrics();
650
+ this.runtime.initialMetrics = metrics;
651
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
652
+ this.broadcastSync();
653
+ this.publishRuntimeContext();
654
+ }
438
655
  async applySessionPluginOverrides(overrides, persist) {
439
656
  const support = this.runtime.pluginSupport;
440
657
  if (!support)
@@ -448,6 +665,9 @@ export class WebRepl {
448
665
  for (const tool of plugin.tools)
449
666
  this.runtime.tools.register(tool);
450
667
  support.overrides = normalized;
668
+ const toolSupport = this.runtime.toolSupport;
669
+ if (toolSupport)
670
+ applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
451
671
  this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
452
672
  const sessionId = this.runtime.engine.snapshot().session?.sessionId;
453
673
  if (persist && sessionId && support.persist)
@@ -474,6 +694,7 @@ export class WebRepl {
474
694
  this.broadcastSync();
475
695
  return { ok: true };
476
696
  }
697
+ this.publishRuntimeContext();
477
698
  this.startRun(text, attachments);
478
699
  return { ok: true };
479
700
  }
@@ -532,7 +753,7 @@ export class WebRepl {
532
753
  const snapshot = this.runtime.engine.snapshot().session;
533
754
  if (!snapshot)
534
755
  throw new Error("session transcripts are disabled");
535
- await this.loadSessionPlugins(snapshot.sessionId);
756
+ await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
536
757
  await this.refreshSessionView();
537
758
  return { ok: true };
538
759
  }
@@ -549,7 +770,7 @@ export class WebRepl {
549
770
  const snapshot = this.runtime.engine.snapshot().session;
550
771
  if (!snapshot)
551
772
  throw new Error("session transcripts are disabled");
552
- await this.loadSessionPlugins(snapshot.sessionId);
773
+ await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
553
774
  await this.refreshSessionView();
554
775
  return { ok: true };
555
776
  }
@@ -628,22 +849,28 @@ export class WebRepl {
628
849
  imagePayload(messageId, blockIndex) {
629
850
  if (!messageId || !Number.isInteger(blockIndex) || blockIndex < 0)
630
851
  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))
852
+ let block;
853
+ for (const entry of this.runtime.engine.getDisplayEntries()) {
854
+ if (entry.type !== "message" || entry.message.id !== messageId)
855
+ continue;
856
+ const candidate = entry.message.blocks[blockIndex];
857
+ if (candidate?.type === "image")
858
+ block = candidate;
859
+ break;
860
+ }
861
+ return block ? imagePayloadForBlock(block) : undefined;
862
+ }
863
+ imagePayloadById(imageId) {
864
+ if (!imageId)
644
865
  return undefined;
645
- const body = Buffer.from(base64, "base64");
646
- return body.length ? { body, mimeType } : undefined;
866
+ for (const entry of this.runtime.engine.getDisplayEntries()) {
867
+ if (entry.type !== "message")
868
+ continue;
869
+ const block = entry.message.blocks.find((candidate) => candidate.type === "image" && candidate.imageId === imageId);
870
+ if (block)
871
+ return imagePayloadForBlock(block);
872
+ }
873
+ return undefined;
647
874
  }
648
875
  async setFastMode(enabled) {
649
876
  try {
@@ -655,6 +882,25 @@ export class WebRepl {
655
882
  return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
656
883
  }
657
884
  }
885
+ async setContextWindowK(value) {
886
+ if (this.busy)
887
+ return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
888
+ if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
889
+ return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
890
+ }
891
+ const contextWindowK = Number(value);
892
+ const contextWindowTokens = contextWindowK * 1000;
893
+ try {
894
+ await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
895
+ const metrics = await this.runtime.engine.contextMetrics();
896
+ this.runtime.initialMetrics = metrics;
897
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
898
+ return { ok: true, contextWindowK, contextWindowTokens };
899
+ }
900
+ catch (error) {
901
+ return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
902
+ }
903
+ }
658
904
  loginForm(providerValue) {
659
905
  const provider = parseLoginProvider(providerValue);
660
906
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -711,6 +957,12 @@ export class WebRepl {
711
957
  this.broadcastSync();
712
958
  return id;
713
959
  }
960
+ appendCompaction(report, createdAt) {
961
+ this.lines = this.lines.map((line) => line.compaction
962
+ ? { ...line, compaction: { ...line.compaction, current: false } }
963
+ : line);
964
+ return this.append(compactionLine(report, createdAt, true));
965
+ }
714
966
  appendLineText(id, text) {
715
967
  this.lines = this.lines.map((line) => line.id === id ? { ...line, text: line.text + text } : line);
716
968
  this.broadcastDelta({ type: "line.text.append", id, text });
@@ -749,7 +1001,7 @@ export class WebRepl {
749
1001
  this.interruptArmed = false;
750
1002
  this.queuedInput = undefined;
751
1003
  this.queuedAttachments = undefined;
752
- this.pendingUserImageEchoLabels = undefined;
1004
+ this.pendingUserImageEchoIds = undefined;
753
1005
  this.pendingUserImageEchoMessageId = undefined;
754
1006
  this.finalizeForegroundView();
755
1007
  this.finalizeLiveToolLines();
@@ -765,10 +1017,15 @@ export class WebRepl {
765
1017
  kind: "agent",
766
1018
  taskId: task.taskId,
767
1019
  agentId: task.agentId,
1020
+ agentType: task.agentType,
768
1021
  type: task.type,
769
1022
  status: task.status,
770
1023
  description: task.description,
1024
+ prompt: task.prompt,
1025
+ progress: task.progress,
1026
+ outputFile: task.outputFile,
771
1027
  createdAt: task.createdAt,
1028
+ updatedAt: task.updatedAt,
772
1029
  }));
773
1030
  const terminalTasks = this.runtime.execProcessManager.list()
774
1031
  .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
@@ -786,8 +1043,26 @@ export class WebRepl {
786
1043
  shell: terminal.shell,
787
1044
  tty: terminal.tty,
788
1045
  durationMs: terminal.duration_ms,
1046
+ output: terminal.output,
1047
+ outputEnd: terminal.outputEnd,
789
1048
  }));
790
- return [...agentTasks, ...terminalTasks];
1049
+ const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1050
+ kind: "session",
1051
+ taskId: `session:${run.sessionId}`,
1052
+ type: "会话",
1053
+ status: "running",
1054
+ description: run.title || "未命名会话",
1055
+ createdAt: run.startedAt,
1056
+ sessionId: run.sessionId,
1057
+ }));
1058
+ const createdAtTime = (value) => {
1059
+ if (typeof value === "number")
1060
+ return value;
1061
+ const parsed = Date.parse(value);
1062
+ return Number.isFinite(parsed) ? parsed : 0;
1063
+ };
1064
+ return [...agentTasks, ...terminalTasks, ...sessionTasks]
1065
+ .sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
791
1066
  }
792
1067
  async detachRunningForeground(reason) {
793
1068
  if (!this.busy)
@@ -816,7 +1091,7 @@ export class WebRepl {
816
1091
  this.interruptArmed = false;
817
1092
  this.queuedInput = undefined;
818
1093
  this.queuedAttachments = undefined;
819
- this.pendingUserImageEchoLabels = undefined;
1094
+ this.pendingUserImageEchoIds = undefined;
820
1095
  this.pendingUserImageEchoMessageId = undefined;
821
1096
  this.busy = false;
822
1097
  this.status = { ...this.status, phase: "ready", detail: undefined };
@@ -862,7 +1137,7 @@ export class WebRepl {
862
1137
  this.reduce(event);
863
1138
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
864
1139
  const messageId = this.pendingUserImageEchoMessageId;
865
- this.pendingUserImageEchoLabels = undefined;
1140
+ this.pendingUserImageEchoIds = undefined;
866
1141
  this.pendingUserImageEchoMessageId = undefined;
867
1142
  renderMessageImages(event.message, (line) => this.append(line), messageId);
868
1143
  this.broadcastSync();
@@ -872,6 +1147,10 @@ export class WebRepl {
872
1147
  this.queueDeltaStatus();
873
1148
  return;
874
1149
  }
1150
+ if (event.type === "context.compacted") {
1151
+ this.appendCompaction(event.compaction);
1152
+ return;
1153
+ }
875
1154
  if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
876
1155
  this.broadcastSync();
877
1156
  return;
@@ -922,10 +1201,11 @@ export class WebRepl {
922
1201
  const liveLineId = this.liveToolLineIds.get(toolUseId);
923
1202
  if (liveLineId === undefined)
924
1203
  return this.append(line);
925
- this.replaceLine(liveLineId, line);
1204
+ const liveLine = this.lines.find((item) => item.id === liveLineId);
1205
+ this.replaceLine(liveLineId, { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, liveLine?.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, liveLine?.toolDisplay) });
926
1206
  this.liveToolLineIds.delete(toolUseId);
927
1207
  return liveLineId;
928
- });
1208
+ }, runtimeToolResultPresenter(this.runtime));
929
1209
  renderMessageImages(event.message, (line) => this.append(line));
930
1210
  return;
931
1211
  }
@@ -946,6 +1226,7 @@ export class WebRepl {
946
1226
  if (event.type === "tool.started") {
947
1227
  this.finalizeLiveLine(this.assistantLineId);
948
1228
  this.finalizeThinkingLine();
1229
+ this.assistantLineId = undefined;
949
1230
  const id = this.append({ ...formatToolUse(event.toolUse), live: true });
950
1231
  this.liveToolLineIds.set(event.toolUse.id, id);
951
1232
  return;
@@ -970,13 +1251,13 @@ export class WebRepl {
970
1251
  this.append({ kind: "error", text: event.error.message });
971
1252
  }
972
1253
  matchesPendingUserImageEcho(message) {
973
- const pending = this.pendingUserImageEchoLabels;
1254
+ const pending = this.pendingUserImageEchoIds;
974
1255
  if (!pending?.length || message.role !== "user")
975
1256
  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]);
1257
+ const imageIds = message.blocks
1258
+ .filter((block) => block.type === "image" && typeof block.imageId === "string")
1259
+ .map((block) => block.imageId);
1260
+ return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
980
1261
  }
981
1262
  async handleCommandOrPrompt(text, attachments = []) {
982
1263
  const command = parseReplCommand(text);
@@ -1078,11 +1359,11 @@ export class WebRepl {
1078
1359
  const promptPayload = buildWebPromptPayload(command.text, attachments);
1079
1360
  const optimisticMessageId = `web-user-${this.lineId + 1}`;
1080
1361
  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;
1362
+ const optimisticImageIds = (promptPayload.blocks ?? [])
1363
+ .filter((block) => block.type === "image" && typeof block.imageId === "string")
1364
+ .map((block) => block.imageId);
1365
+ this.pendingUserImageEchoIds = optimisticImageIds.length ? optimisticImageIds : undefined;
1366
+ this.pendingUserImageEchoMessageId = optimisticImageIds.length ? optimisticMessageId : undefined;
1086
1367
  const runToken = ++this.foregroundRunToken;
1087
1368
  const abortController = new AbortController();
1088
1369
  this.activeAbortController = abortController;
@@ -1091,7 +1372,12 @@ export class WebRepl {
1091
1372
  this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
1092
1373
  const engine = this.runtime.engine;
1093
1374
  try {
1094
- for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: promptPayload.displayText })) {
1375
+ for await (const event of engine.sendUserText(promptPayload.text, {
1376
+ abortSignal: abortController.signal,
1377
+ blocks: promptPayload.blocks,
1378
+ displayText: promptPayload.displayText,
1379
+ stopAfterTurn: () => this.queuedInput !== undefined,
1380
+ })) {
1095
1381
  if (this.foregroundRunToken !== runToken)
1096
1382
  continue;
1097
1383
  if (this.runtime.engine !== engine)
@@ -1122,7 +1408,7 @@ export class WebRepl {
1122
1408
  this.activeAbortController = undefined;
1123
1409
  this.interruptArmed = false;
1124
1410
  this.finalizeForegroundView();
1125
- this.pendingUserImageEchoLabels = undefined;
1411
+ this.pendingUserImageEchoIds = undefined;
1126
1412
  this.pendingUserImageEchoMessageId = undefined;
1127
1413
  const queuedText = this.queuedInput;
1128
1414
  const queuedAttach = this.queuedAttachments;
@@ -1155,7 +1441,10 @@ export class WebRepl {
1155
1441
  const metrics = await this.runtime.engine.contextMetrics();
1156
1442
  if (this.foregroundRunToken !== runToken)
1157
1443
  return;
1158
- this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
1444
+ if (result.report)
1445
+ this.appendCompaction(result.report);
1446
+ else
1447
+ this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
1159
1448
  this.handleEvent({ type: "context.metrics", metrics });
1160
1449
  }
1161
1450
  catch (error) {
@@ -1174,7 +1463,9 @@ export class WebRepl {
1174
1463
  }
1175
1464
  send(subscriber, event, data) {
1176
1465
  if (subscriber.blocked) {
1177
- if (event === "runtime.context")
1466
+ if (event === "client.reload")
1467
+ subscriber.pendingClientReload = data;
1468
+ else if (event === "runtime.context")
1178
1469
  subscriber.needsRuntimeContext = true;
1179
1470
  else
1180
1471
  subscriber.needsSync = true;
@@ -1189,6 +1480,11 @@ export class WebRepl {
1189
1480
  subscriber.blocked = false;
1190
1481
  if (subscriber.response.destroyed)
1191
1482
  return;
1483
+ if (subscriber.pendingClientReload) {
1484
+ const payload = subscriber.pendingClientReload;
1485
+ subscriber.pendingClientReload = undefined;
1486
+ this.send(subscriber, "client.reload", payload);
1487
+ }
1192
1488
  if (subscriber.needsSync) {
1193
1489
  subscriber.needsSync = false;
1194
1490
  this.send(subscriber, "sync", this.snapshot(false));
@@ -1264,6 +1560,39 @@ export class WebRepl {
1264
1560
  this.pendingDeltaOperations = [];
1265
1561
  this.pendingDeltaStatus = false;
1266
1562
  }
1563
+ dropInactiveTerminalOutput() {
1564
+ for (const sessionId of this.pendingTerminalOutput.keys()) {
1565
+ if (!this.runtime.execProcessManager.isBackgroundRunning(sessionId))
1566
+ this.pendingTerminalOutput.delete(sessionId);
1567
+ }
1568
+ }
1569
+ queueTerminalOutput(delta) {
1570
+ if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
1571
+ return;
1572
+ const outputStart = Number(delta.outputStart ?? 0);
1573
+ const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
1574
+ const pending = this.pendingTerminalOutput.get(delta.sessionId);
1575
+ if (pending && pending.outputEnd === outputStart) {
1576
+ pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
1577
+ pending.outputEnd = outputEnd;
1578
+ }
1579
+ else {
1580
+ this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
1581
+ }
1582
+ if (this.terminalOutputTimer)
1583
+ return;
1584
+ this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
1585
+ this.terminalOutputTimer.unref?.();
1586
+ }
1587
+ flushTerminalOutput() {
1588
+ this.terminalOutputTimer = undefined;
1589
+ if (!this.pendingTerminalOutput.size)
1590
+ return;
1591
+ const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
1592
+ this.pendingTerminalOutput.clear();
1593
+ for (const subscriber of this.subscribers)
1594
+ this.send(subscriber, "terminal.output", { updates });
1595
+ }
1267
1596
  broadcastSync() {
1268
1597
  this.discardPendingDeltaOperations();
1269
1598
  const payload = this.snapshot(false);
@@ -1271,6 +1600,10 @@ export class WebRepl {
1271
1600
  this.send(subscriber, "sync", payload);
1272
1601
  }
1273
1602
  }
1603
+ function appendBoundedText(current, incoming, limit) {
1604
+ const combined = current + incoming;
1605
+ return combined.length <= limit ? combined : combined.slice(-limit);
1606
+ }
1274
1607
  export function compactLineOperations(operations) {
1275
1608
  const compacted = [];
1276
1609
  for (const operation of operations) {
@@ -1303,20 +1636,67 @@ async function route(req, res, router) {
1303
1636
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1304
1637
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1305
1638
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1639
+ if (req.method === "GET" && url.pathname === "/api/client-info")
1640
+ return sendJson(res, router.clientInfo());
1641
+ if (req.method === "POST" && url.pathname === "/api/client-reload") {
1642
+ const body = await readJsonBody(req);
1643
+ return sendJson(res, await router.requestClientReload(body));
1644
+ }
1306
1645
  const scope = webRuntimeScopeFromUrl(url);
1307
1646
  const repl = await router.get(scope);
1308
1647
  if (req.method === "GET" && url.pathname === "/events")
1309
- return repl.subscribe(res);
1648
+ return repl.subscribe(res, router.clientInfo());
1310
1649
  if (req.method === "GET" && url.pathname === "/api/state")
1311
1650
  return sendJson(res, repl.snapshot(true));
1312
1651
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
1313
1652
  return sendJson(res, await repl.runtimeContext());
1653
+ const workspaceRepl = repl;
1654
+ if (req.method === "GET" && url.pathname === "/api/cwd") {
1655
+ return sendJson(res, workspaceRepl.browseWorkspace
1656
+ ? await workspaceRepl.browseWorkspace(url.searchParams.get("path") ?? undefined)
1657
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1658
+ }
1659
+ if (req.method === "POST" && url.pathname === "/api/cwd/change") {
1660
+ const body = await readJsonBody(req);
1661
+ return sendJson(res, workspaceRepl.changeWorkspace
1662
+ ? await workspaceRepl.changeWorkspace(body.path)
1663
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1664
+ }
1665
+ if (req.method === "POST" && url.pathname === "/api/cwd/create") {
1666
+ const body = await readJsonBody(req);
1667
+ return sendJson(res, workspaceRepl.createWorkspaceDirectory
1668
+ ? await workspaceRepl.createWorkspaceDirectory(body.path)
1669
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1670
+ }
1671
+ if (req.method === "POST" && url.pathname === "/api/cwd/delete") {
1672
+ const body = await readJsonBody(req);
1673
+ return sendJson(res, workspaceRepl.deleteWorkspaceDirectory
1674
+ ? await workspaceRepl.deleteWorkspaceDirectory(body.path)
1675
+ : actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
1676
+ }
1677
+ if (req.method === "GET" && url.pathname === "/api/tools")
1678
+ return sendJson(res, repl.globalTools());
1679
+ if (req.method === "POST" && url.pathname === "/api/tools/global") {
1680
+ const body = await readJsonBody(req);
1681
+ return sendJson(res, await repl.setGlobalTools(body.overrides));
1682
+ }
1683
+ if (req.method === "GET" && url.pathname === "/api/session-tools")
1684
+ return sendJson(res, repl.sessionTools());
1685
+ if (req.method === "POST" && url.pathname === "/api/session-tools") {
1686
+ const body = await readJsonBody(req);
1687
+ return sendJson(res, await repl.setSessionTools(body.overrides));
1688
+ }
1314
1689
  if (req.method === "GET" && url.pathname === "/api/session-plugins")
1315
1690
  return sendJson(res, repl.sessionPlugins());
1316
1691
  if (req.method === "POST" && url.pathname === "/api/session-plugins") {
1317
1692
  const body = await readJsonBody(req);
1318
1693
  return sendJson(res, await repl.setSessionPlugins(body.overrides));
1319
1694
  }
1695
+ const imageIdMatch = /^\/api\/images\/by-id\/([^/]+)$/u.exec(url.pathname);
1696
+ if (req.method === "GET" && imageIdMatch) {
1697
+ const payload = repl.imagePayloadById(decodeURIComponent(imageIdMatch[1]));
1698
+ return payload ? sendImage(res, payload) : sendJson(res, { error: "image not found" }, 404);
1699
+ }
1320
1700
  const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
1321
1701
  if (req.method === "GET" && imageMatch) {
1322
1702
  const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
@@ -1332,6 +1712,10 @@ async function route(req, res, router) {
1332
1712
  const body = await readJsonBody(req);
1333
1713
  return sendJson(res, await repl.setFastMode(body.enabled === true));
1334
1714
  }
1715
+ if (req.method === "POST" && url.pathname === "/api/context-window") {
1716
+ const body = await readJsonBody(req);
1717
+ return sendJson(res, await repl.setContextWindowK(body.value));
1718
+ }
1335
1719
  if (req.method === "POST" && url.pathname === "/api/submit") {
1336
1720
  const body = await readJsonBody(req);
1337
1721
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
@@ -1393,6 +1777,20 @@ function sendJson(res, value, status = 200) {
1393
1777
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1394
1778
  res.end(JSON.stringify(value));
1395
1779
  }
1780
+ function imagePayloadForBlock(block) {
1781
+ const resolved = resolveImageBlockDataSync(block)?.trim();
1782
+ if (!resolved)
1783
+ return undefined;
1784
+ const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/iu.exec(resolved);
1785
+ const mimeType = (dataUrl?.[1] ?? block.mimeType).toLowerCase();
1786
+ if (!mimeType.startsWith("image/"))
1787
+ return undefined;
1788
+ const base64 = (dataUrl?.[2] ?? resolved).replace(/\s+/gu, "");
1789
+ if (!base64 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
1790
+ return undefined;
1791
+ const body = Buffer.from(base64, "base64");
1792
+ return body.length ? { body, mimeType } : undefined;
1793
+ }
1396
1794
  function sendImage(res, payload) {
1397
1795
  res.writeHead(200, {
1398
1796
  "Content-Type": payload.mimeType,
@@ -1416,7 +1814,8 @@ function sanitizeWebAttachments(value) {
1416
1814
  if (!isRecord(item))
1417
1815
  return [];
1418
1816
  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 }];
1817
+ const imageId = typeof item.imageId === "string" && /^image_[0-9a-f-]{36}$/iu.test(item.imageId) ? item.imageId : createImageId();
1818
+ return [{ kind: "image", imageId, label: item.label, mimeType: item.mimeType, data: item.data }];
1420
1819
  }
1421
1820
  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
1821
  return [{
@@ -1454,14 +1853,44 @@ function initialLines(runtime, lineId) {
1454
1853
  lines.push({ id: ++lineId.current, ...line });
1455
1854
  return lines;
1456
1855
  }
1856
+ export function restoreWebHistoryLines(runtime) {
1857
+ return restoredHistoryLines(runtime).map((line) => ({ ...line }));
1858
+ }
1457
1859
  function restoredHistoryLines(runtime) {
1458
1860
  const lines = [];
1861
+ const historyToolLineIndexes = new Map();
1459
1862
  const append = (line) => {
1863
+ if (line.toolUseId && line.titleStatus && historyToolLineIndexes.has(line.toolUseId)) {
1864
+ const index = historyToolLineIndexes.get(line.toolUseId);
1865
+ const started = lines[index];
1866
+ lines[index] = { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
1867
+ return index + 1;
1868
+ }
1460
1869
  lines.push(line);
1870
+ if (line.toolUseId && !line.titleStatus)
1871
+ historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
1461
1872
  return lines.length;
1462
1873
  };
1463
- for (const message of runtime.engine.getHistoryMessages())
1464
- renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: true });
1874
+ const entries = runtime.engine.getDisplayEntries();
1875
+ let lastCompactionIndex = -1;
1876
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
1877
+ if (entries[index].type === "compact") {
1878
+ lastCompactionIndex = index;
1879
+ break;
1880
+ }
1881
+ }
1882
+ for (const [entryIndex, entry] of entries.entries()) {
1883
+ if (entry.type === "message") {
1884
+ renderMessage(entry.message, append, undefined, {
1885
+ includeToolUseBlocks: true,
1886
+ includeThinkingBlocks: true,
1887
+ presentToolResult: runtimeToolResultPresenter(runtime),
1888
+ });
1889
+ continue;
1890
+ }
1891
+ const report = entry.report ?? legacyCompactionReport(entry.reason);
1892
+ append(compactionLine(report, entry.createdAt, entryIndex === lastCompactionIndex));
1893
+ }
1465
1894
  return lines;
1466
1895
  }
1467
1896
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
@@ -1471,7 +1900,18 @@ async function resetStatus(runtime) {
1471
1900
  return initialStatus(runtime, await runtime.engine.contextMetrics());
1472
1901
  }
1473
1902
  function buildWebPromptPayload(displayText, attachments) {
1474
- const activeAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
1903
+ const candidateAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
1904
+ const labels = new Set();
1905
+ const imageIds = new Set();
1906
+ for (const attachment of candidateAttachments) {
1907
+ if (labels.has(attachment.label))
1908
+ throw new Error(`Duplicate image label in one submission: ${attachment.label}`);
1909
+ if (imageIds.has(attachment.imageId))
1910
+ throw new Error(`Duplicate image identity in one submission: ${attachment.imageId}`);
1911
+ labels.add(attachment.label);
1912
+ imageIds.add(attachment.imageId);
1913
+ }
1914
+ const activeAttachments = candidateAttachments;
1475
1915
  const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
1476
1916
  if (activeAttachments.length === 0) {
1477
1917
  const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
@@ -1486,7 +1926,7 @@ function buildWebPromptPayload(displayText, attachments) {
1486
1926
  break;
1487
1927
  }
1488
1928
  pushTextBlock(blocks, displayText.slice(cursor, next.index));
1489
- blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
1929
+ blocks.push({ type: "image", imageId: next.attachment.imageId, mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
1490
1930
  cursor = next.index + next.attachment.label.length;
1491
1931
  }
1492
1932
  pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
@@ -1534,6 +1974,8 @@ function pushTextBlock(blocks, text) {
1534
1974
  function reduceStatus(status, event) {
1535
1975
  if (event.type === "state")
1536
1976
  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 };
1977
+ if (event.type === "context.compacted")
1978
+ return { ...status, phase: "compacting", detail: formatCompactionReportSummary(event.compaction), activityTick: status.activityTick + 1 };
1537
1979
  if (event.type === "context.metrics")
1538
1980
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1539
1981
  if (event.type === "usage")
@@ -1862,6 +2304,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1862
2304
  if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
1863
2305
  return true;
1864
2306
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
2307
+ const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
1865
2308
  let rendered = false;
1866
2309
  for (const [blockIndex, block] of message.blocks.entries()) {
1867
2310
  if (block.type === "text") {
@@ -1878,7 +2321,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1878
2321
  const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1879
2322
  if (!line)
1880
2323
  continue;
1881
- append({ ...line, messageId: message.id, parentToolName });
2324
+ append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
1882
2325
  rendered = true;
1883
2326
  }
1884
2327
  else if (block.type === "thinking") {
@@ -1888,22 +2331,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1888
2331
  rendered = true;
1889
2332
  }
1890
2333
  else if (block.type === "tool_use" && options.includeToolUseBlocks) {
1891
- append({ ...formatToolUse(block), messageId: message.id, live: false });
2334
+ append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
1892
2335
  rendered = true;
1893
2336
  }
1894
2337
  else if (block.type === "tool_result") {
1895
- append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
2338
+ append({ ...formatToolResultLine(block.name, block.output, block.ok, options.presentToolResult), messageId: message.id, toolUseId: block.toolUseId });
1896
2339
  rendered = true;
1897
2340
  }
1898
2341
  }
1899
2342
  return rendered;
1900
2343
  }
1901
- function renderToolResultMessage(message, append) {
2344
+ function renderToolResultMessage(message, append, presentToolResult) {
1902
2345
  let rendered = false;
1903
2346
  for (const block of message.blocks) {
1904
2347
  if (block.type !== "tool_result")
1905
2348
  continue;
1906
- append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
2349
+ append({ ...formatToolResultLine(block.name, block.output, block.ok, presentToolResult), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
1907
2350
  rendered = true;
1908
2351
  }
1909
2352
  return rendered;
@@ -1927,13 +2370,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
1927
2370
  const kind = kindForRole(role);
1928
2371
  if (kind === "meta")
1929
2372
  return undefined;
2373
+ const resolution = resolveImageBlockDataResultSync(block);
1930
2374
  return {
1931
2375
  kind,
1932
2376
  text: block.label ?? `[image ${block.mimeType}]`,
1933
2377
  image: {
1934
- src: `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`,
2378
+ src: resolution.available ? (block.imageId ? `/api/images/by-id/${encodeURIComponent(block.imageId)}` : `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`) : undefined,
2379
+ imageId: block.imageId,
1935
2380
  label: block.label,
1936
2381
  mimeType: block.mimeType,
2382
+ available: resolution.available,
2383
+ error: resolution.available ? undefined : resolution.error,
2384
+ blockIndex,
1937
2385
  },
1938
2386
  };
1939
2387
  }
@@ -1985,20 +2433,187 @@ function titleForKind(kind) {
1985
2433
  function systemLine(text, summaryMaxLines) {
1986
2434
  return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
1987
2435
  }
2436
+ function legacyCompactionReport(reason) {
2437
+ return {
2438
+ reason: reason && reason !== "none" ? reason : "manualcompact",
2439
+ summary: "此检查点由旧版 neoctl 持久化,未保存详细压缩报告。",
2440
+ continuationState: "该旧版检查点没有可用的详细续接上下文。",
2441
+ sourceMessages: 0,
2442
+ preservedUserMessages: 0,
2443
+ newWindowMessages: 0,
2444
+ charsFreed: 0,
2445
+ modelDriven: false,
2446
+ imageCount: 0,
2447
+ };
2448
+ }
2449
+ function compactionLine(report, createdAt = new Date().toISOString(), current = true) {
2450
+ return {
2451
+ kind: "meta",
2452
+ title: "Context compaction",
2453
+ text: formatCompactionReportSummary(report),
2454
+ format: "plain",
2455
+ compaction: { ...report, createdAt, current },
2456
+ };
2457
+ }
2458
+ function compactionReportFromBoundary(message) {
2459
+ if (!message?.metadata?.compactBoundary)
2460
+ return undefined;
2461
+ const stored = message.metadata.compactionReport;
2462
+ if (stored?.continuationState)
2463
+ return { ...stored };
2464
+ const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
2465
+ const continuationState = /<compact_state>\s*([\s\S]*?)\s*<\/compact_state>/i.exec(text)?.[1]?.trim();
2466
+ if (!continuationState)
2467
+ return undefined;
2468
+ const reasonValue = typeof message.metadata.compactionReason === "string" ? message.metadata.compactionReason : "autocompact";
2469
+ const reason = isCompactionReason(reasonValue) ? reasonValue : "autocompact";
2470
+ const imageRegistry = message.metadata.imageRegistry;
2471
+ return {
2472
+ reason,
2473
+ summary: continuationState,
2474
+ continuationState,
2475
+ sourceMessages: 0,
2476
+ preservedUserMessages: 0,
2477
+ newWindowMessages: 1,
2478
+ charsFreed: 0,
2479
+ modelDriven: message.metadata.modelDriven === true,
2480
+ imageCount: Array.isArray(imageRegistry?.images) ? imageRegistry.images.length : 0,
2481
+ };
2482
+ }
2483
+ function isCompactionReason(value) {
2484
+ return ["snip", "microcompact", "autocompact", "reactive_compact", "manualcompact", "purecompact"].includes(value);
2485
+ }
2486
+ function formatCompactionReportSummary(report) {
2487
+ return `${report.newWindowMessages} message(s) in new window, ${report.preservedUserMessages} user message(s) preserved`;
2488
+ }
1988
2489
  function thinkingLine(text, live = false) {
1989
2490
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1990
2491
  }
1991
2492
  function formatToolUse(toolUse) {
2493
+ const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
1992
2494
  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 };
2495
+ return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
1994
2496
  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 };
2497
+ 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", presentationLevel: "primary", collapsible: true };
1996
2498
  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 };
2499
+ return { kind: "tool", toolName: toolUse.name, toolUseId: toolUse.id, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
2500
+ }
2501
+ function formatToolResultLine(toolName, output, ok, presentToolResult) {
2502
+ const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
2503
+ const resources = presentation?.resources ?? toolResources(output);
2504
+ const formatted = resources?.length
2505
+ ? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
2506
+ : formatToolResult(toolName, output, ok);
2507
+ return {
2508
+ kind: ok ? "tool" : "error",
2509
+ toolName,
2510
+ title: presentation?.title ?? toolTitle(toolName, "finished"),
2511
+ bodyTitle: presentation?.bodyTitle ?? formatted.bodyTitle,
2512
+ titleStatus: ok ? "success" : "failure",
2513
+ text: presentation?.text ?? formatted.text,
2514
+ toolDisplay: resources?.length ? undefined : buildToolResultDisplay(toolName, output, ok),
2515
+ format: formatted.format,
2516
+ live: false,
2517
+ previewStyle: formatted.full ? undefined : "summary",
2518
+ summaryMaxLines: formatted.summaryMaxLines,
2519
+ presentationLevel: presentation?.presentationLevel ?? toolPresentationLevel(toolName, output),
2520
+ resources,
2521
+ collapsible: true,
2522
+ };
2523
+ }
2524
+ function toolPresentationLevel(toolName, payload) {
2525
+ if (toolName === "image2")
2526
+ return "primary";
2527
+ if (!isRecord(payload))
2528
+ return "process";
2529
+ const direct = payload.presentationLevel;
2530
+ const ui = isRecord(payload.ui) ? payload.ui.presentationLevel : undefined;
2531
+ const underscoredUi = isRecord(payload._ui) ? payload._ui.presentationLevel : undefined;
2532
+ return [direct, ui, underscoredUi].some((value) => value === "primary") ? "primary" : "process";
2533
+ }
2534
+ function toolUiMetadata(payload) {
2535
+ if (!isRecord(payload))
2536
+ return undefined;
2537
+ const containers = [payload, payload.output, payload.result].filter(isRecord);
2538
+ for (const container of containers) {
2539
+ if (isRecord(container._ui))
2540
+ return container._ui;
2541
+ if (isRecord(container.ui))
2542
+ return container.ui;
2543
+ }
2544
+ return undefined;
2545
+ }
2546
+ function toolResources(payload) {
2547
+ const ui = toolUiMetadata(payload);
2548
+ if (!ui || !Array.isArray(ui.resources))
2549
+ return undefined;
2550
+ return normalizeUiResources(ui.resources);
2551
+ }
2552
+ function normalizeUiResources(values) {
2553
+ if (!Array.isArray(values))
2554
+ return undefined;
2555
+ const resources = values.flatMap((value) => {
2556
+ if (!isRecord(value))
2557
+ return [];
2558
+ const kind = stringValue(value.kind);
2559
+ const url = stringValue(value.url);
2560
+ if (!kind || !url || !safeResourceUrl(url))
2561
+ return [];
2562
+ return [{
2563
+ kind,
2564
+ url,
2565
+ reference: stringValue(value.reference) ?? canonicalResourceReference(url),
2566
+ label: stringValue(value.label),
2567
+ downloadName: stringValue(value.downloadName),
2568
+ sizeBytes: numberValue(value.sizeBytes),
2569
+ expiresAt: numberValue(value.expiresAt),
2570
+ mimeType: stringValue(value.mimeType),
2571
+ height: clampResourceHeight(numberValue(value.height)),
2572
+ }];
2573
+ });
2574
+ return resources.length > 0 ? resources : undefined;
1998
2575
  }
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 };
2576
+ function canonicalResourceReference(url) {
2577
+ return url.startsWith("/") ? `sandbox:${url}` : url;
2578
+ }
2579
+ function clampResourceHeight(value) {
2580
+ return value === undefined ? undefined : Math.max(240, Math.min(1600, Math.round(value)));
2581
+ }
2582
+ function normalizeToolResultPresentation(value) {
2583
+ if (!isRecord(value))
2584
+ return undefined;
2585
+ const level = value.presentationLevel === "primary" || value.presentationLevel === "process" ? value.presentationLevel : undefined;
2586
+ return {
2587
+ title: stringValue(value.title),
2588
+ bodyTitle: stringValue(value.bodyTitle),
2589
+ text: stringValue(value.text),
2590
+ presentationLevel: level,
2591
+ resources: normalizeUiResources(value.resources),
2592
+ };
2593
+ }
2594
+ function runtimeToolResultPresenter(runtime) {
2595
+ return (toolName, output, ok) => {
2596
+ const support = runtime.pluginSupport;
2597
+ if (!support)
2598
+ return undefined;
2599
+ const sessionId = runtime.engine.snapshot().session?.sessionId;
2600
+ for (const plugin of activeWebRuntimePlugins(support.catalog, support.overrides)) {
2601
+ if (typeof plugin.presentToolResult !== "function")
2602
+ continue;
2603
+ try {
2604
+ const presentation = plugin.presentToolResult({ toolName, output, ok, sessionId });
2605
+ if (presentation !== undefined && presentation !== null)
2606
+ return presentation;
2607
+ }
2608
+ catch (error) {
2609
+ console.warn(`plugin ${plugin.id} failed to present ${toolName}:`, error);
2610
+ }
2611
+ }
2612
+ return undefined;
2613
+ };
2614
+ }
2615
+ function safeResourceUrl(value) {
2616
+ return /^(?:https?:\/\/|\/)/i.test(value) && !/^\/\//.test(value);
2002
2617
  }
2003
2618
  function toolTitle(toolName, _phase) {
2004
2619
  const labels = {
@@ -2009,13 +2624,30 @@ function toolTitle(toolName, _phase) {
2009
2624
  expose_downloads: "文件下载",
2010
2625
  grep: "搜索文本",
2011
2626
  image2: "图片生成",
2012
- image_note: "记录图片说明",
2627
+ image_note: "记录图片",
2013
2628
  list: "列出文件",
2014
2629
  load_image: "读取图片",
2015
2630
  plan: "任务计划",
2016
2631
  read: "读取文件",
2017
2632
  search: "网络搜索",
2018
2633
  write: "写入文件",
2634
+ SendMessage: "发送协作消息",
2635
+ sendmessage: "发送协作消息",
2636
+ TaskGet: "读取后台任务",
2637
+ taskget: "读取后台任务",
2638
+ TaskList: "后台任务列表",
2639
+ tasklist: "后台任务列表",
2640
+ TaskOutput: "读取任务输出",
2641
+ taskoutput: "读取任务输出",
2642
+ TaskResume: "继续后台任务",
2643
+ taskresume: "继续后台任务",
2644
+ TaskStop: "停止后台任务",
2645
+ taskstop: "停止后台任务",
2646
+ multi_tool_use: "并行执行工具",
2647
+ secret_list: "密钥列表",
2648
+ secret_info: "查看密钥",
2649
+ secret_request: "申请密钥",
2650
+ agent_report: "子任务报告",
2019
2651
  };
2020
2652
  return labels[toolName] ?? toolName;
2021
2653
  }
@@ -2046,6 +2678,253 @@ function toolUsePurpose(toolName, input) {
2046
2678
  return `修改 ${path.basename(input.path)}`;
2047
2679
  return undefined;
2048
2680
  }
2681
+ function mergePresentationLevel(result, started) {
2682
+ return result === "primary" || started === "primary" ? "primary" : result ?? started;
2683
+ }
2684
+ function mergeToolDisplays(result, started) {
2685
+ if (!result)
2686
+ return started;
2687
+ if (!started)
2688
+ return result;
2689
+ const facts = [...started.facts];
2690
+ for (const fact of result.facts) {
2691
+ const existing = facts.findIndex((item) => item.label === fact.label);
2692
+ if (existing >= 0)
2693
+ facts[existing] = fact;
2694
+ else
2695
+ facts.push(fact);
2696
+ }
2697
+ const subject = result.subject || started.subject;
2698
+ const purpose = result.purpose || started.purpose;
2699
+ return normalizeToolDisplay({
2700
+ subject,
2701
+ purpose,
2702
+ facts,
2703
+ previews: result.previews.length > 0 ? result.previews : started.previews,
2704
+ });
2705
+ }
2706
+ function normalizeToolDisplay(display) {
2707
+ let subject = display.subject?.trim() || undefined;
2708
+ const purpose = display.purpose?.trim() || undefined;
2709
+ if (subject && purpose && normalizeDisplayText(subject) === normalizeDisplayText(purpose))
2710
+ subject = undefined;
2711
+ const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
2712
+ const facts = display.facts.filter((fact) => {
2713
+ const normalized = normalizeDisplayText(fact.value);
2714
+ if (!normalized || visibleText.has(normalized))
2715
+ return false;
2716
+ visibleText.add(normalized);
2717
+ return true;
2718
+ });
2719
+ const previews = display.previews.filter((preview) => {
2720
+ const normalized = normalizeDisplayText(preview.content);
2721
+ if (!normalized || visibleText.has(normalized))
2722
+ return false;
2723
+ visibleText.add(normalized);
2724
+ return true;
2725
+ });
2726
+ return { subject, purpose, facts, previews };
2727
+ }
2728
+ function normalizeDisplayText(value) {
2729
+ return value.replace(/\s+/gu, " ").trim().toLowerCase();
2730
+ }
2731
+ function buildToolUseDisplay(toolName, input) {
2732
+ const data = isRecord(input) ? input : {};
2733
+ const purpose = stringValue(data.description);
2734
+ const subject = toolName === "exec_command" ? undefined : toolDisplaySubject(toolName, data);
2735
+ const facts = [];
2736
+ const previews = [];
2737
+ if (toolName === "exec_command")
2738
+ pushToolPreview(previews, "命令", "code", data.cmd);
2739
+ else if (toolName === "write_stdin") {
2740
+ pushToolFact(facts, "会话", data.session_id, true);
2741
+ pushToolFact(facts, "输入", data.chars, true);
2742
+ }
2743
+ else if (toolName === "grep")
2744
+ pushToolFact(facts, "范围", data.path, true);
2745
+ else if (toolName === "load_image")
2746
+ pushToolFact(facts, "图片", arrayLabel(data.imageRefs), true);
2747
+ else if (toolName === "image2") {
2748
+ pushToolFact(facts, "模式", data.mode === "edit" ? "编辑" : "生成");
2749
+ pushToolFact(facts, "名称", data.semanticName);
2750
+ }
2751
+ else if (toolName === "expose_downloads")
2752
+ pushToolFact(facts, "文件", arrayLabel(data.paths), true);
2753
+ else if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage")
2754
+ pushToolFact(facts, "接收方", data.target);
2755
+ else if (toolName.toLowerCase().startsWith("task"))
2756
+ pushToolFact(facts, "任务", data.task_id, true);
2757
+ else if (toolName.startsWith("secret_")) {
2758
+ pushToolFact(facts, "密钥", data.key, true);
2759
+ pushToolFact(facts, "用途", data.reason);
2760
+ }
2761
+ else if (toolName === "agent") {
2762
+ pushToolFact(facts, "任务", data.description ?? data.prompt);
2763
+ pushToolFact(facts, "类型", data.subagent_type ?? data.mode);
2764
+ }
2765
+ return normalizeToolDisplay({ subject, purpose, facts, previews });
2766
+ }
2767
+ function buildToolResultDisplay(toolName, output, ok) {
2768
+ const data = isRecord(output) ? output : {};
2769
+ const facts = [];
2770
+ const previews = [];
2771
+ let subject = toolDisplaySubject(toolName, data);
2772
+ let purpose = stringValue(data.description);
2773
+ if ((toolName === "edit" || toolName === "write") && isEditToolOutput(data)) {
2774
+ subject = data.path;
2775
+ pushToolPreview(previews, undefined, "diff", compactEditDiff(data));
2776
+ }
2777
+ else if (isExecOutput(data)) {
2778
+ subject = undefined;
2779
+ purpose = stringValue(data.description);
2780
+ pushToolPreview(previews, "命令", "code", data.command);
2781
+ const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
2782
+ if (outputText)
2783
+ pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
2784
+ if (data.timed_out && !outputText)
2785
+ pushToolPreview(previews, undefined, "text", "命令执行超时");
2786
+ else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
2787
+ pushToolPreview(previews, undefined, "text", `命令退出码:${data.exit_code ?? "未知"}`);
2788
+ }
2789
+ else if (toolName === "grep") {
2790
+ subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
2791
+ pushToolPreview(previews, undefined, "list", grepPreview(data));
2792
+ }
2793
+ else if (toolName === "search") {
2794
+ subject = stringValue(data.query);
2795
+ pushToolPreview(previews, undefined, "list", searchPreview(data));
2796
+ }
2797
+ else if (toolName === "read") {
2798
+ subject = stringValue(data.path);
2799
+ if (numberValue(data.startLine) !== undefined && numberValue(data.endLine) !== undefined)
2800
+ pushToolFact(facts, "范围", `${data.startLine}–${data.endLine} 行`);
2801
+ pushToolPreview(previews, undefined, "code", data.content);
2802
+ }
2803
+ else if (toolName === "list") {
2804
+ subject = stringValue(data.path);
2805
+ pushToolPreview(previews, undefined, "list", listPreview(data));
2806
+ }
2807
+ else if (toolName === "image2") {
2808
+ subject = stringValue(data.semanticName);
2809
+ pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
2810
+ pushToolFact(facts, "尺寸", data.size);
2811
+ }
2812
+ else if (toolName === "load_image") {
2813
+ subject = arrayLabel(data.imageRefs);
2814
+ }
2815
+ else if (toolResources(output)?.length) {
2816
+ subject = toolResources(output)?.map((item) => item.label || item.downloadName).filter(Boolean).join("、");
2817
+ }
2818
+ else if (toolName.toLowerCase().startsWith("task") || toolName === "agent") {
2819
+ subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
2820
+ }
2821
+ else if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage") {
2822
+ subject = stringValue(data.target);
2823
+ }
2824
+ else if (toolName.startsWith("secret_")) {
2825
+ subject = stringValue(data.key);
2826
+ pushToolFact(facts, "密钥", data.key, true);
2827
+ if (toolName === "secret_list")
2828
+ pushToolFact(facts, "数量", `${arrayLength(data.secrets)} 项`);
2829
+ }
2830
+ else if (toolName === "agent_report") {
2831
+ }
2832
+ else {
2833
+ subject = subject || stringValue(data.path) || stringValue(data.id) || stringValue(data.name);
2834
+ }
2835
+ return normalizeToolDisplay({ subject, purpose, facts, previews });
2836
+ }
2837
+ function toolDisplaySubject(toolName, data) {
2838
+ if (["read", "write", "edit", "list"].includes(toolName))
2839
+ return stringValue(data.path);
2840
+ if (toolName === "exec_command")
2841
+ return stringValue(data.cmd) || stringValue(data.command);
2842
+ if (toolName === "grep" || toolName === "search")
2843
+ return stringValue(data.query);
2844
+ if (toolName === "image2")
2845
+ return stringValue(data.semanticName);
2846
+ if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage")
2847
+ return stringValue(data.target);
2848
+ if (toolName.toLowerCase().startsWith("task"))
2849
+ return stringValue(data.task_id);
2850
+ return undefined;
2851
+ }
2852
+ function pushToolPreview(previews, label, kind, value) {
2853
+ if (typeof value !== "string" || !value.trim())
2854
+ return;
2855
+ const content = value.replace(/\r\n/g, "\n").trim();
2856
+ previews.push({ label, kind, content: truncate(content, 1400) });
2857
+ }
2858
+ function compactEditDiff(output) {
2859
+ const lines = [];
2860
+ for (const hunk of output.patch.slice(0, 3)) {
2861
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
2862
+ lines.push(...hunk.lines.slice(0, 14));
2863
+ }
2864
+ return lines.join("\n");
2865
+ }
2866
+ function grepPreview(data) {
2867
+ if (!Array.isArray(data.matches))
2868
+ return "";
2869
+ return data.matches.slice(0, 5).filter(isRecord).map((match) => {
2870
+ const file = stringValue(match.file) || "";
2871
+ const line = numberValue(match.line);
2872
+ const text = stringValue(match.text) || "";
2873
+ return `${file}${line !== undefined ? `:${line}` : ""} ${text}`.trim();
2874
+ }).filter(Boolean).join("\n");
2875
+ }
2876
+ function searchPreview(data) {
2877
+ if (!Array.isArray(data.results))
2878
+ return "";
2879
+ return data.results.slice(0, 4).filter(isRecord).map((result) => {
2880
+ const title = stringValue(result.title) || stringValue(result.url) || "";
2881
+ return title;
2882
+ }).filter(Boolean).join("\n");
2883
+ }
2884
+ function listPreview(data) {
2885
+ if (!Array.isArray(data.entries))
2886
+ return "";
2887
+ return data.entries.slice(0, 8).filter(isRecord).map((entry) => stringValue(entry.path) || stringValue(entry.name) || "").filter(Boolean).join("\n");
2888
+ }
2889
+ function pushToolFact(facts, label, value, code = false, tone = "neutral") {
2890
+ const normalized = stringValue(value);
2891
+ if (!normalized || facts.some((fact) => fact.label === label && fact.value === normalized))
2892
+ return;
2893
+ facts.push({ label, value: truncate(normalized, 220), code, tone });
2894
+ }
2895
+ function stringValue(value) {
2896
+ if (typeof value === "string" && value.trim())
2897
+ return value.replace(/\s+/gu, " ").trim();
2898
+ if (typeof value === "number" || typeof value === "boolean")
2899
+ return String(value);
2900
+ return undefined;
2901
+ }
2902
+ function numberValue(value) {
2903
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2904
+ }
2905
+ function arrayLength(value) {
2906
+ return Array.isArray(value) ? value.length : 0;
2907
+ }
2908
+ function arrayLabel(value) {
2909
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0).join("、") : undefined;
2910
+ }
2911
+ function editOperationLabel(operation) {
2912
+ if (operation === "create")
2913
+ return "新建";
2914
+ if (operation === "edit")
2915
+ return "修改";
2916
+ if (operation === "write")
2917
+ return "写入";
2918
+ return operation;
2919
+ }
2920
+ function formatDuration(durationMs) {
2921
+ return durationMs >= 1000 ? `${(durationMs / 1000).toFixed(durationMs >= 10000 ? 0 : 1)} 秒` : `${durationMs} 毫秒`;
2922
+ }
2923
+ function taskStatusDisplay(value, ok) {
2924
+ const labels = { pending: "排队中", queued: "排队中", running: "运行中", completed: "已完成", failed: "失败", killed: "已停止", stopped: "已停止" };
2925
+ const status = stringValue(value);
2926
+ return status ? labels[status] ?? status : ok ? "已完成" : "失败";
2927
+ }
2049
2928
  function formatToolResult(toolName, output, ok) {
2050
2929
  if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
2051
2930
  return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
@@ -2061,36 +2940,15 @@ function formatToolResult(toolName, output, ok) {
2061
2940
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2062
2941
  if (toolName === "image2" && isRecord(output))
2063
2942
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
2064
- if (toolName === "expose_downloads")
2065
- return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
2943
+ const resources = toolResources(output);
2944
+ if (resources?.length)
2945
+ return { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" };
2066
2946
  if (toolName === "plan" && isWebPlanPayload(output))
2067
2947
  return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
2068
2948
  if (typeof output === "string")
2069
2949
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2070
2950
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2071
2951
  }
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
2952
  function isEditToolOutput(value) {
2095
2953
  return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
2096
2954
  }
@@ -2395,8 +3253,9 @@ function formatUsageTotals(totals) {
2395
3253
  }
2396
3254
  function formatManualCompaction(result) {
2397
3255
  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`;
3256
+ return "No conversation context available to compact.";
3257
+ const preservedUsers = result.report?.preservedUserMessages ?? result.messages.filter((message) => message.metadata?.compactPreservedUser === true).length;
3258
+ return `context compacted: ${result.messages.length} message(s) in new window, ${preservedUsers} user message(s) preserved`;
2400
3259
  }
2401
3260
  function formatPureCompaction(result) {
2402
3261
  if (!result.changed)
@@ -2406,6 +3265,8 @@ function formatPureCompaction(result) {
2406
3265
  function formatNumber(value) {
2407
3266
  return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
2408
3267
  }
3268
+ const TERMINAL_OUTPUT_FLUSH_MS = 100;
3269
+ const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
2409
3270
  const THINKING_SUMMARY_MAX_LINES = 1000;
2410
3271
  const EXPANDED_SUMMARY_MAX_LINES = 1000;
2411
3272
  const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;