neoctl 0.2.26 → 0.2.28

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 (135) hide show
  1. package/README.md +36 -48
  2. package/dist/agents/agent-definition.js +1 -1
  3. package/dist/agents/agent-definition.js.map +1 -1
  4. package/dist/agents/agent-tool.js +1 -1
  5. package/dist/agents/agent-tool.js.map +1 -1
  6. package/dist/agents/smoke-agents.js +2 -2
  7. package/dist/agents/smoke-agents.js.map +1 -1
  8. package/dist/context/compaction.js +8 -5
  9. package/dist/context/compaction.js.map +1 -1
  10. package/dist/context/context-manager.d.ts +8 -9
  11. package/dist/context/context-manager.js +33 -41
  12. package/dist/context/context-manager.js.map +1 -1
  13. package/dist/context/prompts.js +1 -1
  14. package/dist/context/prompts.js.map +1 -1
  15. package/dist/context/smoke-context.js +30 -8
  16. package/dist/context/smoke-context.js.map +1 -1
  17. package/dist/core/context-metrics.d.ts +1 -1
  18. package/dist/core/context-metrics.js +5 -3
  19. package/dist/core/context-metrics.js.map +1 -1
  20. package/dist/core/message-pipeline.js +13 -21
  21. package/dist/core/message-pipeline.js.map +1 -1
  22. package/dist/core/prompt-cache-key.d.ts +10 -0
  23. package/dist/core/prompt-cache-key.js +34 -0
  24. package/dist/core/prompt-cache-key.js.map +1 -0
  25. package/dist/core/prompt-cache-telemetry.d.ts +2 -1
  26. package/dist/core/prompt-cache-telemetry.js +23 -1
  27. package/dist/core/prompt-cache-telemetry.js.map +1 -1
  28. package/dist/core/query-engine.d.ts +6 -5
  29. package/dist/core/query-engine.js +18 -18
  30. package/dist/core/query-engine.js.map +1 -1
  31. package/dist/core/query.d.ts +0 -1
  32. package/dist/core/query.js +22 -14
  33. package/dist/core/query.js.map +1 -1
  34. package/dist/core/run-agent.d.ts +0 -1
  35. package/dist/core/run-agent.js +0 -1
  36. package/dist/core/run-agent.js.map +1 -1
  37. package/dist/core/smoke-core-loop.js +27 -3
  38. package/dist/core/smoke-core-loop.js.map +1 -1
  39. package/dist/core/state.d.ts +0 -2
  40. package/dist/core/state.js +0 -1
  41. package/dist/core/state.js.map +1 -1
  42. package/dist/index.d.ts +1 -2
  43. package/dist/index.js +1 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/model/config.d.ts +2 -9
  46. package/dist/model/config.js +3 -31
  47. package/dist/model/config.js.map +1 -1
  48. package/dist/model/env.js +2 -10
  49. package/dist/model/env.js.map +1 -1
  50. package/dist/model/model-gateway.d.ts +0 -6
  51. package/dist/model/model-gateway.js.map +1 -1
  52. package/dist/model/model-metadata.json +0 -40
  53. package/dist/model/openai-adapter.d.ts +2 -1
  54. package/dist/model/openai-adapter.js +19 -0
  55. package/dist/model/openai-adapter.js.map +1 -1
  56. package/dist/model/openai-chat-mapper.d.ts +0 -1
  57. package/dist/model/openai-chat-mapper.js +13 -9
  58. package/dist/model/openai-chat-mapper.js.map +1 -1
  59. package/dist/model/openai-mappers.d.ts +4 -4
  60. package/dist/model/openai-mappers.js +23 -17
  61. package/dist/model/openai-mappers.js.map +1 -1
  62. package/dist/model/openai-responses-mapper.js +76 -35
  63. package/dist/model/openai-responses-mapper.js.map +1 -1
  64. package/dist/model/provider-factory.js +1 -22
  65. package/dist/model/provider-factory.js.map +1 -1
  66. package/dist/model/smoke-responses-mapper.js +82 -3
  67. package/dist/model/smoke-responses-mapper.js.map +1 -1
  68. package/dist/repl/commands.js +5 -2
  69. package/dist/repl/commands.js.map +1 -1
  70. package/dist/repl/index.js +85 -129
  71. package/dist/repl/index.js.map +1 -1
  72. package/dist/repl/run-command.d.ts +37 -0
  73. package/dist/repl/run-command.js +167 -0
  74. package/dist/repl/run-command.js.map +1 -0
  75. package/dist/repl/smoke-run-command.js +43 -0
  76. package/dist/repl/smoke-run-command.js.map +1 -0
  77. package/dist/secrets/smoke-secrets.js +5 -4
  78. package/dist/secrets/smoke-secrets.js.map +1 -1
  79. package/dist/session/session-export.d.ts +0 -1
  80. package/dist/session/session-export.js +0 -1
  81. package/dist/session/session-export.js.map +1 -1
  82. package/dist/session/smoke-session.js +6 -2
  83. package/dist/session/smoke-session.js.map +1 -1
  84. package/dist/tools/builtins/exec-process-manager.d.ts +78 -0
  85. package/dist/tools/builtins/exec-process-manager.js +394 -0
  86. package/dist/tools/builtins/exec-process-manager.js.map +1 -0
  87. package/dist/tools/builtins/exec-pty-host.d.ts +1 -0
  88. package/dist/tools/builtins/exec-pty-host.js +70 -0
  89. package/dist/tools/builtins/exec-pty-host.js.map +1 -0
  90. package/dist/tools/builtins/exec-tool.d.ts +16 -27
  91. package/dist/tools/builtins/exec-tool.js +153 -324
  92. package/dist/tools/builtins/exec-tool.js.map +1 -1
  93. package/dist/tools/builtins/image-generation-tool.d.ts +0 -4
  94. package/dist/tools/builtins/image-generation-tool.js +59 -185
  95. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  96. package/dist/tools/smoke-exec-process.d.ts +1 -0
  97. package/dist/tools/smoke-exec-process.js +42 -0
  98. package/dist/tools/smoke-exec-process.js.map +1 -0
  99. package/dist/tools/smoke-tool-system.js +28 -36
  100. package/dist/tools/smoke-tool-system.js.map +1 -1
  101. package/dist/types/events.d.ts +8 -0
  102. package/dist/types/messages.d.ts +5 -0
  103. package/dist/types/messages.js +14 -0
  104. package/dist/types/messages.js.map +1 -1
  105. package/dist/web/html.js +76 -29
  106. package/dist/web/html.js.map +1 -1
  107. package/dist/web/index.d.ts +98 -37
  108. package/dist/web/index.js +384 -140
  109. package/dist/web/index.js.map +1 -1
  110. package/dist/web/plan-payload.d.ts +14 -0
  111. package/dist/web/plan-payload.js +27 -0
  112. package/dist/web/plan-payload.js.map +1 -0
  113. package/dist/web/runtime-context-protocol.d.ts +46 -0
  114. package/dist/web/runtime-context-protocol.js +72 -0
  115. package/dist/web/runtime-context-protocol.js.map +1 -0
  116. package/dist/web/smoke-plan-payload.d.ts +1 -0
  117. package/dist/web/smoke-plan-payload.js +30 -0
  118. package/dist/web/smoke-plan-payload.js.map +1 -0
  119. package/dist/web/smoke-runtime-context-protocol.d.ts +1 -0
  120. package/dist/web/smoke-runtime-context-protocol.js +31 -0
  121. package/dist/web/smoke-runtime-context-protocol.js.map +1 -0
  122. package/dist/web/smoke-web-queue.d.ts +1 -0
  123. package/dist/web/smoke-web-queue.js +7 -0
  124. package/dist/web/smoke-web-queue.js.map +1 -0
  125. package/package.json +10 -5
  126. package/scripts/verify-ripgrep.cjs +8 -8
  127. package/dist/model/anthropic-adapter.d.ts +0 -30
  128. package/dist/model/anthropic-adapter.js +0 -106
  129. package/dist/model/anthropic-adapter.js.map +0 -1
  130. package/dist/model/anthropic-mapper.d.ts +0 -13
  131. package/dist/model/anthropic-mapper.js +0 -508
  132. package/dist/model/anthropic-mapper.js.map +0 -1
  133. package/dist/model/smoke-anthropic-mapper.js +0 -89
  134. package/dist/model/smoke-anthropic-mapper.js.map +0 -1
  135. /package/dist/{model/smoke-anthropic-mapper.d.ts → repl/smoke-run-command.d.ts} +0 -0
