neoctl 0.2.27 → 0.2.29

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 +35 -47
  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 +94 -37
  108. package/dist/web/index.js +345 -117
  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 +7 -2
  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,11 +617,12 @@ 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
  }
456
628
  imagePayload(messageId, blockIndex) {
@@ -480,7 +652,7 @@ export class WebRepl {
480
652
  return { ok: true, fastMode };
481
653
  }
482
654
  catch (error) {
483
- 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));
484
656
  }
485
657
  }
486
658
  loginForm(providerValue) {
@@ -490,11 +662,11 @@ export class WebRepl {
490
662
  async saveLogin(providerValue, values) {
491
663
  const provider = parseLoginProvider(providerValue);
492
664
  if (!provider)
493
- return { ok: false, error: "provider must be openai or anthropic" };
665
+ return actionFailure("LOGIN_INVALID", "provider must be openai");
494
666
  const payload = { ...createLoginFormPayload(this.runtime.envPath, provider), provider, values };
495
667
  const validationError = validateLoginFormPayload(payload);
496
668
  if (validationError)
497
- return { ok: false, error: validationError };
669
+ return actionFailure("LOGIN_INVALID", validationError);
498
670
  try {
499
671
  await saveLoginPayloadToEnv(payload);
500
672
  applyLoginPayloadToProcessEnv(payload);
@@ -504,18 +676,17 @@ export class WebRepl {
504
676
  const innerGateway = createModelGatewayFromConfig(config);
505
677
  this.runtime.modelGateway.setInner(innerGateway);
506
678
  this.runtime.agentRuntime.modelGateway = this.runtime.modelGateway;
507
- 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 });
508
680
  syncImageGenerationTool(this.runtime, config.provider);
509
681
  this.runtime.defaultReasoning = config.defaultReasoning;
510
682
  const metrics = await this.runtime.engine.contextMetrics();
511
683
  this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
512
- 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();
513
685
  return { ok: true };
514
686
  }
515
687
  catch (error) {
516
688
  const message = `Login save failed: ${error instanceof Error ? error.message : String(error)}`;
517
- this.append({ kind: "error", text: message });
518
- return { ok: false, error: message };
689
+ return actionFailure("LOGIN_SAVE_FAILED", message);
519
690
  }
520
691
  }
521
692
  interrupt() {
@@ -578,6 +749,8 @@ export class WebRepl {
578
749
  this.interruptArmed = false;
579
750
  this.queuedInput = undefined;
580
751
  this.queuedAttachments = undefined;
752
+ this.pendingUserImageEchoLabels = undefined;
753
+ this.pendingUserImageEchoMessageId = undefined;
581
754
  this.finalizeForegroundView();
582
755
  this.finalizeLiveToolLines();
583
756
  this.busy = false;
@@ -624,9 +797,10 @@ export class WebRepl {
624
797
  this.interruptArmed = false;
625
798
  this.queuedInput = undefined;
626
799
  this.queuedAttachments = undefined;
800
+ this.pendingUserImageEchoLabels = undefined;
801
+ this.pendingUserImageEchoMessageId = undefined;
627
802
  this.busy = false;
628
803
  this.status = { ...this.status, phase: "ready", detail: undefined };
629
- this.append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
630
804
  return true;
631
805
  }
632
806
  async reattachRunningSession(run) {
@@ -637,7 +811,7 @@ export class WebRepl {
637
811
  this.interruptArmed = false;
638
812
  this.foregroundRun = run.promise;
639
813
  this.suppressReattachedStreaming.add(run.engine);
640
- await this.refreshSessionView(systemLine(`reattached running session ${run.sessionId}`));
814
+ await this.refreshSessionView();
641
815
  this.setBusy(true);
642
816
  this.setStatus({ ...this.status, phase: "running", detail: "working" });
643
817
  }
@@ -668,8 +842,10 @@ export class WebRepl {
668
842
  handleEvent(event) {
669
843
  this.reduce(event);
670
844
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
845
+ const messageId = this.pendingUserImageEchoMessageId;
671
846
  this.pendingUserImageEchoLabels = undefined;
672
- renderMessageImages(event.message, (line) => this.append(line));
847
+ this.pendingUserImageEchoMessageId = undefined;
848
+ renderMessageImages(event.message, (line) => this.append(line), messageId);
673
849
  this.broadcastSync();
674
850
  return;
675
851
  }
@@ -798,6 +974,7 @@ export class WebRepl {
798
974
  this.runtime.usage.reset();
799
975
  this.status = await resetStatus(this.runtime);
800
976
  this.append(systemLine("transcript reset"));
977
+ this.publishRuntimeContext();
801
978
  return;
802
979
  }
803
980
  if (command.type === "state") {
@@ -852,6 +1029,7 @@ export class WebRepl {
852
1029
  this.append(await handleModelCommand(command, this.runtime));
853
1030
  const metrics = await this.runtime.engine.contextMetrics();
854
1031
  this.setStatus({ ...this.status, phase: "ready", detail: undefined, metrics, activityTick: this.status.activityTick + 1 });
1032
+ this.publishRuntimeContext();
855
1033
  }
856
1034
  finally {
857
1035
  this.setBusy(false);
@@ -879,11 +1057,13 @@ export class WebRepl {
879
1057
  return;
880
1058
  }
881
1059
  const promptPayload = buildWebPromptPayload(command.text, attachments);
882
- this.append({ kind: "user", text: promptPayload.displayText });
1060
+ const optimisticMessageId = `web-user-${this.lineId + 1}`;
1061
+ this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
883
1062
  const optimisticImageLabels = (promptPayload.blocks ?? [])
884
1063
  .filter((block) => block.type === "image" && typeof block.label === "string")
885
1064
  .map((block) => block.label);
886
1065
  this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
1066
+ this.pendingUserImageEchoMessageId = optimisticImageLabels.length ? optimisticMessageId : undefined;
887
1067
  const runToken = ++this.foregroundRunToken;
888
1068
  const abortController = new AbortController();
889
1069
  this.activeAbortController = abortController;
@@ -924,6 +1104,7 @@ export class WebRepl {
924
1104
  this.interruptArmed = false;
925
1105
  this.finalizeForegroundView();
926
1106
  this.pendingUserImageEchoLabels = undefined;
1107
+ this.pendingUserImageEchoMessageId = undefined;
927
1108
  const queuedText = this.queuedInput;
928
1109
  const queuedAttach = this.queuedAttachments;
929
1110
  this.queuedInput = undefined;
@@ -974,7 +1155,10 @@ export class WebRepl {
974
1155
  }
975
1156
  send(subscriber, event, data) {
976
1157
  if (subscriber.blocked) {
977
- subscriber.needsSync = true;
1158
+ if (event === "runtime.context")
1159
+ subscriber.needsRuntimeContext = true;
1160
+ else
1161
+ subscriber.needsSync = true;
978
1162
  return;
979
1163
  }
980
1164
  const eventId = ++this.eventSequence;
@@ -984,10 +1168,41 @@ export class WebRepl {
984
1168
  subscriber.blocked = true;
985
1169
  subscriber.response.once("drain", () => {
986
1170
  subscriber.blocked = false;
987
- if (!subscriber.needsSync || subscriber.response.destroyed)
1171
+ if (subscriber.response.destroyed)
988
1172
  return;
989
- subscriber.needsSync = false;
990
- 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
+ }
991
1206
  });
992
1207
  }
993
1208
  broadcastDelta(operation) {
@@ -1075,6 +1290,14 @@ async function route(req, res, router) {
1075
1290
  return repl.subscribe(res);
1076
1291
  if (req.method === "GET" && url.pathname === "/api/state")
1077
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
+ }
1078
1301
  const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
1079
1302
  if (req.method === "GET" && imageMatch) {
1080
1303
  const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
@@ -1094,10 +1317,16 @@ async function route(req, res, router) {
1094
1317
  const body = await readJsonBody(req);
1095
1318
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
1096
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
+ }
1097
1324
  if (req.method === "POST" && url.pathname === "/api/interrupt")
1098
1325
  return sendJson(res, repl.interrupt());
1099
1326
  if (req.method === "POST" && url.pathname === "/api/queue/cancel")
1100
1327
  return sendJson(res, repl.cancelQueue());
1328
+ if (req.method === "POST" && url.pathname === "/api/queue/send-now")
1329
+ return sendJson(res, repl.sendQueuedNow());
1101
1330
  if (req.method === "GET" && url.pathname === "/api/sessions")
1102
1331
  return sendJson(res, await repl.listSessions());
1103
1332
  if (req.method === "POST" && url.pathname === "/api/sessions/resume") {
@@ -1119,7 +1348,7 @@ async function route(req, res, router) {
1119
1348
  sendJson(res, { error: "not found" }, 404);
1120
1349
  }
1121
1350
  catch (error) {
1122
- 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);
1123
1352
  }
1124
1353
  }
1125
1354
  function webRuntimeScopeFromUrl(url) {
@@ -1164,10 +1393,22 @@ async function readJsonBody(req) {
1164
1393
  function sanitizeWebAttachments(value) {
1165
1394
  if (!Array.isArray(value))
1166
1395
  return [];
1167
- return value.filter((item) => {
1396
+ return value.slice(0, 20).flatMap((item) => {
1168
1397
  if (!isRecord(item))
1169
- return false;
1170
- 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 [];
1171
1412
  });
1172
1413
  }
1173
1414
  function webCatalog(runtime) {
@@ -1201,7 +1442,7 @@ function restoredHistoryLines(runtime) {
1201
1442
  return lines.length;
1202
1443
  };
1203
1444
  for (const message of runtime.engine.getHistoryMessages())
1204
- renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: false });
1445
+ renderMessage(message, append, undefined, { includeToolUseBlocks: true, includeThinkingBlocks: true });
1205
1446
  return lines;
1206
1447
  }
1207
1448
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
@@ -1211,9 +1452,12 @@ async function resetStatus(runtime) {
1211
1452
  return initialStatus(runtime, await runtime.engine.contextMetrics());
1212
1453
  }
1213
1454
  function buildWebPromptPayload(displayText, attachments) {
1214
- const activeAttachments = attachments.filter((attachment) => displayText.includes(attachment.label));
1215
- if (activeAttachments.length === 0)
1216
- 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
+ }
1217
1461
  const blocks = [];
1218
1462
  let cursor = 0;
1219
1463
  while (cursor < displayText.length) {
@@ -1226,8 +1470,27 @@ function buildWebPromptPayload(displayText, attachments) {
1226
1470
  blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
1227
1471
  cursor = next.index + next.attachment.label.length;
1228
1472
  }
1473
+ pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
1229
1474
  const text = blocks.map((block) => block.type === "text" ? block.text : block.type === "image" ? block.label ?? "[image]" : "").join("");
1230
- 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");
1231
1494
  }
1232
1495
  function nextWebAttachmentOccurrence(text, attachments, start) {
1233
1496
  let best;
@@ -1320,7 +1583,7 @@ async function handleModelCommand(command, runtime) {
1320
1583
  const innerGateway = createModelGatewayFromConfig(config);
1321
1584
  runtime.modelGateway.setInner(innerGateway);
1322
1585
  runtime.agentRuntime.modelGateway = runtime.modelGateway;
1323
- 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 });
1324
1587
  syncImageGenerationTool(runtime, config.provider);
1325
1588
  runtime.defaultReasoning = config.defaultReasoning;
1326
1589
  }
@@ -1372,18 +1635,18 @@ async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
1372
1635
  return { providerChanged: targetProvider !== currentProvider };
1373
1636
  }
1374
1637
  function currentModelProvider() {
1375
- return parseLoginProvider(process.env.MODEL_PROVIDER) ?? (process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai");
1638
+ return "openai";
1376
1639
  }
1377
1640
  function parseLoginProvider(value) {
1378
- if (value === "openai" || value === "anthropic")
1641
+ if (value === "openai")
1379
1642
  return value;
1380
1643
  return undefined;
1381
1644
  }
1382
- const LOGIN_PROVIDERS = ["openai", "anthropic"];
1645
+ const LOGIN_PROVIDERS = ["openai"];
1383
1646
  const SHARED_LOGIN_FIELDS = [
1384
1647
  { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
1385
1648
  { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
1386
- { 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" },
1387
1650
  { key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
1388
1651
  { key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
1389
1652
  { key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
@@ -1393,23 +1656,13 @@ const LOGIN_FIELD_DEFINITIONS = {
1393
1656
  { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
1394
1657
  { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
1395
1658
  { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.6" },
1396
- { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
1397
1659
  { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
1398
1660
  ...SHARED_LOGIN_FIELDS,
1399
1661
  ],
1400
- anthropic: [
1401
- { key: "apiKey", label: "API key", envKey: "ANTHROPIC_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-ant-..." },
1402
- { key: "baseUrl", label: "Base URL", envKey: "ANTHROPIC_BASE_URL", scope: "provider", placeholder: "https://api.anthropic.com" },
1403
- { key: "model", label: "Model", envKey: "ANTHROPIC_MODEL", scope: "provider", required: true, placeholder: "claude-sonnet-4-6" },
1404
- { key: "fallbackModel", label: "Fallback model", envKey: "ANTHROPIC_FALLBACK_MODEL", scope: "provider" },
1405
- { key: "version", label: "Anthropic version", envKey: "ANTHROPIC_VERSION", scope: "provider", placeholder: "2023-06-01" },
1406
- ...SHARED_LOGIN_FIELDS,
1407
- ],
1408
1662
  };
1409
1663
  const DEPRECATED_MODEL_ENV_KEYS = [
1410
1664
  "MODEL_API_KEY", "MODEL_BASE_URL", "MODEL_ID", "MODEL_FALLBACK_ID", "MODEL_ENDPOINT", "OPENAI_PROVIDER",
1411
1665
  "OPENAI_REASONING_EFFORT", "OPENAI_REASONING_SUMMARY", "OPENAI_MAX_OUTPUT_TOKENS", "OPENAI_TIMEOUT_MS", "OPENAI_STREAM_IDLE_TIMEOUT_MS", "OPENAI_MAX_RETRIES",
1412
- "ANTHROPIC_REASONING_EFFORT", "ANTHROPIC_REASONING_SUMMARY", "ANTHROPIC_MAX_OUTPUT_TOKENS", "ANTHROPIC_TIMEOUT_MS", "ANTHROPIC_STREAM_IDLE_TIMEOUT_MS", "ANTHROPIC_MAX_RETRIES",
1413
1666
  ];
1414
1667
  function createLoginFormPayload(envPath, provider) {
1415
1668
  const env = parseEnvFileSafe(envPath);
@@ -1434,19 +1687,13 @@ function loginValuesForProvider(provider, env) {
1434
1687
  values.endpoint = "auto";
1435
1688
  return values;
1436
1689
  }
1437
- function guessLoginProvider(env) {
1438
- if (env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)
1439
- return "anthropic";
1690
+ function guessLoginProvider(_env) {
1440
1691
  return currentModelProvider();
1441
1692
  }
1442
- function defaultBaseUrlForLoginProvider(provider) {
1443
- if (provider === "anthropic")
1444
- return "https://api.anthropic.com";
1693
+ function defaultBaseUrlForLoginProvider(_provider) {
1445
1694
  return "https://api.openai.com";
1446
1695
  }
1447
- function defaultModelForLoginProvider(provider) {
1448
- if (provider === "anthropic")
1449
- return "claude-sonnet-4-6";
1696
+ function defaultModelForLoginProvider(_provider) {
1450
1697
  return "gpt-5.6";
1451
1698
  }
1452
1699
  function validateLoginFormPayload(payload) {
@@ -1462,6 +1709,9 @@ function validateLoginFormPayload(payload) {
1462
1709
  if (value && !Number.isFinite(Number(value)))
1463
1710
  return `${fieldKey} must be a number.`;
1464
1711
  }
1712
+ if (payload.provider === "openai" && payload.values.endpoint === "chat" && payload.values.reasoningSummary?.trim()) {
1713
+ return "Reasoning summary requires the Responses API endpoint.";
1714
+ }
1465
1715
  return undefined;
1466
1716
  }
1467
1717
  async function saveLoginPayloadToEnv(payload) {
@@ -1496,9 +1746,7 @@ function stripEnvQuotes(value) {
1496
1746
  return value.slice(1, -1);
1497
1747
  return value;
1498
1748
  }
1499
- function modelEnvKeyForProvider(provider) {
1500
- if (provider === "anthropic")
1501
- return "ANTHROPIC_MODEL";
1749
+ function modelEnvKeyForProvider(_provider) {
1502
1750
  return "OPENAI_MODEL";
1503
1751
  }
1504
1752
  function envValueForReasoning(reasoning) {
@@ -1547,9 +1795,6 @@ function formatEnvValue(value) {
1547
1795
  return value;
1548
1796
  return JSON.stringify(value);
1549
1797
  }
1550
- function formatResume(snapshot) {
1551
- return `resumed session ${snapshot.sessionId}: ${snapshot.resumedMessages} messages from ${snapshot.transcriptPath}`;
1552
- }
1553
1798
  function applyEnvUpdatesToProcess(updates) {
1554
1799
  for (const [key, value] of Object.entries(updates)) {
1555
1800
  if (value === undefined)
@@ -1573,8 +1818,6 @@ function validateModelReasoningArgument(modelId, reasoning) {
1573
1818
  function formatModelSettings(settings, defaultReasoning) {
1574
1819
  const window = resolveContextWindowTokens(settings.model);
1575
1820
  const lines = ["Model settings:", ` Model: ${settings.model ?? "<provider default>"}`];
1576
- if (settings.fallbackModel)
1577
- lines.push(` Fallback: ${settings.fallbackModel}`);
1578
1821
  lines.push(` Reasoning effort: ${formatReasoningSetting(settings.reasoning)}`);
1579
1822
  if (defaultReasoning?.effort)
1580
1823
  lines.push(` Env default reasoning: ${defaultReasoning.effort}`);
@@ -1607,30 +1850,30 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1607
1850
  if (kind === "meta")
1608
1851
  continue;
1609
1852
  if (kind === "system")
1610
- 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" });
1611
1854
  else
1612
- append({ kind, text: block.text });
1855
+ append({ kind, text: block.text, messageId: message.id });
1613
1856
  rendered = true;
1614
1857
  }
1615
1858
  else if (block.type === "image") {
1616
1859
  const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1617
1860
  if (!line)
1618
1861
  continue;
1619
- append({ ...line, parentToolName });
1862
+ append({ ...line, messageId: message.id, parentToolName });
1620
1863
  rendered = true;
1621
1864
  }
1622
1865
  else if (block.type === "thinking") {
1623
1866
  if (options.includeThinkingBlocks === false)
1624
1867
  continue;
1625
- append(thinkingLine(block.text));
1868
+ append({ ...thinkingLine(block.text), messageId: message.id });
1626
1869
  rendered = true;
1627
1870
  }
1628
1871
  else if (block.type === "tool_use" && options.includeToolUseBlocks) {
1629
- append({ ...formatToolUse(block), live: false });
1872
+ append({ ...formatToolUse(block), messageId: message.id, live: false });
1630
1873
  rendered = true;
1631
1874
  }
1632
1875
  else if (block.type === "tool_result") {
1633
- append(formatToolResultLine(block.name, block.output, block.ok));
1876
+ append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
1634
1877
  rendered = true;
1635
1878
  }
1636
1879
  }
@@ -1641,12 +1884,12 @@ function renderToolResultMessage(message, append) {
1641
1884
  for (const block of message.blocks) {
1642
1885
  if (block.type !== "tool_result")
1643
1886
  continue;
1644
- 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);
1645
1888
  rendered = true;
1646
1889
  }
1647
1890
  return rendered;
1648
1891
  }
1649
- function renderMessageImages(message, append) {
1892
+ function renderMessageImages(message, append, messageId = message.id) {
1650
1893
  let rendered = false;
1651
1894
  const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
1652
1895
  const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
@@ -1656,7 +1899,7 @@ function renderMessageImages(message, append) {
1656
1899
  const line = imageLineForBlock(message.role, block, message.id, blockIndex);
1657
1900
  if (!line)
1658
1901
  continue;
1659
- append({ ...line, parentToolName, parentToolUseId });
1902
+ append({ ...line, messageId, parentToolName, parentToolUseId });
1660
1903
  rendered = true;
1661
1904
  }
1662
1905
  return rendered;
@@ -1727,8 +1970,8 @@ function thinkingLine(text, live = false) {
1727
1970
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1728
1971
  }
1729
1972
  function formatToolUse(toolUse) {
1730
- if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1731
- 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 };
1732
1975
  if (toolUse.name === "image2")
1733
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 };
1734
1977
  const summary = summarizeToolUse(toolUse.name, toolUse.input);
@@ -1742,7 +1985,8 @@ function toolTitle(toolName, _phase) {
1742
1985
  const labels = {
1743
1986
  agent: "子任务",
1744
1987
  edit: "编辑文件",
1745
- exec: "执行命令",
1988
+ exec_command: "执行命令",
1989
+ write_stdin: "终端交互",
1746
1990
  expose_downloads: "文件下载",
1747
1991
  grep: "搜索文本",
1748
1992
  image2: "图片生成",
@@ -1783,24 +2027,6 @@ function toolUsePurpose(toolName, input) {
1783
2027
  return `修改 ${path.basename(input.path)}`;
1784
2028
  return undefined;
1785
2029
  }
1786
- function isPlanToolPayload(value) {
1787
- if (!isRecord(value) || !Array.isArray(value.items))
1788
- return false;
1789
- return value.items.every((item) => isRecord(item) && typeof item.description === "string" && (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
1790
- }
1791
- function planToolBodyTitle(payload) {
1792
- const title = payload.title?.trim();
1793
- return title ? title : undefined;
1794
- }
1795
- function formatPlanToolPayload(payload) {
1796
- const sections = [];
1797
- if (payload.summary?.trim())
1798
- sections.push(payload.summary.trim());
1799
- if (payload.note?.trim())
1800
- sections.push(payload.note.trim());
1801
- sections.push(payload.items.map((item) => item.status === "completed" ? `- ~~${item.description.trim()}~~` : item.status === "in_progress" ? `- ▶ ${item.description.trim()}` : `- ${item.description.trim()}`).join("\n"));
1802
- return sections.filter(Boolean).join("\n");
1803
- }
1804
2030
  function formatToolResult(toolName, output, ok) {
1805
2031
  if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
1806
2032
  return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
@@ -1818,8 +2044,8 @@ function formatToolResult(toolName, output, ok) {
1818
2044
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
1819
2045
  if (toolName === "expose_downloads")
1820
2046
  return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
1821
- if (toolName === "plan" && isPlanToolPayload(output))
1822
- 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) };
1823
2049
  if (typeof output === "string")
1824
2050
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1825
2051
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
@@ -1895,12 +2121,14 @@ function diffLineMarker(line) {
1895
2121
  return marker === "+" || marker === "-" || marker === " " ? marker : undefined;
1896
2122
  }
1897
2123
  function isExecOutput(value) {
1898
- return isRecord(value) && typeof value.command === "string" && typeof value.durationMs === "number";
2124
+ return isRecord(value) && typeof value.command === "string" && typeof value.duration_ms === "number";
1899
2125
  }
1900
2126
  function formatExecToolResult(output, ok) {
1901
- const status = output.timedOut ? "已超时" : ok ? "已完成" : "执行失败";
2127
+ const status = output.status === "running" ? "运行中" : output.timed_out ? "已超时" : ok ? "已完成" : "执行失败";
1902
2128
  const description = typeof output.description === "string" ? output.description.trim() : "";
1903
- 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}`);
1904
2132
  const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
1905
2133
  const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
1906
2134
  if (stdout)