neoctl 0.2.29 → 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 (98) hide show
  1. package/README.md +40 -4
  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 +16 -2
  60. package/dist/tools/builtins/exec-process-manager.js +119 -25
  61. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  62. package/dist/tools/builtins/exec-tool.js +11 -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 +88 -4
  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 +176 -11
  85. package/dist/web/index.js +986 -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.d.ts +1 -0
  96. package/dist/web/smoke-web-terminal-tasks.js +90 -0
  97. package/dist/web/smoke-web-terminal-tasks.js.map +1 -0
  98. package/package.json +5 -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();
@@ -759,16 +1011,58 @@ export class WebRepl {
759
1011
  return runWasActive;
760
1012
  }
761
1013
  backgroundTasks() {
762
- return this.runtime.taskStore.list()
1014
+ const agentTasks = this.runtime.taskStore.list()
763
1015
  .filter((task) => !this.runtime.taskStore.isTerminal(task))
764
1016
  .map((task) => ({
1017
+ kind: "agent",
765
1018
  taskId: task.taskId,
766
1019
  agentId: task.agentId,
1020
+ agentType: task.agentType,
767
1021
  type: task.type,
768
1022
  status: task.status,
769
1023
  description: task.description,
1024
+ prompt: task.prompt,
1025
+ progress: task.progress,
1026
+ outputFile: task.outputFile,
770
1027
  createdAt: task.createdAt,
1028
+ updatedAt: task.updatedAt,
1029
+ }));
1030
+ const terminalTasks = this.runtime.execProcessManager.list()
1031
+ .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
1032
+ .map((terminal) => ({
1033
+ kind: "terminal",
1034
+ taskId: `terminal:${terminal.session_id}`,
1035
+ type: "终端",
1036
+ status: terminal.status,
1037
+ description: terminal.description || terminal.command,
1038
+ createdAt: Date.now() - terminal.duration_ms,
1039
+ sessionId: terminal.session_id,
1040
+ processId: terminal.process_id,
1041
+ command: terminal.command,
1042
+ cwd: terminal.cwd,
1043
+ shell: terminal.shell,
1044
+ tty: terminal.tty,
1045
+ durationMs: terminal.duration_ms,
1046
+ output: terminal.output,
1047
+ outputEnd: terminal.outputEnd,
771
1048
  }));
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));
772
1066
  }
773
1067
  async detachRunningForeground(reason) {
774
1068
  if (!this.busy)
@@ -797,7 +1091,7 @@ export class WebRepl {
797
1091
  this.interruptArmed = false;
798
1092
  this.queuedInput = undefined;
799
1093
  this.queuedAttachments = undefined;
800
- this.pendingUserImageEchoLabels = undefined;
1094
+ this.pendingUserImageEchoIds = undefined;
801
1095
  this.pendingUserImageEchoMessageId = undefined;
802
1096
  this.busy = false;
803
1097
  this.status = { ...this.status, phase: "ready", detail: undefined };
@@ -843,7 +1137,7 @@ export class WebRepl {
843
1137
  this.reduce(event);
844
1138
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
845
1139
  const messageId = this.pendingUserImageEchoMessageId;
846
- this.pendingUserImageEchoLabels = undefined;
1140
+ this.pendingUserImageEchoIds = undefined;
847
1141
  this.pendingUserImageEchoMessageId = undefined;
848
1142
  renderMessageImages(event.message, (line) => this.append(line), messageId);
849
1143
  this.broadcastSync();
@@ -853,6 +1147,10 @@ export class WebRepl {
853
1147
  this.queueDeltaStatus();
854
1148
  return;
855
1149
  }
1150
+ if (event.type === "context.compacted") {
1151
+ this.appendCompaction(event.compaction);
1152
+ return;
1153
+ }
856
1154
  if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
857
1155
  this.broadcastSync();
858
1156
  return;
@@ -903,10 +1201,11 @@ export class WebRepl {
903
1201
  const liveLineId = this.liveToolLineIds.get(toolUseId);
904
1202
  if (liveLineId === undefined)
905
1203
  return this.append(line);
906
- 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) });
907
1206
  this.liveToolLineIds.delete(toolUseId);
908
1207
  return liveLineId;
909
- });
1208
+ }, runtimeToolResultPresenter(this.runtime));
910
1209
  renderMessageImages(event.message, (line) => this.append(line));