package/dist/web/index.js CHANGED
@@ -15,7 +15,8 @@ import { CommunicationLogger, LoggingModelGateway } from "../model/communication
15
15
  import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
16
16
  import { ToolRegistry } from "../tools/registry.js";
17
17
  import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
18
- import { createExecTool } from "../tools/builtins/exec-tool.js";
18
+ import { createExecTool, createWriteStdinTool } from "../tools/builtins/exec-tool.js";
19
+ import { ExecProcessManager } from "../tools/builtins/exec-process-manager.js";
19
20
  import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
20
21
  import { grepTool } from "../tools/builtins/grep-tool.js";
21
22
  import { searchTool } from "../tools/builtins/search-tool.js";
@@ -27,9 +28,12 @@ import { createTaskTools } from "../tasks/task-tools.js";
27
28
  import { TaskStore } from "../tasks/task-store.js";
28
29
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
29
30
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
+ import { DefaultContextManager } from "../context/context-manager.js";
30
32
  import { WEB_HTML } from "./html.js";
31
33
  import { openDirectory } from "../open-directory.js";
32
34
  import { resolveImageBlockDataSync } from "../core/image-storage.js";
35
+ import { isWebPlanPayload, serializeWebPlanPayload, webPlanBodyTitle } from "./plan-payload.js";
36
+ import { createWebRuntimeContextPayload } from "./runtime-context-protocol.js";
33
37
  const require = createRequire(import.meta.url);
34
38
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
35
39
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
@@ -80,6 +84,11 @@ function sumUsageTokens(left, right) {
80
84
  return undefined;
81
85
  return (left ?? 0) + (right ?? 0);
82
86
  }
87
+ // UI actions report structured results to the client. They must not append UiLine
88
+ // entries: the transcript is reserved for conversation, tool, and user-command events.
89
+ function actionFailure(errorCode, error) {
90
+ return { ok: false, errorCode, error };
91
+ }
83
92
  const DEFAULT_WEB_RUNTIME_KEY = "__default__";
