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.
- package/README.md +36 -48
- package/dist/agents/agent-definition.js +1 -1
- package/dist/agents/agent-definition.js.map +1 -1
- package/dist/agents/agent-tool.js +1 -1
- package/dist/agents/agent-tool.js.map +1 -1
- package/dist/agents/smoke-agents.js +2 -2
- package/dist/agents/smoke-agents.js.map +1 -1
- package/dist/context/compaction.js +8 -5
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/context-manager.d.ts +8 -9
- package/dist/context/context-manager.js +33 -41
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/prompts.js +1 -1
- package/dist/context/prompts.js.map +1 -1
- package/dist/context/smoke-context.js +30 -8
- package/dist/context/smoke-context.js.map +1 -1
- package/dist/core/context-metrics.d.ts +1 -1
- package/dist/core/context-metrics.js +5 -3
- package/dist/core/context-metrics.js.map +1 -1
- package/dist/core/message-pipeline.js +13 -21
- package/dist/core/message-pipeline.js.map +1 -1
- package/dist/core/prompt-cache-key.d.ts +10 -0
- package/dist/core/prompt-cache-key.js +34 -0
- package/dist/core/prompt-cache-key.js.map +1 -0
- package/dist/core/prompt-cache-telemetry.d.ts +2 -1
- package/dist/core/prompt-cache-telemetry.js +23 -1
- package/dist/core/prompt-cache-telemetry.js.map +1 -1
- package/dist/core/query-engine.d.ts +6 -5
- package/dist/core/query-engine.js +18 -18
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +0 -1
- package/dist/core/query.js +22 -14
- package/dist/core/query.js.map +1 -1
- package/dist/core/run-agent.d.ts +0 -1
- package/dist/core/run-agent.js +0 -1
- package/dist/core/run-agent.js.map +1 -1
- package/dist/core/smoke-core-loop.js +27 -3
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/core/state.d.ts +0 -2
- package/dist/core/state.js +0 -1
- package/dist/core/state.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/model/config.d.ts +2 -9
- package/dist/model/config.js +3 -31
- package/dist/model/config.js.map +1 -1
- package/dist/model/env.js +2 -10
- package/dist/model/env.js.map +1 -1
- package/dist/model/model-gateway.d.ts +0 -6
- package/dist/model/model-gateway.js.map +1 -1
- package/dist/model/model-metadata.json +0 -40
- package/dist/model/openai-adapter.d.ts +2 -1
- package/dist/model/openai-adapter.js +19 -0
- package/dist/model/openai-adapter.js.map +1 -1
- package/dist/model/openai-chat-mapper.d.ts +0 -1
- package/dist/model/openai-chat-mapper.js +13 -9
- package/dist/model/openai-chat-mapper.js.map +1 -1
- package/dist/model/openai-mappers.d.ts +4 -4
- package/dist/model/openai-mappers.js +23 -17
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/model/openai-responses-mapper.js +76 -35
- package/dist/model/openai-responses-mapper.js.map +1 -1
- package/dist/model/provider-factory.js +1 -22
- package/dist/model/provider-factory.js.map +1 -1
- package/dist/model/smoke-responses-mapper.js +82 -3
- package/dist/model/smoke-responses-mapper.js.map +1 -1
- package/dist/repl/commands.js +5 -2
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +85 -129
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/run-command.d.ts +37 -0
- package/dist/repl/run-command.js +167 -0
- package/dist/repl/run-command.js.map +1 -0
- package/dist/repl/smoke-run-command.js +43 -0
- package/dist/repl/smoke-run-command.js.map +1 -0
- package/dist/secrets/smoke-secrets.js +5 -4
- package/dist/secrets/smoke-secrets.js.map +1 -1
- package/dist/session/session-export.d.ts +0 -1
- package/dist/session/session-export.js +0 -1
- package/dist/session/session-export.js.map +1 -1
- package/dist/session/smoke-session.js +6 -2
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/tools/builtins/exec-process-manager.d.ts +78 -0
- package/dist/tools/builtins/exec-process-manager.js +394 -0
- package/dist/tools/builtins/exec-process-manager.js.map +1 -0
- package/dist/tools/builtins/exec-pty-host.d.ts +1 -0
- package/dist/tools/builtins/exec-pty-host.js +70 -0
- package/dist/tools/builtins/exec-pty-host.js.map +1 -0
- package/dist/tools/builtins/exec-tool.d.ts +16 -27
- package/dist/tools/builtins/exec-tool.js +153 -324
- package/dist/tools/builtins/exec-tool.js.map +1 -1
- package/dist/tools/builtins/image-generation-tool.d.ts +0 -4
- package/dist/tools/builtins/image-generation-tool.js +59 -185
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/tools/smoke-exec-process.d.ts +1 -0
- package/dist/tools/smoke-exec-process.js +42 -0
- package/dist/tools/smoke-exec-process.js.map +1 -0
- package/dist/tools/smoke-tool-system.js +28 -36
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/types/events.d.ts +8 -0
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/messages.js +14 -0
- package/dist/types/messages.js.map +1 -1
- package/dist/web/html.js +76 -29
- package/dist/web/html.js.map +1 -1
- package/dist/web/index.d.ts +98 -37
- package/dist/web/index.js +384 -140
- package/dist/web/index.js.map +1 -1
- package/dist/web/plan-payload.d.ts +14 -0
- package/dist/web/plan-payload.js +27 -0
- package/dist/web/plan-payload.js.map +1 -0
- package/dist/web/runtime-context-protocol.d.ts +46 -0
- package/dist/web/runtime-context-protocol.js +72 -0
- package/dist/web/runtime-context-protocol.js.map +1 -0
- package/dist/web/smoke-plan-payload.d.ts +1 -0
- package/dist/web/smoke-plan-payload.js +30 -0
- package/dist/web/smoke-plan-payload.js.map +1 -0
- package/dist/web/smoke-runtime-context-protocol.d.ts +1 -0
- package/dist/web/smoke-runtime-context-protocol.js +31 -0
- package/dist/web/smoke-runtime-context-protocol.js.map +1 -0
- package/dist/web/smoke-web-queue.d.ts +1 -0
- package/dist/web/smoke-web-queue.js +7 -0
- package/dist/web/smoke-web-queue.js.map +1 -0
- package/package.json +10 -5
- package/scripts/verify-ripgrep.cjs +8 -8
- package/dist/model/anthropic-adapter.d.ts +0 -30
- package/dist/model/anthropic-adapter.js +0 -106
- package/dist/model/anthropic-adapter.js.map +0 -1
- package/dist/model/anthropic-mapper.d.ts +0 -13
- package/dist/model/anthropic-mapper.js +0 -508
- package/dist/model/anthropic-mapper.js.map +0 -1
- package/dist/model/smoke-anthropic-mapper.js +0 -89
- package/dist/model/smoke-anthropic-mapper.js.map +0 -1
- /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({
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
390
|
-
return { ok: false, error: message };
|
|
558
|
+
return actionFailure("SESSION_CREATE_FAILED", message);
|
|
391
559
|
}
|
|
392
560
|
}
|
|
393
|
-
async refreshSessionView(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
862
|
-
|
|
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
|
-
|
|
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 (
|
|
1171
|
+
if (subscriber.response.destroyed)
|
|
969
1172
|
return;
|
|
970
|
-
subscriber.needsSync
|
|
971
|
-
|
|
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.
|
|
1396
|
+
return value.slice(0, 20).flatMap((item) => {
|
|
1136
1397
|
if (!isRecord(item))
|
|
1137
|
-
return
|
|
1138
|
-
|
|
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:
|
|
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
|
-
|
|
1184
|
-
|
|
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
|
|
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,
|
|
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
|
|
1638
|
+
return "openai";
|
|
1344
1639
|
}
|
|
1345
1640
|
function parseLoginProvider(value) {
|
|
1346
|
-
if (value === "openai"
|
|
1641
|
+
if (value === "openai")
|
|
1347
1642
|
return value;
|
|
1348
1643
|
return undefined;
|
|
1349
1644
|
}
|
|
1350
|
-
const LOGIN_PROVIDERS = ["openai"
|
|
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"
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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" &&
|
|
1715
|
-
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle:
|
|
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
|
-
|
|
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" &&
|
|
1806
|
-
return { text:
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|