911
1210
  return;
912
1211
  }
@@ -927,6 +1226,7 @@ export class WebRepl {
927
1226
  if (event.type === "tool.started") {
928
1227
  this.finalizeLiveLine(this.assistantLineId);
929
1228
  this.finalizeThinkingLine();
1229
+ this.assistantLineId = undefined;
930
1230
  const id = this.append({ ...formatToolUse(event.toolUse), live: true });
931
1231
  this.liveToolLineIds.set(event.toolUse.id, id);
932
1232
  return;
@@ -951,13 +1251,13 @@ export class WebRepl {
951
1251
  this.append({ kind: "error", text: event.error.message });
952
1252
  }
953
1253
  matchesPendingUserImageEcho(message) {
954
- const pending = this.pendingUserImageEchoLabels;
1254
+ const pending = this.pendingUserImageEchoIds;
955
1255
  if (!pending?.length || message.role !== "user")
956
1256
  return false;
957
- const labels = message.blocks
958
- .filter((block) => block.type === "image" && typeof block.label === "string")
959
- .map((block) => block.label);
960
- 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]);
961
1261
  }
962
1262
  async handleCommandOrPrompt(text, attachments = []) {
963
1263
  const command = parseReplCommand(text);
@@ -1059,11 +1359,11 @@ export class WebRepl {
1059
1359
  const promptPayload = buildWebPromptPayload(command.text, attachments);
1060
1360
  const optimisticMessageId = `web-user-${this.lineId + 1}`;
1061
1361
  this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
1062
- const optimisticImageLabels = (promptPayload.blocks ?? [])
1063
- .filter((block) => block.type === "image" && typeof block.label === "string")
1064
- .map((block) => block.label);
1065
- this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
1066
- 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;
1067
1367
  const runToken = ++this.foregroundRunToken;
1068
1368
  const abortController = new AbortController();
1069
1369
  this.activeAbortController = abortController;
@@ -1072,7 +1372,12 @@ export class WebRepl {
1072
1372
  this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
1073
1373
  const engine = this.runtime.engine;
1074
1374
  try {
1075
- 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
+ })) {
1076
1381
  if (this.foregroundRunToken !== runToken)
1077
1382
  continue;
1078
1383
  if (this.runtime.engine !== engine)
@@ -1103,7 +1408,7 @@ export class WebRepl {
1103
1408
  this.activeAbortController = undefined;
1104
1409
  this.interruptArmed = false;
1105
1410
  this.finalizeForegroundView();
1106
- this.pendingUserImageEchoLabels = undefined;
1411
+ this.pendingUserImageEchoIds = undefined;
1107
1412
  this.pendingUserImageEchoMessageId = undefined;
1108
1413
  const queuedText = this.queuedInput;
1109
1414
  const queuedAttach = this.queuedAttachments;
@@ -1136,7 +1441,10 @@ export class WebRepl {
1136
1441
  const metrics = await this.runtime.engine.contextMetrics();
1137
1442
  if (this.foregroundRunToken !== runToken)
1138
1443
  return;
1139
- 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)));
1140
1448
  this.handleEvent({ type: "context.metrics", metrics });
1141
1449
  }