84
93
  export async function runWebServer(argv = process.argv.slice(2), runtimeOptions = {}) {
85
94
  const options = parseWebArgs(argv);
@@ -114,10 +123,15 @@ export async function createWebRuntime(options = {}) {
114
123
  const communicationLogger = new CommunicationLogger();
115
124
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
116
125
  const taskStore = new TaskStore();
126
+ const pluginCatalog = normalizeWebRuntimePlugins(options.externalPlugins);
127
+ const pluginOverrides = normalizePluginOverrides(options.sessionPluginOverrides, pluginCatalog);
128
+ const activePlugins = activeWebRuntimePlugins(pluginCatalog, pluginOverrides);
117
129
  const tools = new ToolRegistry();
130
+ const execProcessManager = new ExecProcessManager();
118
131
  tools.register(editTool);
119
132
  tools.register(writeTool);
120
- tools.register(createExecTool({ taskStore }));
133
+ tools.register(createExecTool({ processManager: execProcessManager }));
134
+ tools.register(createWriteStdinTool(execProcessManager));
121
135
  tools.register(listDirectoryTool);
122
136
  tools.register(readFileTool);
123
137
  tools.register(grepTool);
@@ -128,13 +142,16 @@ export async function createWebRuntime(options = {}) {
128
142
  tools.register(planTool);
129
143
  for (const tool of options.externalTools ?? [])
130
144
  tools.register(tool);
145
+ for (const plugin of activePlugins)
146
+ for (const tool of plugin.tools)
147
+ tools.register(tool);
131
148
  const agentRuntime = { modelGateway, tools, taskStore };
132
149
  tools.register(createAgentTool(agentRuntime));
133
150
  const resumeHandler = async (taskId, directive) => {
134
151
  const dummyContext = {
135
152
  agentId: "main",
136
153
  tools,
137
- appState: new InMemoryAppState("main"),
154
+ appState: new InMemoryAppState("main", options.cwd),
138
155
  emit: () => undefined,
139
156
  };
140
157
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
@@ -144,15 +161,21 @@ export async function createWebRuntime(options = {}) {
144
161
  const appPromptStore = new InMemoryAppPromptStore();
145
162
  const engine = new QueryEngine({
146
163
  agentId: options.agentId ?? "main",
164
+ cwd: options.cwd,
147
165
  model: modelConfig?.model,
148
- fallbackModel: modelConfig?.fallbackModel,
149
166
  reasoning: modelConfig?.defaultReasoning,
150
167
  queryOrigin: "web",
151
168
  modelGateway,
152
169
  tools,
170
+ contextManagerFactory: (cwd) => new DefaultContextManager({ cwd }),
171
+ additionalPromptSections: [
172
+ ...(options.externalPromptSections ?? []),
173
+ ...activePlugins.flatMap((plugin) => plugin.promptSections ?? []),
174
+ ],
153
175
  appPromptStore,
154
176
  taskNotificationSource: createTaskNotificationSource(taskStore),
155
177
  commands: replCommandDefinitions.map((command) => command.usage),
178
+ plugins: [...(options.plugins ?? []), ...activePlugins.map((plugin) => plugin.id)],
156
179
  session: {
157
180
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
158
181
  sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
@@ -171,13 +194,45 @@ export async function createWebRuntime(options = {}) {
171
194
  agentRuntime,
172
195
  usage: new SessionUsageTracker(),
173
196
  taskStore,
197
+ execProcessManager,
174
198
  tools,
175
199
  initialMetrics,
176
200
  defaultReasoning: modelConfig?.defaultReasoning,
177
201
  envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
178
202
  envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
203
+ pluginSupport: pluginCatalog.length ? {
204
+ catalog: pluginCatalog,
205
+ overrides: pluginOverrides,
206
+ basePromptSections: [...(options.externalPromptSections ?? [])],
207
+ basePluginIds: [...(options.plugins ?? [])],
208
+ persist: options.persistSessionPluginOverrides,
209
+ resolve: options.resolveSessionPluginOverrides,
210
+ } : undefined,
179
211
  };
180
212
  }
213
+ function normalizeWebRuntimePlugins(value) {
214
+ if (!Array.isArray(value))
215
+ return [];
216
+ const plugins = value.map((plugin) => ({
217
+ ...plugin,
218
+ id: plugin.id.trim(),
219
+ name: plugin.name.trim() || plugin.id.trim(),
220
+ version: plugin.version.trim() || "0.0.0",
221
+ tools: [...plugin.tools],
222
+ promptSections: [...(plugin.promptSections ?? [])],
223
+ })).filter((plugin) => plugin.id);
224
+ plugins.sort((left, right) => left.id.localeCompare(right.id));
225
+ if (new Set(plugins.map((plugin) => plugin.id)).size !== plugins.length)
226
+ throw new Error("duplicate web runtime plugin id");
227
+ return plugins;
228
+ }
229
+ function normalizePluginOverrides(value, catalog) {
230
+ const available = new Set(catalog.map((plugin) => plugin.id));
231
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([id, enabled]) => available.has(id) && typeof enabled === "boolean"));
232
+ }
233
+ function activeWebRuntimePlugins(catalog, overrides) {
234
+ return catalog.filter((plugin) => plugin.globallyEnabled && overrides[plugin.id] !== false);
235
+ }
181
236
  function createTaskNotificationSource(taskStore) {
182
237
  return {
183
238
  collectUnnotifiedCompletions() {
@@ -200,7 +255,7 @@ function syncImageGenerationTool(runtime, provider) {
200
255
  runtime.tools.register(createOpenAIImageGenerationTool());
201
256
  }
202
257
  function formatCreatedEnvNotice(dotEnvPath) {
203
- return `Created default config file: ${dotEnvPath}\nSet MODEL_PROVIDER and the matching provider section (OPENAI_API_KEY or ANTHROPIC_API_KEY), then restart neo.`;
258
+ return `Created default config file: ${dotEnvPath}\nSet OPENAI_API_KEY, then restart neo.`;
204
259
  }
205
260
  function parseResumeFlag(value) {
206
261
  if (!value)
@@ -268,6 +323,9 @@ export class WebRepl {
268
323
  foregroundRun;
269
324
  foregroundRunToken = 0;
270
325
  pendingUserImageEchoLabels;
326
+ pendingUserImageEchoMessageId;
327
+ runtimeContextRevision = 0;
328
+ runtimeContextPublishQueue = Promise.resolve();
271
329
  backgroundSessionRuns = new Map();
272
330
  suppressReattachedStreaming = new Set();
273
331
  backgroundTaskCount;
@@ -275,15 +333,19 @@ export class WebRepl {
275
333
  this.runtime = runtime;
276
334
  this.lines = initialLines(runtime, { current: 0 });
277
335
  this.status = initialStatus(runtime);
278
- this.backgroundTaskCount = runtime.taskStore.activeCount();
336
+ this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
279
337
  runtime.taskStore.subscribe(() => {
280
- this.backgroundTaskCount = runtime.taskStore.activeCount();
338
+ this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
339
+ this.broadcastSync();
340
+ });
341
+ runtime.execProcessManager.subscribe(() => {
342
+ this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
281
343
  this.broadcastSync();
282
344
  });
283
345
  runtime.engine.onSessionTitleChange(() => this.broadcastSync());
284
346
  }
285
347
  subscribe(res) {
286
- const subscriber = { response: res, blocked: false, needsSync: false };
348
+ const subscriber = { response: res, blocked: false, needsSync: false, needsRuntimeContext: false };
287
349
  this.subscribers.add(subscriber);
288
350
  res.writeHead(200, {
289
351
  "Content-Type": "text/event-stream; charset=utf-8",
@@ -292,6 +354,7 @@ export class WebRepl {
292
354
  "X-Accel-Buffering": "no",
293
355
  });
294
356
  this.send(subscriber, "sync", this.snapshot(true));
357
+ this.publishRuntimeContext([subscriber]);
295
358
  reqKeepAlive(res);
296
359
  res.on("close", () => this.subscribers.delete(subscriber));
297
360
  }
@@ -312,6 +375,89 @@ export class WebRepl {
312
375
  interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
313
376
  };
314
377
  }
378
+ async runtimeContext() {
379
+ return this.buildRuntimeContext(this.runtimeContextRevision);
380
+ }
381
+ sessionPlugins() {
382
+ const support = this.runtime.pluginSupport;
383
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
384
+ if (!support)
385
+ return { sessionId, busy: this.busy, items: [] };
386
+ return {
387
+ sessionId,
388
+ busy: this.busy,
389
+ items: support.catalog.map((plugin) => {
390
+ const override = support.overrides[plugin.id];
391
+ return {
392
+ id: plugin.id,
393
+ name: plugin.name,
394
+ version: plugin.version,
395
+ globallyEnabled: plugin.globallyEnabled,
396
+ mode: override === undefined ? "inherit" : override ? "enabled" : "disabled",
397
+ effectiveEnabled: plugin.globallyEnabled && override !== false,
398
+ tools: plugin.tools.map((tool) => tool.name),
399
+ promptSections: plugin.promptSections?.length ?? 0,
400
+ };
401
+ }),
402
+ };
403
+ }
404
+ async setSessionPlugins(value) {
405
+ if (this.busy)
406
+ return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
407
+ if (!isRecord(value))
408
+ return actionFailure("INVALID_REQUEST", "plugin overrides must be an object");
409
+ const support = this.runtime.pluginSupport;
410
+ if (!support)
411
+ return actionFailure("PLUGIN_NOT_CONFIGURED", "web plugins are not configured");
412
+ const available = new Set(support.catalog.map((plugin) => plugin.id));
413
+ const overrides = {};
414
+ for (const [id, mode] of Object.entries(value)) {
415
+ if (!available.has(id))
416
+ return actionFailure("PLUGIN_INVALID", `unknown web plugin: ${id}`);
417
+ if (mode === "enabled" || mode === true)
418
+ overrides[id] = true;
419
+ else if (mode === "disabled" || mode === false)
420
+ overrides[id] = false;
421
+ else if (mode !== "inherit" && mode !== null && mode !== undefined)
422
+ return actionFailure("PLUGIN_INVALID", `invalid plugin mode: ${id}`);
423
+ }
424
+ try {
425
+ await this.applySessionPluginOverrides(overrides, true);
426
+ return { ok: true, state: this.sessionPlugins() };
427
+ }
428
+ catch (error) {
429
+ return actionFailure("PLUGIN_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
430
+ }
431
+ }
432
+ async loadSessionPlugins(sessionId) {
433
+ const support = this.runtime.pluginSupport;
434
+ if (!support?.resolve || !sessionId)
435
+ return;
436
+ await this.applySessionPluginOverrides(await support.resolve(sessionId), false);
437
+ }
438
+ async applySessionPluginOverrides(overrides, persist) {
439
+ const support = this.runtime.pluginSupport;
440
+ if (!support)
441
+ return;
442
+ const normalized = normalizePluginOverrides(overrides, support.catalog);
443
+ for (const plugin of support.catalog)
444
+ for (const tool of plugin.tools)
445
+ this.runtime.tools.unregister(tool.name);
446
+ const activePlugins = activeWebRuntimePlugins(support.catalog, normalized);
447
+ for (const plugin of activePlugins)
448
+ for (const tool of plugin.tools)
449
+ this.runtime.tools.register(tool);
450
+ support.overrides = normalized;
451
+ this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
452
+ const sessionId = this.runtime.engine.snapshot().session?.sessionId;
453
+ if (persist && sessionId && support.persist)
454
+ await support.persist(sessionId, normalized);
455
+ const metrics = await this.runtime.engine.contextMetrics();
456
+ this.runtime.initialMetrics = metrics;
457
+ this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
458
+ this.broadcastSync();
459
+ this.publishRuntimeContext();
460
+ }
315
461
  async submit(text, attachments = []) {
316
462
  const trimmed = text.trim();
317
463
  if (!trimmed && attachments.length === 0)
@@ -324,14 +470,36 @@ export class WebRepl {
324
470
  await this.detachRunningForeground("session browser");
325
471
  }
326
472
  else if (this.busy) {
327
- this.queuedInput = text;
328
- this.queuedAttachments = attachments;
473
+ this.enqueueInput(text, attachments);
329
474
  this.broadcastSync();
330
475
  return { ok: true };
331
476
  }
332
477
  this.startRun(text, attachments);
333
478
  return { ok: true };
334
479
  }
480
+ submitImmediately(text, attachments = []) {
481
+ if (!this.busy) {
482
+ this.startRun(text, attachments);
483
+ return { ok: true, interrupted: false };
484
+ }
485
+ this.enqueueInput(text, attachments);
486
+ return this.sendQueuedNow();
487
+ }
488
+ sendQueuedNow() {
489
+ const queuedText = this.queuedInput;
490
+ const queuedAttachments = this.queuedAttachments ?? [];
491
+ if (queuedText === undefined)
492
+ return { ok: true, interrupted: false };
493
+ this.queuedInput = undefined;
494
+ this.queuedAttachments = undefined;
495
+ const interrupted = this.stopForegroundRun("Interrupted to send queued web input");
496
+ this.startRun(queuedText, queuedAttachments);
497
+ return { ok: true, interrupted };
498
+ }
499
+ enqueueInput(text, attachments) {
500
+ this.queuedInput = mergeWebQueuedInput(this.queuedInput, text);
501
+ this.queuedAttachments = [...(this.queuedAttachments ?? []), ...attachments].slice(0, 20);
502
+ }
335
503
  startRun(text, attachments = []) {
336
504
  const run = this.handleCommandOrPrompt(text, attachments).catch((error) => {
337
505
  this.append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
@@ -351,7 +519,7 @@ export class WebRepl {
351
519
  }
352
520
  async resumeSession(sessionId) {
353
521
  if (!sessionId)
354
- return { ok: false, error: "sessionId is required" };
522
+ return actionFailure("INVALID_REQUEST", "sessionId is required");
355
523
  try {
356
524
  const running = this.backgroundSessionRuns.get(sessionId);
357
525
  if (running) {
@@ -364,13 +532,13 @@ export class WebRepl {
364
532
  const snapshot = this.runtime.engine.snapshot().session;
365
533
  if (!snapshot)
366
534
  throw new Error("session transcripts are disabled");
367
- await this.refreshSessionView(systemLine(formatResume(snapshot)));
535
+ await this.loadSessionPlugins(snapshot.sessionId);
536
+ await this.refreshSessionView();
368
537
  return { ok: true };
369
538
  }
370
539
  catch (error) {
371
540
  const message = error instanceof Error ? error.message : String(error);
372
- this.append({ kind: "error", text: message });
373
- return { ok: false, error: message };
541
+ return actionFailure("SESSION_RESUME_FAILED", message);
374
542
  }
375
543
  }
376
544
  async newSession() {
@@ -381,16 +549,16 @@ export class WebRepl {
381
549
  const snapshot = this.runtime.engine.snapshot().session;
382
550
  if (!snapshot)
383
551
  throw new Error("session transcripts are disabled");
384
- await this.refreshSessionView(systemLine(`new session ${snapshot.sessionId}`));
552
+ await this.loadSessionPlugins(snapshot.sessionId);
553
+ await this.refreshSessionView();
385
554
  return { ok: true };
386
555
  }
387
556
  catch (error) {
388
557
  const message = error instanceof Error ? error.message : String(error);
389
- this.append({ kind: "error", text: message });
390
- return { ok: false, error: message };
558
+ return actionFailure("SESSION_CREATE_FAILED", message);
391
559
  }
392
560
  }
393
- async refreshSessionView(line) {
561
+ async refreshSessionView() {
394
562
  const metrics = await this.runtime.engine.contextMetrics();
395
563
  this.runtime.usage.reset();
396
564
  this.status = initialStatus(this.runtime, metrics);
@@ -401,24 +569,27 @@ export class WebRepl {
401
569
  this.thinkingLineId = undefined;
402
570
  this.finalizedThinkingLineId = undefined;
403
571
  this.liveToolLineIds.clear();
404
- if (line)
405
- this.append(line);
406
572
  this.broadcastSync();
573
+ this.publishRuntimeContext();
407
574
  }
408
575
  async deleteSession(sessionId) {
409
576
  if (!sessionId)
410
- return { ok: false, error: "sessionId is required" };
577
+ return actionFailure("INVALID_REQUEST", "sessionId is required");
578
+ if (this.runtime.engine.snapshot().session?.sessionId === sessionId) {
579
+ return actionFailure("SESSION_ACTIVE", "cannot delete the active session");
580
+ }
581
+ if (this.backgroundSessionRuns.has(sessionId)) {
582
+ return actionFailure("SESSION_RUNNING", "cannot delete a running session");
583
+ }
411
584
  try {
412
585
  const deleted = await this.runtime.engine.deleteSession(sessionId);
413
586
  if (!deleted)
414
- return { ok: false, error: `session not found: ${sessionId}` };
415
- this.append(systemLine(`deleted session ${sessionId}`));
587
+ return actionFailure("SESSION_NOT_FOUND", `session not found: ${sessionId}`);
416
588
  return { ok: true };
417
589
  }
418
590
  catch (error) {
419
591
  const message = error instanceof Error ? error.message : String(error);
420
- this.append({ kind: "error", text: message });
421
- return { ok: false, error: message };
592
+ return actionFailure("SESSION_DELETE_FAILED", message);
422
593
  }
423
594
  }
424
595
  appPrompt() {
@@ -446,13 +617,34 @@ export class WebRepl {
446
617
  });
447
618
  }
448
619
  this.broadcastSync();
620
+ this.publishRuntimeContext();
449
621
  return { ok: true, appPrompt };
450
622
  }
451
623
  catch (error) {
452
624
  const message = error instanceof Error ? error.message : String(error);
453
- return { ok: false, error: message };
625
+ return actionFailure("PROMPT_UPDATE_FAILED", message);
454
626
  }
455
627
  }
628
+ imagePayload(messageId, blockIndex) {
629
+ if (!messageId || !Number.isInteger(blockIndex) || blockIndex < 0)
630
+ 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))
644
+ return undefined;
645
+ const body = Buffer.from(base64, "base64");
646
+ return body.length ? { body, mimeType } : undefined;
647
+ }
456
648
  async setFastMode(enabled) {
457
649
  try {
458
650
  const fastMode = await this.runtime.engine.setFastMode(enabled);
@@ -460,7 +652,7 @@ export class WebRepl {
460
652
  return { ok: true, fastMode };
461
653
  }
462
654
  catch (error) {
463
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
655
+ return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
464
656
  }
465
657
  }
466
658
  loginForm(providerValue) {
@@ -470,11 +662,11 @@ export class WebRepl {
470
662
  async saveLogin(providerValue, values) {
471
663
  const provider = parseLoginProvider(providerValue);
472
664
  if (!provider)
473
- return { ok: false, error: "provider must be openai or anthropic" };
665
+ return actionFailure("LOGIN_INVALID", "provider must be openai");
474
666
  const payload = { ...createLoginFormPayload(this.runtime.envPath, provider), provider, values };
475
667
  const validationError = validateLoginFormPayload(payload);
476
668
  if (validationError)
477
- return { ok: false, error: validationError };
669
+ return actionFailure("LOGIN_INVALID", validationError);
478
670
  try {
479
671
  await saveLoginPayloadToEnv(payload);
480
672
  applyLoginPayloadToProcessEnv(payload);
@@ -484,18 +676,17 @@ export class WebRepl {
484
676
  const innerGateway = createModelGatewayFromConfig(config);
485
677
  this.runtime.modelGateway.setInner(innerGateway);
486
678
  this.runtime.agentRuntime.modelGateway = this.runtime.modelGateway;
487
- this.runtime.engine.setModelProvider({ modelGateway: this.runtime.modelGateway, model: config.model, fallbackModel: config.fallbackModel, reasoning: config.defaultReasoning });
679
+ this.runtime.engine.setModelProvider({ modelGateway: this.runtime.modelGateway, model: config.model, reasoning: config.defaultReasoning });
488
680
  syncImageGenerationTool(this.runtime, config.provider);
489
681
  this.runtime.defaultReasoning = config.defaultReasoning;
490
682
  const metrics = await this.runtime.engine.contextMetrics();
491
683
  this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
492
- this.append(systemLine(`Saved ${provider} login to ${this.runtime.envPath}\n${formatModelSettings(this.runtime.engine.getModelSettings(), this.runtime.defaultReasoning)}`, EXPANDED_SUMMARY_MAX_LINES));
684
+ this.publishRuntimeContext();
493
685
  return { ok: true };
494
686
  }
495
687
  catch (error) {
496
688
  const message = `Login save failed: ${error instanceof Error ? error.message : String(error)}`;
497
- this.append({ kind: "error", text: message });
498
- return { ok: false, error: message };
689
+ return actionFailure("LOGIN_SAVE_FAILED", message);
499
690
  }
500
691
  }
501
692
  interrupt() {
@@ -558,6 +749,8 @@ export class WebRepl {
558
749
  this.interruptArmed = false;
559
750
  this.queuedInput = undefined;
560
751
  this.queuedAttachments = undefined;
752
+ this.pendingUserImageEchoLabels = undefined;
753
+ this.pendingUserImageEchoMessageId = undefined;
561
754
  this.finalizeForegroundView();
562
755
  this.finalizeLiveToolLines();
563
756
  this.busy = false;
@@ -604,9 +797,10 @@ export class WebRepl {
604
797
  this.interruptArmed = false;
605
798
  this.queuedInput = undefined;
606
799
  this.queuedAttachments = undefined;
800
+ this.pendingUserImageEchoLabels = undefined;
801
+ this.pendingUserImageEchoMessageId = undefined;
607
802
  this.busy = false;
608
803
  this.status = { ...this.status, phase: "ready", detail: undefined };
609
- this.append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
610
804
  return true;
611
805
  }
612
806
  async reattachRunningSession(run) {
@@ -617,7 +811,7 @@ export class WebRepl {
617
811
  this.interruptArmed = false;
618
812
  this.foregroundRun = run.promise;
619
813
  this.suppressReattachedStreaming.add(run.engine);
620
- await this.refreshSessionView(systemLine(`reattached running session ${run.sessionId}`));
814
+ await this.refreshSessionView();
621
815
  this.setBusy(true);
622
816
  this.setStatus({ ...this.status, phase: "running", detail: "working" });
623
817
  }
@@ -648,7 +842,10 @@ export class WebRepl {
648
842
  handleEvent(event) {
649
843
  this.reduce(event);
650
844
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
845
+ const messageId = this.pendingUserImageEchoMessageId;
651
846
  this.pendingUserImageEchoLabels = undefined;
847
+ this.pendingUserImageEchoMessageId = undefined;
848
+ renderMessageImages(event.message, (line) => this.append(line), messageId);
652
849
  this.broadcastSync();
653
850
  return;
654
851
  }
@@ -777,6 +974,7 @@ export class WebRepl {
777
974
  this.runtime.usage.reset();
778
975
  this.status = await resetStatus(this.runtime);
779
976
  this.append(systemLine("transcript reset"));
977
+ this.publishRuntimeContext();
780
978
  return;
781
979
  }
782
980
  if (command.type === "state") {
@@ -831,6 +1029,7 @@ export class WebRepl {
831
1029
  this.append(await handleModelCommand(command, this.runtime));
832
1030
  const metrics = await this.runtime.engine.contextMetrics();
833
1031
  this.setStatus({ ...this.status, phase: "ready", detail: undefined, metrics, activityTick: this.status.activityTick + 1 });
1032
+ this.publishRuntimeContext();
834
1033
  }
835
1034
  finally {
836
1035
  this.setBusy(false);
@@ -858,13 +1057,13 @@ export class WebRepl {
858
1057
  return;
859
1058
  }
860
1059
  const promptPayload = buildWebPromptPayload(command.text, attachments);
861
- this.append({ kind: "user", text: promptPayload.displayText });
862
- for (const line of imageLinesForBlocks("user", promptPayload.blocks))
863
- this.append(line);
1060
+ const optimisticMessageId = `web-user-${this.lineId + 1}`;
1061
+ this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
864
1062
  const optimisticImageLabels = (promptPayload.blocks ?? [])
865
1063
  .filter((block) => block.type === "image" && typeof block.label === "string")
866
1064
  .map((block) => block.label);
867
1065
  this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
1066
+ this.pendingUserImageEchoMessageId = optimisticImageLabels.length ? optimisticMessageId : undefined;
868
1067
  const runToken = ++this.foregroundRunToken;
869
1068
  const abortController = new AbortController();
870
1069
  this.activeAbortController = abortController;
@@ -905,6 +1104,7 @@ export class WebRepl {
905
1104
  this.interruptArmed = false;
906
1105
  this.finalizeForegroundView();
907
1106
  this.pendingUserImageEchoLabels = undefined;
1107
+ this.pendingUserImageEchoMessageId = undefined;
908
1108
  const queuedText = this.queuedInput;
909
1109
  const queuedAttach = this.queuedAttachments;
910
1110
  this.queuedInput = undefined;
@@ -955,7 +1155,10 @@ export class WebRepl {
955
1155
  }
956
1156
  send(subscriber, event, data) {
957
1157
  if (subscriber.blocked) {
958
- subscriber.needsSync = true;
1158
+ if (event === "runtime.context")
1159
+ subscriber.needsRuntimeContext = true;
1160
+ else
1161
+ subscriber.needsSync = true;
959
1162
  return;
960
1163
  }
961
1164
  const eventId = ++this.eventSequence;
@@ -965,10 +1168,41 @@ export class WebRepl {
965
1168
  subscriber.blocked = true;
966
1169
  subscriber.response.once("drain", () => {
967
1170
  subscriber.blocked = false;
968
- if (!subscriber.needsSync || subscriber.response.destroyed)
1171
+ if (subscriber.response.destroyed)
969
1172
  return;
970
- subscriber.needsSync = false;
971
- this.send(subscriber, "sync", this.snapshot(false));
1173
+ if (subscriber.needsSync) {
1174
+ subscriber.needsSync = false;
1175
+ this.send(subscriber, "sync", this.snapshot(false));
1176
+ }
1177
+ if (subscriber.needsRuntimeContext) {
1178
+ subscriber.needsRuntimeContext = false;
1179
+ this.publishRuntimeContext([subscriber]);
1180
+ }
1181
+ });
1182
+ }
1183
+ async buildRuntimeContext(revision) {
1184
+ const snapshot = await this.runtime.engine.promptExportSnapshot();
1185
+ return createWebRuntimeContextPayload(snapshot, {
1186
+ revision,
1187
+ sessionId: this.runtime.engine.snapshot().session?.sessionId,
1188
+ });
1189
+ }
1190
+ publishRuntimeContext(targets) {
1191
+ this.runtimeContextPublishQueue = this.runtimeContextPublishQueue.then(async () => {
1192
+ const revision = ++this.runtimeContextRevision;
1193
+ const payload = await this.buildRuntimeContext(revision);
1194
+ const subscribers = targets ?? [...this.subscribers];
1195
+ for (const subscriber of subscribers) {
1196
+ if (!subscriber.response.destroyed && this.subscribers.has(subscriber))
1197
+ this.send(subscriber, "runtime.context", payload);
1198
+ }
1199
+ }).catch((error) => {
1200
+ const payload = { protocolVersion: 1, message: error instanceof Error ? error.message : String(error) };
1201
+ const subscribers = targets ?? [...this.subscribers];
1202
+ for (const subscriber of subscribers) {
1203
+ if (!subscriber.response.destroyed && this.subscribers.has(subscriber))
1204
+ this.send(subscriber, "runtime.context.error", payload);
1205
+ }
972
1206
  });
973
1207
  }
974
1208
  broadcastDelta(operation) {
@@ -1056,6 +1290,19 @@ async function route(req, res, router) {
1056
1290
  return repl.subscribe(res);
1057
1291
  if (req.method === "GET" && url.pathname === "/api/state")
1058
1292
  return sendJson(res, repl.snapshot(true));
1293
+ if (req.method === "GET" && url.pathname === "/api/runtime-context")
1294
+ return sendJson(res, await repl.runtimeContext());
1295
+ if (req.method === "GET" && url.pathname === "/api/session-plugins")
1296
+ return sendJson(res, repl.sessionPlugins());
1297
+ if (req.method === "POST" && url.pathname === "/api/session-plugins") {
1298
+ const body = await readJsonBody(req);
1299
+ return sendJson(res, await repl.setSessionPlugins(body.overrides));
1300
+ }
1301
+ const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
1302
+ if (req.method === "GET" && imageMatch) {
1303
+ const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
1304
+ return payload ? sendImage(res, payload) : sendJson(res, { error: "image not found" }, 404);
1305
+ }
1059
1306
  if (req.method === "GET" && url.pathname === "/api/app-prompt")
1060
1307
  return sendJson(res, repl.appPrompt());
1061
1308
  if (req.method === "POST" && url.pathname === "/api/app-prompt") {
@@ -1070,10 +1317,16 @@ async function route(req, res, router) {
1070
1317
  const body = await readJsonBody(req);
1071
1318
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
1072
1319
  }
1320
+ if (req.method === "POST" && url.pathname === "/api/submit-now") {
1321
+ const body = await readJsonBody(req);
1322
+ return sendJson(res, repl.submitImmediately(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
1323
+ }
1073
1324
  if (req.method === "POST" && url.pathname === "/api/interrupt")
1074
1325
  return sendJson(res, repl.interrupt());
1075
1326
  if (req.method === "POST" && url.pathname === "/api/queue/cancel")
1076
1327
  return sendJson(res, repl.cancelQueue());
1328
+ if (req.method === "POST" && url.pathname === "/api/queue/send-now")
1329
+ return sendJson(res, repl.sendQueuedNow());
1077
1330
  if (req.method === "GET" && url.pathname === "/api/sessions")
1078
1331
  return sendJson(res, await repl.listSessions());
1079
1332
  if (req.method === "POST" && url.pathname === "/api/sessions/resume") {
@@ -1095,7 +1348,7 @@ async function route(req, res, router) {
1095
1348
  sendJson(res, { error: "not found" }, 404);
1096
1349
  }
1097
1350
  catch (error) {
1098
- sendJson(res, { error: error instanceof Error ? error.message : String(error) }, 500);
1351
+ sendJson(res, { errorCode: "WEB_REQUEST_FAILED", error: error instanceof Error ? error.message : String(error) }, 500);
1099
1352
  }
1100
1353
  }
1101
1354
  function webRuntimeScopeFromUrl(url) {
@@ -1121,6 +1374,14 @@ function sendJson(res, value, status = 200) {
1121
1374
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1122
1375
  res.end(JSON.stringify(value));
1123
1376
  }
1377
+ function sendImage(res, payload) {
1378
+ res.writeHead(200, {
1379
+ "Content-Type": payload.mimeType,
1380
+ "Content-Length": String(payload.body.length),
1381
+ "Cache-Control": "private, max-age=31536000, immutable",
1382
+ });
1383
+ res.end(payload.body);
1384
+ }
1124
1385
  async function readJsonBody(req) {
1125
1386
  const chunks = [];
1126
1387
  for await (const chunk of req)
@@ -1132,10 +1393,22 @@ async function readJsonBody(req) {
1132
1393
  function sanitizeWebAttachments(value) {
1133
1394
  if (!Array.isArray(value))
1134
1395
  return [];
1135
- return value.filter((item) => {
1396
+ return value.slice(0, 20).flatMap((item) => {
1136
1397
  if (!isRecord(item))
1137
- return false;
1138
- return item.kind === "image" && typeof item.label === "string" && /^\[img#\d+\]$/.test(item.label) && typeof item.mimeType === "string" && item.mimeType.startsWith("image/") && typeof item.data === "string";
1398
+ return [];
1399
+ 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 }];
1401
+ }
1402
+ 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
+ return [{
1404
+ kind: "file",
1405
+ name: path.basename(item.name).slice(0, 180),
1406
+ mimeType: item.mimeType.slice(0, 160),
1407
+ size: Math.max(0, Math.round(item.size)),
1408
+ absolutePath: path.resolve(item.absolutePath),
1409
+ }];
1410
+ }
1411
+ return [];
1139
1412
  });
1140
1413
  }
1141
1414
  function webCatalog(runtime) {
@@ -1169,7 +1442,7 @@ function restoredHistoryLines(runtime) {
1169
1442
  return lines.length;
1170
1443
  };
1171
1444
  for (const message of runtime.engine.getHistoryMessages())
1172
- renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: false });
1445
+ renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: true });
1173
1446
  return lines;
1174
1447
  }
1175
1448
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
@@ -1179,9 +1452,12 @@ async function resetStatus(runtime) {
1179
1452
  return initialStatus(runtime, await runtime.engine.contextMetrics());
1180
1453
  }
1181
1454
  function buildWebPromptPayload(displayText, attachments) {
1182
- const activeAttachments = attachments.filter((attachment) => displayText.includes(attachment.label));
1183
- if (activeAttachments.length === 0)
1184
- return { text: displayText, displayText };
1455
+ const activeAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
1456
+ const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
1457
+ if (activeAttachments.length === 0) {
1458
+ const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
1459
+ return { text, displayText };
1460
+ }
1185
1461
  const blocks = [];
1186
1462
  let cursor = 0;
1187
1463
  while (cursor < displayText.length) {
@@ -1194,8 +1470,27 @@ function buildWebPromptPayload(displayText, attachments) {
1194
1470
  blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
1195
1471
  cursor = next.index + next.attachment.label.length;
1196
1472
  }
1473
+ pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
1197
1474
  const text = blocks.map((block) => block.type === "text" ? block.text : block.type === "image" ? block.label ?? "[image]" : "").join("");
1198
- return { text, displayText: text, blocks };
1475
+ return { text, displayText, blocks };
1476
+ }
1477
+ function buildWebFileAttachmentManifest(attachments) {
1478
+ if (attachments.length === 0)
1479
+ return "";
1480
+ return [
1481
+ "<<ATTACHMENT_MANIFEST>>",
1482
+ "Files attached by the user for this turn:",
1483
+ ...attachments.map((attachment, index) => [
1484
+ `- file${index + 1}: ${attachment.name || `attachment-${index + 1}`}`,
1485
+ ` path: ${attachment.absolutePath}`,
1486
+ ` mimeType: ${attachment.mimeType || "application/octet-stream"}`,
1487
+ ` size: ${attachment.size} bytes`,
1488
+ ].join("\n")),
1489
+ "<</ATTACHMENT_MANIFEST>>",
1490
+ ].join("\n");
1491
+ }
1492
+ export function mergeWebQueuedInput(current, next) {
1493
+ return [current?.trimEnd(), next.trim()].filter(Boolean).join("\n");
1199
1494
  }
1200
1495
  function nextWebAttachmentOccurrence(text, attachments, start) {
1201
1496
  let best;
@@ -1288,7 +1583,7 @@ async function handleModelCommand(command, runtime) {
1288
1583
  const innerGateway = createModelGatewayFromConfig(config);
1289
1584
  runtime.modelGateway.setInner(innerGateway);
1290
1585
  runtime.agentRuntime.modelGateway = runtime.modelGateway;
1291
- runtime.engine.setModelProvider({ modelGateway: runtime.modelGateway, model: config.model, fallbackModel: config.fallbackModel, reasoning: config.defaultReasoning });
1586
+ runtime.engine.setModelProvider({ modelGateway: runtime.modelGateway, model: config.model, reasoning: config.defaultReasoning });
1292
1587
  syncImageGenerationTool(runtime, config.provider);
1293
1588
  runtime.defaultReasoning = config.defaultReasoning;
1294
1589
  }
@@ -1340,18 +1635,18 @@ async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
1340
1635
  return { providerChanged: targetProvider !== currentProvider };
1341
1636
  }
1342
1637
  function currentModelProvider() {
1343
- return parseLoginProvider(process.env.MODEL_PROVIDER) ?? (process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai");
1638
+ return "openai";
1344
1639
  }
1345
1640
  function parseLoginProvider(value) {
1346
- if (value === "openai" || value === "anthropic")
1641
+ if (value === "openai")
1347
1642
  return value;
1348
1643
  return undefined;
1349
1644
  }
1350
- const LOGIN_PROVIDERS = ["openai", "anthropic"];
1645
+ const LOGIN_PROVIDERS = ["openai"];
1351
1646
  const SHARED_LOGIN_FIELDS = [
1352
1647
  { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
1353
1648
  { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
1354
- { key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
1649
+ { key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared" },
1355
1650
  { key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
1356
1651
  { key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
1357
1652
  { key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
@@ -1361,23 +1656,13 @@ const LOGIN_FIELD_DEFINITIONS = {
1361
1656
  { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
1362
1657
  { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
1363
1658
  { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.6" },
1364
- { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
1365
1659
  { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
1366
1660
  ...SHARED_LOGIN_FIELDS,
1367
1661
  ],
1368
- anthropic: [
1369
- { key: "apiKey", label: "API key", envKey: "ANTHROPIC_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-ant-..." },
1370
- { key: "baseUrl", label: "Base URL", envKey: "ANTHROPIC_BASE_URL", scope: "provider", placeholder: "https://api.anthropic.com" },
1371
- { key: "model", label: "Model", envKey: "ANTHROPIC_MODEL", scope: "provider", required: true, placeholder: "claude-sonnet-4-6" },
1372
- { key: "fallbackModel", label: "Fallback model", envKey: "ANTHROPIC_FALLBACK_MODEL", scope: "provider" },
1373
- { key: "version", label: "Anthropic version", envKey: "ANTHROPIC_VERSION", scope: "provider", placeholder: "2023-06-01" },
1374
- ...SHARED_LOGIN_FIELDS,
1375
- ],
1376
1662
  };
1377
1663
  const DEPRECATED_MODEL_ENV_KEYS = [
1378
1664
  "MODEL_API_KEY", "MODEL_BASE_URL", "MODEL_ID", "MODEL_FALLBACK_ID", "MODEL_ENDPOINT", "OPENAI_PROVIDER",
1379
1665
  "OPENAI_REASONING_EFFORT", "OPENAI_REASONING_SUMMARY", "OPENAI_MAX_OUTPUT_TOKENS", "OPENAI_TIMEOUT_MS", "OPENAI_STREAM_IDLE_TIMEOUT_MS", "OPENAI_MAX_RETRIES",
1380
- "ANTHROPIC_REASONING_EFFORT", "ANTHROPIC_REASONING_SUMMARY", "ANTHROPIC_MAX_OUTPUT_TOKENS", "ANTHROPIC_TIMEOUT_MS", "ANTHROPIC_STREAM_IDLE_TIMEOUT_MS", "ANTHROPIC_MAX_RETRIES",
1381
1666
  ];
1382
1667
  function createLoginFormPayload(envPath, provider) {
1383
1668
  const env = parseEnvFileSafe(envPath);
@@ -1402,19 +1687,13 @@ function loginValuesForProvider(provider, env) {
1402
1687
  values.endpoint = "auto";
1403
1688
  return values;
1404
1689
  }
1405
- function guessLoginProvider(env) {
1406
- if (env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)
1407
- return "anthropic";
1690
+ function guessLoginProvider(_env) {
1408
1691
  return currentModelProvider();
1409
1692
  }
1410
- function defaultBaseUrlForLoginProvider(provider) {
1411
- if (provider === "anthropic")
1412
- return "https://api.anthropic.com";
1693
+ function defaultBaseUrlForLoginProvider(_provider) {
1413
1694
  return "https://api.openai.com";
1414
1695
  }
1415
- function defaultModelForLoginProvider(provider) {
1416
- if (provider === "anthropic")
1417
- return "claude-sonnet-4-6";
1696
+ function defaultModelForLoginProvider(_provider) {
1418
1697
  return "gpt-5.6";
1419
1698
  }
1420
1699
  function validateLoginFormPayload(payload) {
@@ -1430,6 +1709,9 @@ function validateLoginFormPayload(payload) {
1430
1709
  if (value && !Number.isFinite(Number(value)))
1431
1710
  return `${fieldKey} must be a number.`;
1432
1711
  }
1712
+ if (payload.provider === "openai" && payload.values.endpoint === "chat" && payload.values.reasoningSummary?.trim()) {
1713
+ return "Reasoning summary requires the Responses API endpoint.";
1714
+ }
1433
1715
  return undefined;
1434
1716
  }
1435
1717
  async function saveLoginPayloadToEnv(payload) {
@@ -1464,9 +1746,7 @@ function stripEnvQuotes(value) {
1464
1746
  return value.slice(1, -1);
1465
1747
  return value;
1466
1748
  }
1467
- function modelEnvKeyForProvider(provider) {
1468
- if (provider === "anthropic")
1469
- return "ANTHROPIC_MODEL";
1749
+ function modelEnvKeyForProvider(_provider) {
1470
1750
  return "OPENAI_MODEL";
1471
1751
  }
1472
1752
  function envValueForReasoning(reasoning) {
@@ -1515,9 +1795,6 @@ function formatEnvValue(value) {
1515
1795
  return value;
1516
1796
  return JSON.stringify(value);
1517
1797
  }
1518
- function formatResume(snapshot) {
1519
- return `resumed session ${snapshot.sessionId}: ${snapshot.resumedMessages} messages from ${snapshot.transcriptPath}`;
1520
- }
1521
1798
  function applyEnvUpdatesToProcess(updates) {
1522
1799
  for (const [key, value] of Object.entries(updates)) {
1523
1800
  if (value === undefined)
@@ -1541,8 +1818,6 @@ function validateModelReasoningArgument(modelId, reasoning) {
1541
1818
  function formatModelSettings(settings, defaultReasoning) {
1542
1819
  const window = resolveContextWindowTokens(settings.model);
1543
1820
  const lines = ["Model settings:", ` Model: ${settings.model ?? "<provider default>"}`];
1544
- if (settings.fallbackModel)
1545
- lines.push(` Fallback: ${settings.fallbackModel}`);
1546
1821
  lines.push(` Reasoning effort: ${formatReasoningSetting(settings.reasoning)}`);
1547
1822
  if (defaultReasoning?.effort)
1548
1823
  lines.push(` Env default reasoning: ${defaultReasoning.effort}`);
@@ -1569,36 +1844,36 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1569
1844
  return true;
1570
1845
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
1571
1846
  let rendered = false;
1572
- for (const block of message.blocks) {
1847
+ for (const [blockIndex, block] of message.blocks.entries()) {
1573
1848
  if (block.type === "text") {
1574
1849
  const kind = kindForRole(message.role);
1575
1850
  if (kind === "meta")
1576
1851
  continue;
1577
1852
  if (kind === "system")
1578
- append({ kind, title: titleForRole(message.role), text: block.text, previewStyle: "summary" });
1853
+ append({ kind, title: titleForRole(message.role), text: block.text, messageId: message.id, previewStyle: "summary" });
1579
1854
  else
1580
- append({ kind, text: block.text });
1855
+ append({ kind, text: block.text, messageId: message.id });
1581
1856
  rendered = true;
1582
1857
  }
1583
1858
  else if (block.type === "image") {
1584
- const line = imageLineForBlock(message.role, block);
1859
+ const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1585
1860
  if (!line)
1586
1861
  continue;
1587
- append({ ...line, parentToolName });
1862
+ append({ ...line, messageId: message.id, parentToolName });
1588
1863
  rendered = true;
1589
1864
  }
1590
1865
  else if (block.type === "thinking") {
1591
1866
  if (options.includeThinkingBlocks === false)
1592
1867
  continue;
1593
- append(thinkingLine(block.text));
1868
+ append({ ...thinkingLine(block.text), messageId: message.id });
1594
1869
  rendered = true;
1595
1870
  }
1596
1871
  else if (block.type === "tool_use" && options.includeToolUseBlocks) {
1597
- append({ ...formatToolUse(block), live: false });
1872
+ append({ ...formatToolUse(block), messageId: message.id, live: false });
1598
1873
  rendered = true;
1599
1874
  }
1600
1875
  else if (block.type === "tool_result") {
1601
- append(formatToolResultLine(block.name, block.output, block.ok));
1876
+ append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
1602
1877
  rendered = true;
1603
1878
  }
1604
1879
  }
@@ -1609,35 +1884,27 @@ function renderToolResultMessage(message, append) {
1609
1884
  for (const block of message.blocks) {
1610
1885
  if (block.type !== "tool_result")
1611
1886
  continue;
1612
- append({ ...formatToolResultLine(block.name, block.output, block.ok), toolUseId: block.toolUseId }, block.toolUseId);
1887
+ append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
1613
1888
  rendered = true;
1614
1889
  }
1615
1890
  return rendered;
1616
1891
  }
1617
- function renderMessageImages(message, append) {
1892
+ function renderMessageImages(message, append, messageId = message.id) {
1618
1893
  let rendered = false;
1619
1894
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
1620
1895
  const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
1621
- for (const block of message.blocks) {
1896
+ for (const [blockIndex, block] of message.blocks.entries()) {
1622
1897
  if (block.type !== "image")
1623
1898
  continue;
1624
- const line = imageLineForBlock(message.role, block);
1899
+ const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1625
1900
  if (!line)
1626
1901
  continue;
1627
- append({ ...line, parentToolName, parentToolUseId });
1902
+ append({ ...line, messageId, parentToolName, parentToolUseId });
1628
1903
  rendered = true;
1629
1904
  }
1630
1905
  return rendered;
1631
1906
  }
1632
- function imageLinesForBlocks(role, blocks) {
1633
- if (!blocks?.length)
1634
- return [];
1635
- return blocks
1636
- .filter((block) => block.type === "image")
1637
- .map((block) => imageLineForBlock(role, block))
1638
- .filter((line) => Boolean(line));
1639
- }
1640
- function imageLineForBlock(role, block) {
1907
+ function imageLineForBlock(role, block, messageId, blockIndex) {
1641
1908
  const kind = kindForRole(role);
1642
1909
  if (kind === "meta")
1643
1910
  return undefined;
@@ -1645,20 +1912,12 @@ function imageLineForBlock(role, block) {
1645
1912
  kind,
1646
1913
  text: block.label ?? `[image ${block.mimeType}]`,
1647
1914
  image: {
1648
- src: imageBlockToDataUrl(block),
1915
+ src: `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`,
1649
1916
  label: block.label,
1650
1917
  mimeType: block.mimeType,
1651
1918
  },
1652
1919
  };
1653
1920
  }
1654
- function imageBlockToDataUrl(block) {
1655
- const data = resolveImageBlockDataSync(block);
1656
- if (!data)
1657
- return "";
1658
- if (data.startsWith("data:"))
1659
- return data;
1660
- return `data:${block.mimeType};base64,${data}`;
1661
- }
1662
1921
  function assistantText(message) {
1663
1922
  const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1664
1923
  return text.length > 0 ? text : undefined;
@@ -1711,8 +1970,8 @@ function thinkingLine(text, live = false) {
1711
1970
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1712
1971
  }
1713
1972
  function formatToolUse(toolUse) {
1714
- if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1715
- return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
1973
+ 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 };
1716
1975
  if (toolUse.name === "image2")
1717
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 };
1718
1977
  const summary = summarizeToolUse(toolUse.name, toolUse.input);
@@ -1726,7 +1985,8 @@ function toolTitle(toolName, _phase) {
1726
1985
  const labels = {
1727
1986
  agent: "子任务",
1728
1987
  edit: "编辑文件",
1729
- exec: "执行命令",
1988
+ exec_command: "执行命令",
1989
+ write_stdin: "终端交互",
1730
1990
  expose_downloads: "文件下载",
1731
1991
  grep: "搜索文本",
1732
1992
  image2: "图片生成",
@@ -1767,24 +2027,6 @@ function toolUsePurpose(toolName, input) {
1767
2027
  return `修改 ${path.basename(input.path)}`;
1768
2028
  return undefined;
1769
2029
  }
1770
- function isPlanToolPayload(value) {
1771
- if (!isRecord(value) || !Array.isArray(value.items))
1772
- return false;
1773
- return value.items.every((item) => isRecord(item) && typeof item.description === "string" && (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
1774
- }
1775
- function planToolBodyTitle(payload) {
1776
- const title = payload.title?.trim();
1777
- return title ? title : undefined;
1778
- }
1779
- function formatPlanToolPayload(payload) {
1780
- const sections = [];
1781
- if (payload.summary?.trim())
1782
- sections.push(payload.summary.trim());
1783
- if (payload.note?.trim())
1784
- sections.push(payload.note.trim());
1785
- sections.push(payload.items.map((item) => item.status === "completed" ? `- ~~${item.description.trim()}~~` : item.status === "in_progress" ? `- ▶ ${item.description.trim()}` : `- ${item.description.trim()}`).join("\n"));
1786
- return sections.filter(Boolean).join("\n");
1787
- }
1788
2030
  function formatToolResult(toolName, output, ok) {
1789
2031
  if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
1790
2032
  return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
@@ -1802,8 +2044,8 @@ function formatToolResult(toolName, output, ok) {
1802
2044
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
1803
2045
  if (toolName === "expose_downloads")
1804
2046
  return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
1805
- if (toolName === "plan" && isPlanToolPayload(output))
1806
- return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
2047
+ if (toolName === "plan" && isWebPlanPayload(output))
2048
+ return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
1807
2049
  if (typeof output === "string")
1808
2050
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1809
2051
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
@@ -1879,12 +2121,14 @@ function diffLineMarker(line) {
1879
2121
  return marker === "+" || marker === "-" || marker === " " ? marker : undefined;
1880
2122
  }
1881
2123
  function isExecOutput(value) {
1882
- return isRecord(value) && typeof value.command === "string" && typeof value.durationMs === "number";
2124
+ return isRecord(value) && typeof value.command === "string" && typeof value.duration_ms === "number";
1883
2125
  }
1884
2126
  function formatExecToolResult(output, ok) {
1885
- const status = output.timedOut ? "已超时" : ok ? "已完成" : "执行失败";
2127
+ const status = output.status === "running" ? "运行中" : output.timed_out ? "已超时" : ok ? "已完成" : "执行失败";
1886
2128
  const description = typeof output.description === "string" ? output.description.trim() : "";
1887
- const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.durationMs}ms`];
2129
+ const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.duration_ms}ms`];
2130
+ if (output.status === "running" && typeof output.session_id === "string")
2131
+ lines.push(`会话:${output.session_id}`);
1888
2132
  const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
1889
2133
  const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
1890
2134
  if (stdout)