1142
1450
  catch (error) {
@@ -1155,7 +1463,9 @@ export class WebRepl {
1155
1463
  }
1156
1464
  send(subscriber, event, data) {
1157
1465
  if (subscriber.blocked) {
1158
- if (event === "runtime.context")
1466
+ if (event === "client.reload")
1467
+ subscriber.pendingClientReload = data;
1468
+ else if (event === "runtime.context")
1159
1469
  subscriber.needsRuntimeContext = true;
1160
1470
  else
1161
1471
  subscriber.needsSync = true;
@@ -1170,6 +1480,11 @@ export class WebRepl {
1170
1480
  subscriber.blocked = false;
1171
1481
  if (subscriber.response.destroyed)
1172
1482
  return;
1483
+ if (subscriber.pendingClientReload) {
1484
+ const payload = subscriber.pendingClientReload;
1485
+ subscriber.pendingClientReload = undefined;
1486
+ this.send(subscriber, "client.reload", payload);
1487
+ }
1173
1488
  if (subscriber.needsSync) {
1174
1489
  subscriber.needsSync = false;
1175
1490
  this.send(subscriber, "sync", this.snapshot(false));
@@ -1245,6 +1560,39 @@ export class WebRepl {
1245
1560
  this.pendingDeltaOperations = [];
1246
1561
  this.pendingDeltaStatus = false;
1247
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
+ }
1248
1596
  broadcastSync() {
1249
1597
  this.discardPendingDeltaOperations();
1250
1598
  const payload = this.snapshot(false);
@@ -1252,6 +1600,10 @@ export class WebRepl {
1252
1600
  this.send(subscriber, "sync", payload);
1253
1601
  }
1254
1602
  }
1603
+ function appendBoundedText(current, incoming, limit) {
1604
+ const combined = current + incoming;
1605
+ return combined.length <= limit ? combined : combined.slice(-limit);
1606
+ }
1255
1607
  export function compactLineOperations(operations) {
1256
1608
  const compacted = [];
1257
1609
  for (const operation of operations) {
@@ -1284,20 +1636,67 @@ async function route(req, res, router) {
1284
1636
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1285
1637
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1286
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
+ }
1287
1645
  const scope = webRuntimeScopeFromUrl(url);
1288
1646
  const repl = await router.get(scope);
1289
1647
  if (req.method === "GET" && url.pathname === "/events")
1290
- return repl.subscribe(res);
1648
+ return repl.subscribe(res, router.clientInfo());
1291
1649
  if (req.method === "GET" && url.pathname === "/api/state")
1292
1650
  return sendJson(res, repl.snapshot(true));
1293
1651
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
1294
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
+ }
1295
1689
  if (req.method === "GET" && url.pathname === "/api/session-plugins")
1296
1690
  return sendJson(res, repl.sessionPlugins());
1297
1691
  if (req.method === "POST" && url.pathname === "/api/session-plugins") {
1298
1692
  const body = await readJsonBody(req);
1299
1693
  return sendJson(res, await repl.setSessionPlugins(body.overrides));
1300
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
+ }
1301
1700
  const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
1302
1701
  if (req.method === "GET" && imageMatch) {
1303
1702
  const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
@@ -1313,6 +1712,10 @@ async function route(req, res, router) {
1313
1712
  const body = await readJsonBody(req);
1314
1713
  return sendJson(res, await repl.setFastMode(body.enabled === true));
1315
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
+ }
1316
1719
  if (req.method === "POST" && url.pathname === "/api/submit") {
1317
1720
  const body = await readJsonBody(req);
1318
1721
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
@@ -1374,6 +1777,20 @@ function sendJson(res, value, status = 200) {
1374
1777
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1375
1778
  res.end(JSON.stringify(value));
1376
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
+ }
1377
1794
  function sendImage(res, payload) {
1378
1795
  res.writeHead(200, {
1379
1796
  "Content-Type": payload.mimeType,
@@ -1397,7 +1814,8 @@ function sanitizeWebAttachments(value) {
1397
1814
  if (!isRecord(item))
1398
1815
  return [];
1399
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") {
1400
- 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 }];
1401
1819
  }
1402
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)) {
1403
1821
  return [{
@@ -1435,14 +1853,44 @@ function initialLines(runtime, lineId) {
1435
1853
  lines.push({ id: ++lineId.current, ...line });
1436
1854
  return lines;
1437
1855
  }
1856
+ export function restoreWebHistoryLines(runtime) {
1857
+ return restoredHistoryLines(runtime).map((line) => ({ ...line }));
1858
+ }
1438
1859
  function restoredHistoryLines(runtime) {
1439
1860
  const lines = [];
1861
+ const historyToolLineIndexes = new Map();
1440
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
+ }
1441
1869
  lines.push(line);
1870
+ if (line.toolUseId && !line.titleStatus)
1871
+ historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
1442
1872
  return lines.length;
1443
1873
  };
1444
- for (const message of runtime.engine.getHistoryMessages())
1445
- 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
+ }
1446
1894
  return lines;
1447
1895
  }
1448
1896
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
@@ -1452,7 +1900,18 @@ async function resetStatus(runtime) {
1452
1900
  return initialStatus(runtime, await runtime.engine.contextMetrics());
1453
1901
  }
1454
1902
  function buildWebPromptPayload(displayText, attachments) {
1455
- 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;
1456
1915
  const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
1457
1916
  if (activeAttachments.length === 0) {
1458
1917
  const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
@@ -1467,7 +1926,7 @@ function buildWebPromptPayload(displayText, attachments) {
1467
1926
  break;
1468
1927
  }
1469
1928
  pushTextBlock(blocks, displayText.slice(cursor, next.index));
1470
- 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 });
1471
1930
  cursor = next.index + next.attachment.label.length;
1472
1931
  }
1473
1932
  pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
@@ -1515,6 +1974,8 @@ function pushTextBlock(blocks, text) {
1515
1974
  function reduceStatus(status, event) {
1516
1975
  if (event.type === "state")
1517
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 };
1518
1979
  if (event.type === "context.metrics")
1519
1980
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1520
1981
  if (event.type === "usage")
@@ -1843,6 +2304,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1843
2304
  if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
1844
2305
  return true;
1845
2306
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
2307
+ const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
1846
2308
  let rendered = false;
1847
2309
  for (const [blockIndex, block] of message.blocks.entries()) {
1848
2310
  if (block.type === "text") {
@@ -1859,7 +2321,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1859
2321
  const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1860
2322
  if (!line)
1861
2323
  continue;
1862
- append({ ...line, messageId: message.id, parentToolName });
2324
+ append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
1863
2325
  rendered = true;
1864
2326
  }
1865
2327
  else if (block.type === "thinking") {
@@ -1869,22 +2331,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1869
2331
  rendered = true;
1870
2332
  }
1871
2333
  else if (block.type === "tool_use" && options.includeToolUseBlocks) {
1872
- append({ ...formatToolUse(block), messageId: message.id, live: false });
2334
+ append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
1873
2335
  rendered = true;
1874
2336
  }
1875
2337
  else if (block.type === "tool_result") {
1876
- 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 });
1877
2339
  rendered = true;
1878
2340
  }
1879
2341
  }
1880
2342
  return rendered;
1881
2343
  }
1882
- function renderToolResultMessage(message, append) {
2344
+ function renderToolResultMessage(message, append, presentToolResult) {
1883
2345
  let rendered = false;
1884
2346
  for (const block of message.blocks) {
1885
2347
  if (block.type !== "tool_result")
1886
2348
  continue;
1887
- 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);
1888
2350
  rendered = true;
1889
2351
  }
1890
2352
  return rendered;
@@ -1908,13 +2370,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
1908
2370
  const kind = kindForRole(role);
1909
2371
  if (kind === "meta")
1910
2372
  return undefined;
2373
+ const resolution = resolveImageBlockDataResultSync(block);
1911
2374
  return {
1912
2375
  kind,
1913
2376
  text: block.label ?? `[image ${block.mimeType}]`,
1914
2377
  image: {
1915
- 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,
1916
2380
  label: block.label,
1917
2381
  mimeType: block.mimeType,
2382
+ available: resolution.available,
2383
+ error: resolution.available ? undefined : resolution.error,
2384
+ blockIndex,
1918
2385
  },
1919
2386
  };
1920
2387
  }
@@ -1966,20 +2433,187 @@ function titleForKind(kind) {
1966
2433
  function systemLine(text, summaryMaxLines) {
1967
2434
  return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
1968
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
+ }
1969
2489
  function thinkingLine(text, live = false) {
1970
2490
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1971
2491
  }
1972
2492
  function formatToolUse(toolUse) {
2493
+ const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
1973
2494
  if (toolUse.name === "plan" && isWebPlanPayload(toolUse.input))
1974
- 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 };
1975
2496
  if (toolUse.name === "image2")
1976
- 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 };
1977
2498
  const summary = summarizeToolUse(toolUse.name, toolUse.input);
1978
- 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;
1979
2575
  }
1980
- function formatToolResultLine(toolName, output, ok) {
1981
- const formatted = formatToolResult(toolName, output, ok);
1982
- 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);
1983
2617
  }
1984
2618
  function toolTitle(toolName, _phase) {
1985
2619
  const labels = {
@@ -1990,13 +2624,30 @@ function toolTitle(toolName, _phase) {
1990
2624
  expose_downloads: "文件下载",
1991
2625
  grep: "搜索文本",
1992
2626
  image2: "图片生成",
1993
- image_note: "记录图片说明",
2627
+ image_note: "记录图片",
1994
2628
  list: "列出文件",
1995
2629
  load_image: "读取图片",
1996
2630
  plan: "任务计划",
1997
2631
  read: "读取文件",
1998
2632
  search: "网络搜索",
1999
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: "子任务报告",
2000
2651
  };
2001
2652
  return labels[toolName] ?? toolName;
2002
2653
  }
@@ -2027,6 +2678,253 @@ function toolUsePurpose(toolName, input) {
2027
2678
  return `修改 ${path.basename(input.path)}`;
2028
2679
  return undefined;
2029
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
+ }
2030
2928
  function formatToolResult(toolName, output, ok) {
2031
2929
  if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
2032
2930
  return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
@@ -2042,36 +2940,15 @@ function formatToolResult(toolName, output, ok) {
2042
2940
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2043
2941
  if (toolName === "image2" && isRecord(output))
2044
2942
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
2045
- if (toolName === "expose_downloads")
2046
- 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 ? "资源已就绪" : "资源准备失败" };
2047
2946
  if (toolName === "plan" && isWebPlanPayload(output))
2048
2947
  return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
2049
2948
  if (typeof output === "string")
2050
2949
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2051
2950
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2052
2951
  }
2053
- function formatExposeDownloadsToolResult(output, ok) {
2054
- const downloads = extractDownloadEntries(output);
2055
- if (!ok)
2056
- return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
2057
- if (!downloads.length)
2058
- return "文件已准备好,请点击下载。";
2059
- return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
2060
- }
2061
- function formatDownloadEntries(downloads) {
2062
- return downloads.map((entry) => {
2063
- const filename = entry.filename || entry.name || "下载文件";
2064
- const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
2065
- return url ? `- ${filename}: ${url}` : `- ${filename}`;
2066
- }).join("\n");
2067
- }
2068
- function extractDownloadEntries(output) {
2069
- if (!isRecord(output))
2070
- return [];
2071
- const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
2072
- const downloads = candidates.find(Array.isArray);
2073
- return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
2074
- }
2075
2952
  function isEditToolOutput(value) {
2076
2953
  return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
2077
2954
  }
@@ -2376,8 +3253,9 @@ function formatUsageTotals(totals) {
2376
3253
  }
2377
3254
  function formatManualCompaction(result) {
2378
3255
  if (!result.changed)
2379
- return "No context compaction was needed.";
2380
- 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`;
2381
3259
  }
2382
3260
  function formatPureCompaction(result) {
2383
3261
  if (!result.changed)
@@ -2387,6 +3265,8 @@ function formatPureCompaction(result) {
2387
3265
  function formatNumber(value) {
2388
3266
  return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
2389
3267
  }
3268
+ const TERMINAL_OUTPUT_FLUSH_MS = 100;
3269
+ const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
2390
3270
  const THINKING_SUMMARY_MAX_LINES = 1000;
2391
3271
  const EXPANDED_SUMMARY_MAX_LINES = 1000;
2392
3272
  const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;