neoctl 0.2.30 → 0.2.32
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 +37 -3
- package/dist/agents/agent-activity.d.ts +3 -0
- package/dist/agents/agent-activity.js +33 -6
- package/dist/agents/agent-activity.js.map +1 -1
- package/dist/agents/agent-definition.d.ts +2 -2
- package/dist/agents/agent-definition.js +6 -6
- package/dist/agents/agent-definition.js.map +1 -1
- package/dist/agents/agent-report-tool.d.ts +1 -1
- package/dist/agents/agent-report-tool.js +4 -4
- package/dist/agents/agent-report-tool.js.map +1 -1
- package/dist/agents/agent-tool.d.ts +1 -1
- package/dist/agents/agent-tool.js +97 -18
- package/dist/agents/agent-tool.js.map +1 -1
- package/dist/agents/local-agent-task.d.ts +12 -0
- package/dist/agents/local-agent-task.js +38 -1
- package/dist/agents/local-agent-task.js.map +1 -1
- package/dist/agents/smoke-agents.js +37 -26
- package/dist/agents/smoke-agents.js.map +1 -1
- package/dist/context/compaction.d.ts +20 -4
- package/dist/context/compaction.js +378 -187
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/context-manager.d.ts +7 -1
- package/dist/context/context-manager.js +15 -11
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/prompts.js +8 -6
- package/dist/context/prompts.js.map +1 -1
- package/dist/context/smoke-context.js +273 -24
- package/dist/context/smoke-context.js.map +1 -1
- package/dist/core/assistant-output-filter.d.ts +1 -0
- package/dist/core/assistant-output-filter.js +54 -2
- package/dist/core/assistant-output-filter.js.map +1 -1
- package/dist/core/context-metrics.d.ts +1 -0
- package/dist/core/context-metrics.js +18 -12
- package/dist/core/context-metrics.js.map +1 -1
- package/dist/core/image-registry.d.ts +16 -3
- package/dist/core/image-registry.js +65 -30
- package/dist/core/image-registry.js.map +1 -1
- package/dist/core/image-storage.d.ts +15 -1
- package/dist/core/image-storage.js +140 -17
- package/dist/core/image-storage.js.map +1 -1
- package/dist/core/message-pipeline.js +18 -4
- package/dist/core/message-pipeline.js.map +1 -1
- package/dist/core/query-engine.d.ts +12 -2
- package/dist/core/query-engine.js +59 -8
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +13 -2
- package/dist/core/query.js +144 -27
- package/dist/core/query.js.map +1 -1
- package/dist/core/run-agent.d.ts +13 -2
- package/dist/core/run-agent.js +16 -13
- package/dist/core/run-agent.js.map +1 -1
- package/dist/core/smoke-core-loop.js +121 -3
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/core/smoke-image-integrity.d.ts +1 -0
- package/dist/core/smoke-image-integrity.js +67 -0
- package/dist/core/smoke-image-integrity.js.map +1 -0
- package/dist/core/state.d.ts +1 -1
- package/dist/core/state.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/model/model-metadata.json +4 -4
- package/dist/plugins/plugin-system.d.ts +61 -0
- package/dist/plugins/plugin-system.js +189 -0
- package/dist/plugins/plugin-system.js.map +1 -0
- package/dist/plugins/smoke-plugin-system.d.ts +1 -0
- package/dist/plugins/smoke-plugin-system.js +63 -0
- package/dist/plugins/smoke-plugin-system.js.map +1 -0
- package/dist/repl/index.js +22 -17
- package/dist/repl/index.js.map +1 -1
- package/dist/secrets/secret-redaction.d.ts +5 -0
- package/dist/secrets/secret-redaction.js +43 -0
- package/dist/secrets/secret-redaction.js.map +1 -1
- package/dist/secrets/secret-types.d.ts +4 -0
- package/dist/secrets/secret-types.js.map +1 -1
- package/dist/secrets/smoke-secrets.js +7 -3
- package/dist/secrets/smoke-secrets.js.map +1 -1
- package/dist/session/session-export.d.ts +2 -0
- package/dist/session/session-export.js +10 -0
- package/dist/session/session-export.js.map +1 -1
- package/dist/session/session-store.d.ts +41 -0
- package/dist/session/session-store.js +292 -59
- package/dist/session/session-store.js.map +1 -1
- package/dist/session/smoke-session.js +127 -7
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/skills/smoke-skills.js +5 -5
- package/dist/skills/smoke-skills.js.map +1 -1
- package/dist/tasks/subagent-tools.d.ts +31 -0
- package/dist/tasks/{task-tools.js → subagent-tools.js} +26 -32
- package/dist/tasks/subagent-tools.js.map +1 -0
- package/dist/tools/builtins/edit-tool.js +24 -6
- package/dist/tools/builtins/edit-tool.js.map +1 -1
- package/dist/tools/builtins/exec-process-manager.d.ts +13 -0
- package/dist/tools/builtins/exec-process-manager.js +57 -7
- package/dist/tools/builtins/exec-process-manager.js.map +1 -1
- package/dist/tools/builtins/exec-tool.js +34 -10
- package/dist/tools/builtins/exec-tool.js.map +1 -1
- package/dist/tools/builtins/filesystem-tools.js +2 -4
- package/dist/tools/builtins/filesystem-tools.js.map +1 -1
- package/dist/tools/builtins/grep-tool.js +2 -3
- package/dist/tools/builtins/grep-tool.js.map +1 -1
- package/dist/tools/builtins/image-generation-tool.js +36 -46
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/tools/builtins/image-loader-tool.js +12 -7
- package/dist/tools/builtins/image-loader-tool.js.map +1 -1
- package/dist/tools/builtins/image-note-tool.js +8 -4
- package/dist/tools/builtins/image-note-tool.js.map +1 -1
- package/dist/tools/builtins/plan-tool.js +1 -1
- package/dist/tools/builtins/plan-tool.js.map +1 -1
- package/dist/tools/builtins/search-tool.js +3 -3
- package/dist/tools/builtins/search-tool.js.map +1 -1
- package/dist/tools/registry.d.ts +8 -1
- package/dist/tools/registry.js +37 -13
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/run-tool-use.js +3 -2
- package/dist/tools/run-tool-use.js.map +1 -1
- package/dist/tools/smoke-exec-process.js +12 -0
- package/dist/tools/smoke-exec-process.js.map +1 -1
- package/dist/tools/smoke-tool-system.js +68 -31
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/tools/tool-catalog.d.ts +36 -0
- package/dist/tools/tool-catalog.js +64 -0
- package/dist/tools/tool-catalog.js.map +1 -0
- package/dist/tools/tool-orchestration.d.ts +20 -3
- package/dist/tools/tool-orchestration.js +29 -4
- package/dist/tools/tool-orchestration.js.map +1 -1
- package/dist/tools/tool.d.ts +13 -0
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/events.d.ts +28 -1
- package/dist/types/messages.d.ts +13 -5
- package/dist/types/messages.js +4 -0
- package/dist/types/messages.js.map +1 -1
- package/dist/ui/display-message.d.ts +10 -2
- package/dist/ui/display-message.js +18 -9
- package/dist/ui/display-message.js.map +1 -1
- package/dist/web/index.d.ts +198 -9
- package/dist/web/index.js +1208 -146
- package/dist/web/index.js.map +1 -1
- package/dist/web/runtime-context-protocol.d.ts +19 -1
- package/dist/web/runtime-context-protocol.js +19 -3
- package/dist/web/runtime-context-protocol.js.map +1 -1
- package/dist/web/smoke-runtime-context-protocol.js +13 -2
- package/dist/web/smoke-runtime-context-protocol.js.map +1 -1
- package/dist/web/smoke-web-history.d.ts +1 -0
- package/dist/web/smoke-web-history.js +179 -0
- package/dist/web/smoke-web-history.js.map +1 -0
- package/dist/web/smoke-web-terminal-tasks.js +35 -3
- package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
- package/package.json +4 -1
- package/dist/tasks/task-tools.d.ts +0 -31
- package/dist/tasks/task-tools.js.map +0 -1
package/dist/web/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { findModelMetadata, loadModelCatalog, reasoningEffortsForModel, resolveC
|
|
|
14
14
|
import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
|
|
15
15
|
import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
|
|
16
16
|
import { ToolRegistry } from "../tools/registry.js";
|
|
17
|
+
import { builtinToolPresentation, toolPresentationForSource } from "../tools/tool-catalog.js";
|
|
17
18
|
import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
|
|
18
19
|
import { createExecTool, createWriteStdinTool } from "../tools/builtins/exec-tool.js";
|
|
19
20
|
import { ExecProcessManager } from "../tools/builtins/exec-process-manager.js";
|
|
@@ -24,17 +25,20 @@ import { planTool } from "../tools/builtins/plan-tool.js";
|
|
|
24
25
|
import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
|
|
25
26
|
import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
|
|
26
27
|
import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
|
|
27
|
-
import {
|
|
28
|
+
import { createSubagentTools } from "../tasks/subagent-tools.js";
|
|
28
29
|
import { TaskStore } from "../tasks/task-store.js";
|
|
29
30
|
import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
|
|
30
31
|
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
31
32
|
import { DefaultContextManager } from "../context/context-manager.js";
|
|
33
|
+
import { createImageId } from "../types/messages.js";
|
|
32
34
|
import { WEB_HTML } from "./html.js";
|
|
33
35
|
import { openDirectory } from "../open-directory.js";
|
|
34
|
-
import { resolveImageBlockDataSync } from "../core/image-storage.js";
|
|
36
|
+
import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
|
|
35
37
|
import { isWebPlanPayload, serializeWebPlanPayload, webPlanBodyTitle } from "./plan-payload.js";
|
|
36
38
|
import { createWebRuntimeContextPayload } from "./runtime-context-protocol.js";
|
|
37
39
|
const require = createRequire(import.meta.url);
|
|
40
|
+
const corePackage = require("../../package.json");
|
|
41
|
+
const CORE_VERSION = String(corePackage.version || "unknown");
|
|
38
42
|
const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
|
|
39
43
|
const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
|
|
40
44
|
const markedAssetPath = path.join(markedPackageDir, "lib", "marked.esm.js");
|
|
@@ -127,26 +131,42 @@ export async function createWebRuntime(options = {}) {
|
|
|
127
131
|
const pluginOverrides = normalizePluginOverrides(options.sessionPluginOverrides, pluginCatalog);
|
|
128
132
|
const activePlugins = activeWebRuntimePlugins(pluginCatalog, pluginOverrides);
|
|
129
133
|
const tools = new ToolRegistry();
|
|
134
|
+
const toolCatalog = [];
|
|
135
|
+
const catalogedToolNames = new Set();
|
|
136
|
+
const catalogTool = (tool, item) => {
|
|
137
|
+
if (catalogedToolNames.has(tool.name))
|
|
138
|
+
return;
|
|
139
|
+
catalogedToolNames.add(tool.name);
|
|
140
|
+
toolCatalog.push({ name: tool.name, presentation: toolPresentationForSource(tool.name, item.source), ...item });
|
|
141
|
+
};
|
|
142
|
+
const registerTool = (tool, item) => {
|
|
143
|
+
tools.register(tool);
|
|
144
|
+
catalogTool(tool, item);
|
|
145
|
+
};
|
|
130
146
|
const execProcessManager = new ExecProcessManager();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
registerTool(editTool, { source: "builtin" });
|
|
148
|
+
registerTool(writeTool, { source: "builtin" });
|
|
149
|
+
registerTool(createExecTool({ processManager: execProcessManager }), { source: "builtin" });
|
|
150
|
+
registerTool(createWriteStdinTool(execProcessManager), { source: "builtin" });
|
|
151
|
+
registerTool(listDirectoryTool, { source: "builtin" });
|
|
152
|
+
registerTool(readFileTool, { source: "builtin" });
|
|
153
|
+
registerTool(grepTool, { source: "builtin" });
|
|
154
|
+
registerTool(searchTool, { source: "builtin" });
|
|
155
|
+
registerTool(createLoadImageTool(), { source: "builtin" });
|
|
140
156
|
if (modelConfig?.provider === "openai")
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
|
|
158
|
+
registerTool(planTool, { source: "builtin" });
|
|
143
159
|
for (const tool of options.externalTools ?? [])
|
|
144
|
-
|
|
160
|
+
registerTool(tool, { source: "external" });
|
|
161
|
+
for (const plugin of pluginCatalog) {
|
|
162
|
+
for (const tool of plugin.tools)
|
|
163
|
+
catalogTool(tool, { source: "plugin", pluginId: plugin.id, pluginName: plugin.name });
|
|
164
|
+
}
|
|
145
165
|
for (const plugin of activePlugins)
|
|
146
166
|
for (const tool of plugin.tools)
|
|
147
167
|
tools.register(tool);
|
|
148
168
|
const agentRuntime = { modelGateway, tools, taskStore };
|
|
149
|
-
|
|
169
|
+
registerTool(createAgentTool(agentRuntime), { source: "builtin" });
|
|
150
170
|
const resumeHandler = async (taskId, directive) => {
|
|
151
171
|
const dummyContext = {
|
|
152
172
|
agentId: "main",
|
|
@@ -156,8 +176,11 @@ export async function createWebRuntime(options = {}) {
|
|
|
156
176
|
};
|
|
157
177
|
return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
|
|
158
178
|
};
|
|
159
|
-
for (const tool of
|
|
160
|
-
|
|
179
|
+
for (const tool of createSubagentTools(taskStore, resumeHandler))
|
|
180
|
+
registerTool(tool, { source: "builtin" });
|
|
181
|
+
const globalToolOverrides = normalizeToolOverrides(options.globalToolOverrides, toolCatalog);
|
|
182
|
+
const sessionToolOverrides = normalizeToolOverrides(options.sessionToolOverrides, toolCatalog);
|
|
183
|
+
applyToolOverrides(tools, toolCatalog, globalToolOverrides, sessionToolOverrides);
|
|
161
184
|
const appPromptStore = new InMemoryAppPromptStore();
|
|
162
185
|
const engine = new QueryEngine({
|
|
163
186
|
agentId: options.agentId ?? "main",
|
|
@@ -176,6 +199,8 @@ export async function createWebRuntime(options = {}) {
|
|
|
176
199
|
taskNotificationSource: createTaskNotificationSource(taskStore),
|
|
177
200
|
commands: replCommandDefinitions.map((command) => command.usage),
|
|
178
201
|
plugins: [...(options.plugins ?? []), ...activePlugins.map((plugin) => plugin.id)],
|
|
202
|
+
cwdTransitionPaths: options.cwdTransitionPaths,
|
|
203
|
+
onCwdTransitionConsumed: options.onCwdTransitionConsumed,
|
|
179
204
|
session: {
|
|
180
205
|
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
181
206
|
sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
|
|
@@ -208,6 +233,14 @@ export async function createWebRuntime(options = {}) {
|
|
|
208
233
|
persist: options.persistSessionPluginOverrides,
|
|
209
234
|
resolve: options.resolveSessionPluginOverrides,
|
|
210
235
|
} : undefined,
|
|
236
|
+
toolSupport: {
|
|
237
|
+
catalog: toolCatalog,
|
|
238
|
+
globalOverrides: globalToolOverrides,
|
|
239
|
+
sessionOverrides: sessionToolOverrides,
|
|
240
|
+
persistGlobal: options.persistGlobalToolOverrides,
|
|
241
|
+
persistSession: options.persistSessionToolOverrides,
|
|
242
|
+
resolveSession: options.resolveSessionToolOverrides,
|
|
243
|
+
},
|
|
211
244
|
};
|
|
212
245
|
}
|
|
213
246
|
function normalizeWebRuntimePlugins(value) {
|
|
@@ -233,6 +266,14 @@ function normalizePluginOverrides(value, catalog) {
|
|
|
233
266
|
function activeWebRuntimePlugins(catalog, overrides) {
|
|
234
267
|
return catalog.filter((plugin) => plugin.globallyEnabled && overrides[plugin.id] !== false);
|
|
235
268
|
}
|
|
269
|
+
function normalizeToolOverrides(value, catalog) {
|
|
270
|
+
const available = new Set(catalog.map((tool) => tool.name));
|
|
271
|
+
return Object.fromEntries(Object.entries(value ?? {}).filter(([name, enabled]) => available.has(name) && typeof enabled === "boolean"));
|
|
272
|
+
}
|
|
273
|
+
function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides) {
|
|
274
|
+
for (const tool of catalog)
|
|
275
|
+
registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
|
|
276
|
+
}
|
|
236
277
|
function createTaskNotificationSource(taskStore) {
|
|
237
278
|
return {
|
|
238
279
|
collectUnnotifiedCompletions() {
|
|
@@ -250,7 +291,7 @@ function createTaskNotificationSource(taskStore) {
|
|
|
250
291
|
};
|
|
251
292
|
}
|
|
252
293
|
function syncImageGenerationTool(runtime, provider) {
|
|
253
|
-
runtime.tools.unregister("
|
|
294
|
+
runtime.tools.unregister("image_create");
|
|
254
295
|
if (provider === "openai")
|
|
255
296
|
runtime.tools.register(createOpenAIImageGenerationTool());
|
|
256
297
|
}
|
|
@@ -266,6 +307,8 @@ export class WebRuntimeRouter {
|
|
|
266
307
|
repls = new Map();
|
|
267
308
|
createRuntime;
|
|
268
309
|
createRepl;
|
|
310
|
+
clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
311
|
+
coreVersion = process.env.NEO_CORE_VERSION?.trim() || CORE_VERSION;
|
|
269
312
|
constructor(options = {}) {
|
|
270
313
|
this.createRuntime = options.createRuntime ?? createWebRuntime;
|
|
271
314
|
this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
|
|
@@ -286,6 +329,24 @@ export class WebRuntimeRouter {
|
|
|
286
329
|
activeScopes() {
|
|
287
330
|
return [...this.repls.keys()];
|
|
288
331
|
}
|
|
332
|
+
clientInfo() {
|
|
333
|
+
return {
|
|
334
|
+
protocolVersion: 1,
|
|
335
|
+
revision: this.clientRevision,
|
|
336
|
+
coreVersion: this.coreVersion,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async requestClientReload(options = {}) {
|
|
340
|
+
const payload = {
|
|
341
|
+
...this.clientInfo(),
|
|
342
|
+
requestId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
343
|
+
clearCache: options.clearCache !== false,
|
|
344
|
+
reason: options.reason?.trim() || "deployment",
|
|
345
|
+
};
|
|
346
|
+
const repls = await Promise.all([...this.repls.values()]);
|
|
347
|
+
const clients = repls.reduce((total, repl) => total + repl.requestClientReload(payload), 0);
|
|
348
|
+
return { ...payload, clients };
|
|
349
|
+
}
|
|
289
350
|
}
|
|
290
351
|
export async function createWebRuntimeRouter(options = {}) {
|
|
291
352
|
const router = new WebRuntimeRouter(options);
|
|
@@ -308,6 +369,8 @@ export class WebRepl {
|
|
|
308
369
|
pendingDeltaOperations = [];
|
|
309
370
|
pendingDeltaStatus = false;
|
|
310
371
|
pendingDeltaTimer;
|
|
372
|
+
pendingTerminalOutput = new Map();
|
|
373
|
+
terminalOutputTimer;
|
|
311
374
|
lineId = 0;
|
|
312
375
|
assistantLineId;
|
|
313
376
|
thinkingLineId;
|
|
@@ -322,29 +385,29 @@ export class WebRepl {
|
|
|
322
385
|
queuedAttachments;
|
|
323
386
|
foregroundRun;
|
|
324
387
|
foregroundRunToken = 0;
|
|
325
|
-
|
|
388
|
+
pendingUserImageEchoIds;
|
|
326
389
|
pendingUserImageEchoMessageId;
|
|
327
390
|
runtimeContextRevision = 0;
|
|
328
391
|
runtimeContextPublishQueue = Promise.resolve();
|
|
329
392
|
backgroundSessionRuns = new Map();
|
|
330
393
|
suppressReattachedStreaming = new Set();
|
|
331
|
-
backgroundTaskCount;
|
|
332
394
|
constructor(runtime) {
|
|
333
395
|
this.runtime = runtime;
|
|
334
|
-
|
|
396
|
+
const lineId = { current: 0 };
|
|
397
|
+
this.lines = initialLines(runtime, lineId);
|
|
398
|
+
this.lineId = lineId.current;
|
|
335
399
|
this.status = initialStatus(runtime);
|
|
336
|
-
this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
|
|
337
400
|
runtime.taskStore.subscribe(() => {
|
|
338
|
-
this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
|
|
339
401
|
this.broadcastSync();
|
|
340
402
|
});
|
|
341
403
|
runtime.execProcessManager.subscribe(() => {
|
|
342
|
-
this.
|
|
404
|
+
this.dropInactiveTerminalOutput();
|
|
343
405
|
this.broadcastSync();
|
|
344
406
|
});
|
|
407
|
+
runtime.execProcessManager.subscribeOutput((delta) => this.queueTerminalOutput(delta));
|
|
345
408
|
runtime.engine.onSessionTitleChange(() => this.broadcastSync());
|
|
346
409
|
}
|
|
347
|
-
subscribe(res) {
|
|
410
|
+
subscribe(res, clientInfo) {
|
|
348
411
|
const subscriber = { response: res, blocked: false, needsSync: false, needsRuntimeContext: false };
|
|
349
412
|
this.subscribers.add(subscriber);
|
|
350
413
|
res.writeHead(200, {
|
|
@@ -353,19 +416,31 @@ export class WebRepl {
|
|
|
353
416
|
Connection: "keep-alive",
|
|
354
417
|
"X-Accel-Buffering": "no",
|
|
355
418
|
});
|
|
419
|
+
if (clientInfo)
|
|
420
|
+
this.send(subscriber, "client.version", clientInfo);
|
|
356
421
|
this.send(subscriber, "sync", this.snapshot(true));
|
|
357
422
|
this.publishRuntimeContext([subscriber]);
|
|
358
423
|
reqKeepAlive(res);
|
|
359
424
|
res.on("close", () => this.subscribers.delete(subscriber));
|
|
360
425
|
}
|
|
426
|
+
get backgroundTaskCount() {
|
|
427
|
+
return this.backgroundTasks().length;
|
|
428
|
+
}
|
|
429
|
+
requestClientReload(payload) {
|
|
430
|
+
const subscribers = [...this.subscribers].filter((subscriber) => !subscriber.response.destroyed);
|
|
431
|
+
for (const subscriber of subscribers)
|
|
432
|
+
this.send(subscriber, "client.reload", payload);
|
|
433
|
+
return subscribers.length;
|
|
434
|
+
}
|
|
361
435
|
snapshot(includeCatalog = false) {
|
|
436
|
+
const backgroundTasks = this.backgroundTasks();
|
|
362
437
|
return {
|
|
363
438
|
lines: this.lines,
|
|
364
439
|
status: this.status,
|
|
365
440
|
busy: this.busy,
|
|
366
441
|
queuedInput: this.queuedInput,
|
|
367
|
-
backgroundTaskCount:
|
|
368
|
-
backgroundTasks
|
|
442
|
+
backgroundTaskCount: backgroundTasks.length,
|
|
443
|
+
backgroundTasks,
|
|
369
444
|
backgroundSessionRunCount: this.backgroundSessionRuns.size,
|
|
370
445
|
runningSessionIds: [...this.backgroundSessionRuns.keys()],
|
|
371
446
|
session: this.runtime.engine.snapshot().session,
|
|
@@ -401,6 +476,91 @@ export class WebRepl {
|
|
|
401
476
|
}),
|
|
402
477
|
};
|
|
403
478
|
}
|
|
479
|
+
globalTools() {
|
|
480
|
+
const support = this.runtime.toolSupport;
|
|
481
|
+
if (!support)
|
|
482
|
+
return { items: [] };
|
|
483
|
+
return {
|
|
484
|
+
items: support.catalog.map((tool) => ({
|
|
485
|
+
...tool,
|
|
486
|
+
configuredEnabled: support.globalOverrides[tool.name] ?? true,
|
|
487
|
+
})),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
sessionTools() {
|
|
491
|
+
const support = this.runtime.toolSupport;
|
|
492
|
+
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
493
|
+
if (!support)
|
|
494
|
+
return { sessionId, busy: this.busy, items: [] };
|
|
495
|
+
const activePluginIds = new Set(this.runtime.pluginSupport
|
|
496
|
+
? activeWebRuntimePlugins(this.runtime.pluginSupport.catalog, this.runtime.pluginSupport.overrides).map((plugin) => plugin.id)
|
|
497
|
+
: []);
|
|
498
|
+
return {
|
|
499
|
+
sessionId,
|
|
500
|
+
busy: this.busy,
|
|
501
|
+
items: support.catalog.map((tool) => {
|
|
502
|
+
const override = support.sessionOverrides[tool.name];
|
|
503
|
+
const globallyEnabled = support.globalOverrides[tool.name] ?? true;
|
|
504
|
+
const available = tool.source !== "plugin" || Boolean(tool.pluginId && activePluginIds.has(tool.pluginId));
|
|
505
|
+
return {
|
|
506
|
+
...tool,
|
|
507
|
+
available,
|
|
508
|
+
globallyEnabled,
|
|
509
|
+
mode: override === undefined ? "inherit" : override ? "enabled" : "disabled",
|
|
510
|
+
effectiveEnabled: available && (override ?? globallyEnabled),
|
|
511
|
+
};
|
|
512
|
+
}),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
async setGlobalTools(value) {
|
|
516
|
+
if (this.busy)
|
|
517
|
+
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change global tools while a response is running");
|
|
518
|
+
if (!isRecord(value))
|
|
519
|
+
return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
|
|
520
|
+
const support = this.runtime.toolSupport;
|
|
521
|
+
if (!support)
|
|
522
|
+
return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
|
|
523
|
+
const normalized = this.parseToolOverrides(value, support);
|
|
524
|
+
if (!normalized.ok)
|
|
525
|
+
return normalized;
|
|
526
|
+
try {
|
|
527
|
+
support.globalOverrides = normalized.value;
|
|
528
|
+
this.syncPluginToolRegistration();
|
|
529
|
+
applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
|
|
530
|
+
if (support.persistGlobal)
|
|
531
|
+
await support.persistGlobal(support.globalOverrides);
|
|
532
|
+
await this.refreshToolConfiguration();
|
|
533
|
+
return { ok: true, state: this.globalTools() };
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async setSessionTools(value) {
|
|
540
|
+
if (this.busy)
|
|
541
|
+
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
|
|
542
|
+
if (!isRecord(value))
|
|
543
|
+
return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
|
|
544
|
+
const support = this.runtime.toolSupport;
|
|
545
|
+
if (!support)
|
|
546
|
+
return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
|
|
547
|
+
const normalized = this.parseToolModes(value, support);
|
|
548
|
+
if (!normalized.ok)
|
|
549
|
+
return normalized;
|
|
550
|
+
try {
|
|
551
|
+
await this.applySessionToolOverrides(normalized.value, true);
|
|
552
|
+
return { ok: true, state: this.sessionTools() };
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async loadSessionTools(sessionId) {
|
|
559
|
+
const support = this.runtime.toolSupport;
|
|
560
|
+
if (!support?.resolveSession || !sessionId)
|
|
561
|
+
return;
|
|
562
|
+
await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
|
|
563
|
+
}
|
|
404
564
|
async setSessionPlugins(value) {
|
|
405
565
|
if (this.busy)
|
|
406
566
|
return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
|
|
@@ -435,6 +595,64 @@ export class WebRepl {
|
|
|
435
595
|
return;
|
|
436
596
|
await this.applySessionPluginOverrides(await support.resolve(sessionId), false);
|
|
437
597
|
}
|
|
598
|
+
parseToolOverrides(value, support) {
|
|
599
|
+
const available = new Set(support.catalog.map((tool) => tool.name));
|
|
600
|
+
const overrides = {};
|
|
601
|
+
for (const [name, enabled] of Object.entries(value)) {
|
|
602
|
+
if (!available.has(name))
|
|
603
|
+
return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
|
|
604
|
+
if (typeof enabled !== "boolean")
|
|
605
|
+
return actionFailure("TOOL_INVALID", `invalid tool state: ${name}`);
|
|
606
|
+
overrides[name] = enabled;
|
|
607
|
+
}
|
|
608
|
+
return { ok: true, value: overrides };
|
|
609
|
+
}
|
|
610
|
+
parseToolModes(value, support) {
|
|
611
|
+
const available = new Set(support.catalog.map((tool) => tool.name));
|
|
612
|
+
const overrides = {};
|
|
613
|
+
for (const [name, mode] of Object.entries(value)) {
|
|
614
|
+
if (!available.has(name))
|
|
615
|
+
return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
|
|
616
|
+
if (mode === "enabled" || mode === true)
|
|
617
|
+
overrides[name] = true;
|
|
618
|
+
else if (mode === "disabled" || mode === false)
|
|
619
|
+
overrides[name] = false;
|
|
620
|
+
else if (mode !== "inherit" && mode !== null && mode !== undefined)
|
|
621
|
+
return actionFailure("TOOL_INVALID", `invalid tool mode: ${name}`);
|
|
622
|
+
}
|
|
623
|
+
return { ok: true, value: overrides };
|
|
624
|
+
}
|
|
625
|
+
async applySessionToolOverrides(overrides, persist) {
|
|
626
|
+
const support = this.runtime.toolSupport;
|
|
627
|
+
if (!support)
|
|
628
|
+
return;
|
|
629
|
+
support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
|
|
630
|
+
this.syncPluginToolRegistration();
|
|
631
|
+
applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
|
|
632
|
+
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
633
|
+
if (persist && sessionId && support.persistSession)
|
|
634
|
+
await support.persistSession(sessionId, support.sessionOverrides);
|
|
635
|
+
await this.refreshToolConfiguration();
|
|
636
|
+
}
|
|
637
|
+
syncPluginToolRegistration() {
|
|
638
|
+
const pluginSupport = this.runtime.pluginSupport;
|
|
639
|
+
if (!pluginSupport)
|
|
640
|
+
return;
|
|
641
|
+
for (const plugin of pluginSupport.catalog)
|
|
642
|
+
for (const tool of plugin.tools)
|
|
643
|
+
this.runtime.tools.unregister(tool.name);
|
|
644
|
+
for (const plugin of activeWebRuntimePlugins(pluginSupport.catalog, pluginSupport.overrides)) {
|
|
645
|
+
for (const tool of plugin.tools)
|
|
646
|
+
this.runtime.tools.register(tool);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async refreshToolConfiguration() {
|
|
650
|
+
const metrics = await this.runtime.engine.contextMetrics();
|
|
651
|
+
this.runtime.initialMetrics = metrics;
|
|
652
|
+
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
653
|
+
this.broadcastSync();
|
|
654
|
+
this.publishRuntimeContext();
|
|
655
|
+
}
|
|
438
656
|
async applySessionPluginOverrides(overrides, persist) {
|
|
439
657
|
const support = this.runtime.pluginSupport;
|
|
440
658
|
if (!support)
|
|
@@ -448,6 +666,9 @@ export class WebRepl {
|
|
|
448
666
|
for (const tool of plugin.tools)
|
|
449
667
|
this.runtime.tools.register(tool);
|
|
450
668
|
support.overrides = normalized;
|
|
669
|
+
const toolSupport = this.runtime.toolSupport;
|
|
670
|
+
if (toolSupport)
|
|
671
|
+
applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
|
|
451
672
|
this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
|
|
452
673
|
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
453
674
|
if (persist && sessionId && support.persist)
|
|
@@ -474,6 +695,7 @@ export class WebRepl {
|
|
|
474
695
|
this.broadcastSync();
|
|
475
696
|
return { ok: true };
|
|
476
697
|
}
|
|
698
|
+
this.publishRuntimeContext();
|
|
477
699
|
this.startRun(text, attachments);
|
|
478
700
|
return { ok: true };
|
|
479
701
|
}
|
|
@@ -532,7 +754,7 @@ export class WebRepl {
|
|
|
532
754
|
const snapshot = this.runtime.engine.snapshot().session;
|
|
533
755
|
if (!snapshot)
|
|
534
756
|
throw new Error("session transcripts are disabled");
|
|
535
|
-
await this.loadSessionPlugins(snapshot.sessionId);
|
|
757
|
+
await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
|
|
536
758
|
await this.refreshSessionView();
|
|
537
759
|
return { ok: true };
|
|
538
760
|
}
|
|
@@ -549,7 +771,7 @@ export class WebRepl {
|
|
|
549
771
|
const snapshot = this.runtime.engine.snapshot().session;
|
|
550
772
|
if (!snapshot)
|
|
551
773
|
throw new Error("session transcripts are disabled");
|
|
552
|
-
await this.loadSessionPlugins(snapshot.sessionId);
|
|
774
|
+
await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
|
|
553
775
|
await this.refreshSessionView();
|
|
554
776
|
return { ok: true };
|
|
555
777
|
}
|
|
@@ -628,22 +850,28 @@ export class WebRepl {
|
|
|
628
850
|
imagePayload(messageId, blockIndex) {
|
|
629
851
|
if (!messageId || !Number.isInteger(blockIndex) || blockIndex < 0)
|
|
630
852
|
return undefined;
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (!
|
|
853
|
+
let block;
|
|
854
|
+
for (const entry of this.runtime.engine.getDisplayEntries()) {
|
|
855
|
+
if (entry.type !== "message" || entry.message.id !== messageId)
|
|
856
|
+
continue;
|
|
857
|
+
const candidate = entry.message.blocks[blockIndex];
|
|
858
|
+
if (candidate?.type === "image")
|
|
859
|
+
block = candidate;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
return block ? imagePayloadForBlock(block) : undefined;
|
|
863
|
+
}
|
|
864
|
+
imagePayloadById(imageId) {
|
|
865
|
+
if (!imageId)
|
|
644
866
|
return undefined;
|
|
645
|
-
const
|
|
646
|
-
|
|
867
|
+
for (const entry of this.runtime.engine.getDisplayEntries()) {
|
|
868
|
+
if (entry.type !== "message")
|
|
869
|
+
continue;
|
|
870
|
+
const block = entry.message.blocks.find((candidate) => candidate.type === "image" && candidate.imageId === imageId);
|
|
871
|
+
if (block)
|
|
872
|
+
return imagePayloadForBlock(block);
|
|
873
|
+
}
|
|
874
|
+
return undefined;
|
|
647
875
|
}
|
|
648
876
|
async setFastMode(enabled) {
|
|
649
877
|
try {
|
|
@@ -655,6 +883,25 @@ export class WebRepl {
|
|
|
655
883
|
return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
656
884
|
}
|
|
657
885
|
}
|
|
886
|
+
async setContextWindowK(value) {
|
|
887
|
+
if (this.busy)
|
|
888
|
+
return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
|
|
889
|
+
if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
|
|
890
|
+
return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
|
|
891
|
+
}
|
|
892
|
+
const contextWindowK = Number(value);
|
|
893
|
+
const contextWindowTokens = contextWindowK * 1000;
|
|
894
|
+
try {
|
|
895
|
+
await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
|
|
896
|
+
const metrics = await this.runtime.engine.contextMetrics();
|
|
897
|
+
this.runtime.initialMetrics = metrics;
|
|
898
|
+
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
899
|
+
return { ok: true, contextWindowK, contextWindowTokens };
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
658
905
|
loginForm(providerValue) {
|
|
659
906
|
const provider = parseLoginProvider(providerValue);
|
|
660
907
|
return createLoginFormPayload(this.runtime.envPath, provider);
|
|
@@ -711,6 +958,12 @@ export class WebRepl {
|
|
|
711
958
|
this.broadcastSync();
|
|
712
959
|
return id;
|
|
713
960
|
}
|
|
961
|
+
appendCompaction(report, createdAt) {
|
|
962
|
+
this.lines = this.lines.map((line) => line.compaction
|
|
963
|
+
? { ...line, compaction: { ...line.compaction, current: false } }
|
|
964
|
+
: line);
|
|
965
|
+
return this.append(compactionLine(report, createdAt, true));
|
|
966
|
+
}
|
|
714
967
|
appendLineText(id, text) {
|
|
715
968
|
this.lines = this.lines.map((line) => line.id === id ? { ...line, text: line.text + text } : line);
|
|
716
969
|
this.broadcastDelta({ type: "line.text.append", id, text });
|
|
@@ -749,10 +1002,10 @@ export class WebRepl {
|
|
|
749
1002
|
this.interruptArmed = false;
|
|
750
1003
|
this.queuedInput = undefined;
|
|
751
1004
|
this.queuedAttachments = undefined;
|
|
752
|
-
this.
|
|
1005
|
+
this.pendingUserImageEchoIds = undefined;
|
|
753
1006
|
this.pendingUserImageEchoMessageId = undefined;
|
|
754
1007
|
this.finalizeForegroundView();
|
|
755
|
-
this.
|
|
1008
|
+
this.cancelLiveToolLines(reason);
|
|
756
1009
|
this.busy = false;
|
|
757
1010
|
this.status = { ...this.status, phase: "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
|
|
758
1011
|
this.broadcastSync();
|
|
@@ -765,10 +1018,15 @@ export class WebRepl {
|
|
|
765
1018
|
kind: "agent",
|
|
766
1019
|
taskId: task.taskId,
|
|
767
1020
|
agentId: task.agentId,
|
|
1021
|
+
agentType: task.agentType,
|
|
768
1022
|
type: task.type,
|
|
769
1023
|
status: task.status,
|
|
770
1024
|
description: task.description,
|
|
1025
|
+
prompt: task.prompt,
|
|
1026
|
+
progress: task.progress,
|
|
1027
|
+
outputFile: task.outputFile,
|
|
771
1028
|
createdAt: task.createdAt,
|
|
1029
|
+
updatedAt: task.updatedAt,
|
|
772
1030
|
}));
|
|
773
1031
|
const terminalTasks = this.runtime.execProcessManager.list()
|
|
774
1032
|
.filter((terminal) => terminal.status === "running" && terminal.backgrounded)
|
|
@@ -786,8 +1044,26 @@ export class WebRepl {
|
|
|
786
1044
|
shell: terminal.shell,
|
|
787
1045
|
tty: terminal.tty,
|
|
788
1046
|
durationMs: terminal.duration_ms,
|
|
1047
|
+
output: terminal.output,
|
|
1048
|
+
outputEnd: terminal.outputEnd,
|
|
1049
|
+
}));
|
|
1050
|
+
const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
|
|
1051
|
+
kind: "session",
|
|
1052
|
+
taskId: `session:${run.sessionId}`,
|
|
1053
|
+
type: "会话",
|
|
1054
|
+
status: "running",
|
|
1055
|
+
description: run.title || "未命名会话",
|
|
1056
|
+
createdAt: run.startedAt,
|
|
1057
|
+
sessionId: run.sessionId,
|
|
789
1058
|
}));
|
|
790
|
-
|
|
1059
|
+
const createdAtTime = (value) => {
|
|
1060
|
+
if (typeof value === "number")
|
|
1061
|
+
return value;
|
|
1062
|
+
const parsed = Date.parse(value);
|
|
1063
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1064
|
+
};
|
|
1065
|
+
return [...agentTasks, ...terminalTasks, ...sessionTasks]
|
|
1066
|
+
.sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
|
|
791
1067
|
}
|
|
792
1068
|
async detachRunningForeground(reason) {
|
|
793
1069
|
if (!this.busy)
|
|
@@ -816,7 +1092,7 @@ export class WebRepl {
|
|
|
816
1092
|
this.interruptArmed = false;
|
|
817
1093
|
this.queuedInput = undefined;
|
|
818
1094
|
this.queuedAttachments = undefined;
|
|
819
|
-
this.
|
|
1095
|
+
this.pendingUserImageEchoIds = undefined;
|
|
820
1096
|
this.pendingUserImageEchoMessageId = undefined;
|
|
821
1097
|
this.busy = false;
|
|
822
1098
|
this.status = { ...this.status, phase: "ready", detail: undefined };
|
|
@@ -858,11 +1134,145 @@ export class WebRepl {
|
|
|
858
1134
|
this.finalizeLiveLine(id);
|
|
859
1135
|
this.liveToolLineIds.clear();
|
|
860
1136
|
}
|
|
1137
|
+
cancelLiveToolLines(reason) {
|
|
1138
|
+
for (const id of this.liveToolLineIds.values()) {
|
|
1139
|
+
const line = this.lines.find((item) => item.id === id);
|
|
1140
|
+
const steps = (line?.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "failed", phase: "cancelled" } : step);
|
|
1141
|
+
this.lines = this.lines.map((item) => item.id === id ? { ...item, live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...item.toolStream, phase: "cancelled", message: reason, steps } } : item);
|
|
1142
|
+
this.queueDeltaOperation({ type: "line.patch", id, patch: { live: false, kind: "error", titleStatus: "failure", bodyTitle: reason, toolStream: { ...line?.toolStream, phase: "cancelled", message: reason, steps } } });
|
|
1143
|
+
}
|
|
1144
|
+
this.liveToolLineIds.clear();
|
|
1145
|
+
}
|
|
1146
|
+
patchLiveToolLine(toolUseId, patch) {
|
|
1147
|
+
const id = this.liveToolLineIds.get(toolUseId);
|
|
1148
|
+
if (id === undefined)
|
|
1149
|
+
return;
|
|
1150
|
+
const line = this.lines.find((item) => item.id === id);
|
|
1151
|
+
if (!line)
|
|
1152
|
+
return;
|
|
1153
|
+
this.lines = this.lines.map((item) => item.id === id ? { ...item, ...patch } : item);
|
|
1154
|
+
this.queueDeltaOperation({ type: "line.patch", id, patch });
|
|
1155
|
+
}
|
|
1156
|
+
applyToolProgress(event) {
|
|
1157
|
+
const id = this.liveToolLineIds.get(event.toolUse.id);
|
|
1158
|
+
if (id === undefined)
|
|
1159
|
+
return;
|
|
1160
|
+
const line = this.lines.find((item) => item.id === id);
|
|
1161
|
+
if (!line)
|
|
1162
|
+
return;
|
|
1163
|
+
const progress = event.progress;
|
|
1164
|
+
const previousStream = line.toolStream ?? {};
|
|
1165
|
+
if (progress.sequence !== undefined && previousStream.sequence !== undefined && progress.sequence <= previousStream.sequence)
|
|
1166
|
+
return;
|
|
1167
|
+
const channel = progress.channel ?? inferToolStreamChannel(progress.data);
|
|
1168
|
+
const operation = progress.operation ?? (channel === "stdout" || channel === "stderr" ? "append" : "replace");
|
|
1169
|
+
const stream = { ...previousStream };
|
|
1170
|
+
stream.channel = channel;
|
|
1171
|
+
stream.phase = progress.phase;
|
|
1172
|
+
stream.sequence = progress.sequence ?? stream.sequence;
|
|
1173
|
+
if (progress.progress) {
|
|
1174
|
+
stream.current = progress.progress.current;
|
|
1175
|
+
stream.total = progress.progress.total;
|
|
1176
|
+
stream.unit = progress.progress.unit;
|
|
1177
|
+
}
|
|
1178
|
+
const terminalDelta = terminalOutputDelta(progress.data);
|
|
1179
|
+
const textDelta = toolStreamTextDelta(progress.data);
|
|
1180
|
+
if (terminalDelta) {
|
|
1181
|
+
const key = terminalDelta.stream === "stderr" ? "stderr" : "stdout";
|
|
1182
|
+
stream[key] = operation === "replace"
|
|
1183
|
+
? trimToolStreamText(terminalDelta.text, 40_000)
|
|
1184
|
+
: appendToolStreamText(stream[key], terminalDelta.text, 40_000);
|
|
1185
|
+
stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, key, terminalDelta.text, operation);
|
|
1186
|
+
}
|
|
1187
|
+
else if ((channel === "stdout" || channel === "stderr") && textDelta !== undefined) {
|
|
1188
|
+
stream[channel] = operation === "replace"
|
|
1189
|
+
? trimToolStreamText(textDelta, 40_000)
|
|
1190
|
+
: appendToolStreamText(stream[channel], textDelta, 40_000);
|
|
1191
|
+
stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, channel, textDelta, operation);
|
|
1192
|
+
}
|
|
1193
|
+
else if ((channel === "patch" || channel === "artifact") && textDelta !== undefined) {
|
|
1194
|
+
stream.output = operation === "replace"
|
|
1195
|
+
? trimToolStreamText(textDelta, 40_000)
|
|
1196
|
+
: appendToolStreamText(stream.output, textDelta, 40_000);
|
|
1197
|
+
stream.chunks = appendToolStreamChunk(stream.chunks, progress.sequence, "output", textDelta, operation);
|
|
1198
|
+
stream.message = progress.message;
|
|
1199
|
+
}
|
|
1200
|
+
else if (channel === "item") {
|
|
1201
|
+
const key = progress.key || progress.phase || `step-${progress.sequence ?? Date.now()}`;
|
|
1202
|
+
const steps = [...(stream.steps ?? [])];
|
|
1203
|
+
const index = steps.findIndex((step) => step.key === key);
|
|
1204
|
+
if (operation === "remove") {
|
|
1205
|
+
if (index >= 0)
|
|
1206
|
+
steps.splice(index, 1);
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
const status = toolStreamStepStatus(progress.phase);
|
|
1210
|
+
const message = operation === "append" && index >= 0
|
|
1211
|
+
? `${steps[index].message}${progress.message}`
|
|
1212
|
+
: progress.message;
|
|
1213
|
+
const stepToolName = childToolName(progress.data) ?? steps[index]?.toolName;
|
|
1214
|
+
const step = {
|
|
1215
|
+
key,
|
|
1216
|
+
message,
|
|
1217
|
+
toolName: stepToolName,
|
|
1218
|
+
toolLabel: stepToolName ? toolTitle(stepToolName, "running") : steps[index]?.toolLabel,
|
|
1219
|
+
status,
|
|
1220
|
+
phase: progress.phase,
|
|
1221
|
+
sequence: progress.sequence,
|
|
1222
|
+
};
|
|
1223
|
+
if (index >= 0)
|
|
1224
|
+
steps[index] = step;
|
|
1225
|
+
else
|
|
1226
|
+
steps.push(step);
|
|
1227
|
+
}
|
|
1228
|
+
stream.steps = steps.slice(-24);
|
|
1229
|
+
stream.message = progress.message;
|
|
1230
|
+
}
|
|
1231
|
+
else if (operation === "append") {
|
|
1232
|
+
stream.message = `${stream.message ?? ""}${progress.message}`;
|
|
1233
|
+
}
|
|
1234
|
+
else if (operation === "remove") {
|
|
1235
|
+
stream.message = undefined;
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
stream.message = progress.message;
|
|
1239
|
+
}
|
|
1240
|
+
this.patchLiveToolLine(event.toolUse.id, { toolStream: stream });
|
|
1241
|
+
}
|
|
1242
|
+
applyAvailableToolResult(event) {
|
|
1243
|
+
const liveLineId = this.liveToolLineIds.get(event.toolUse.id);
|
|
1244
|
+
if (liveLineId === undefined)
|
|
1245
|
+
return;
|
|
1246
|
+
const liveLine = this.lines.find((item) => item.id === liveLineId);
|
|
1247
|
+
if (!liveLine)
|
|
1248
|
+
return;
|
|
1249
|
+
let replacement;
|
|
1250
|
+
for (const message of event.messages) {
|
|
1251
|
+
renderToolResultMessage(message, (line, toolUseId) => {
|
|
1252
|
+
if (toolUseId === event.toolUse.id)
|
|
1253
|
+
replacement = line;
|
|
1254
|
+
return liveLineId;
|
|
1255
|
+
}, runtimeToolResultPresenter(this.runtime));
|
|
1256
|
+
}
|
|
1257
|
+
const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: event.ok ? "completed" : "failed" } : step);
|
|
1258
|
+
if (replacement) {
|
|
1259
|
+
this.replaceLine(liveLineId, {
|
|
1260
|
+
...replacement,
|
|
1261
|
+
live: false,
|
|
1262
|
+
presentationLevel: mergePresentationLevel(replacement.presentationLevel, liveLine.presentationLevel),
|
|
1263
|
+
toolDisplay: mergeToolDisplays(replacement.toolDisplay, liveLine.toolDisplay),
|
|
1264
|
+
toolStream: { ...liveLine.toolStream, steps, message: liveLine.toolStream?.message },
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
this.replaceLine(liveLineId, { live: false, titleStatus: event.ok ? "success" : "failure", toolStream: { ...liveLine.toolStream, steps } });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
861
1271
|
handleEvent(event) {
|
|
862
1272
|
this.reduce(event);
|
|
863
1273
|
if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
|
|
864
1274
|
const messageId = this.pendingUserImageEchoMessageId;
|
|
865
|
-
this.
|
|
1275
|
+
this.pendingUserImageEchoIds = undefined;
|
|
866
1276
|
this.pendingUserImageEchoMessageId = undefined;
|
|
867
1277
|
renderMessageImages(event.message, (line) => this.append(line), messageId);
|
|
868
1278
|
this.broadcastSync();
|
|
@@ -872,6 +1282,10 @@ export class WebRepl {
|
|
|
872
1282
|
this.queueDeltaStatus();
|
|
873
1283
|
return;
|
|
874
1284
|
}
|
|
1285
|
+
if (event.type === "context.compacted") {
|
|
1286
|
+
this.appendCompaction(event.compaction);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
875
1289
|
if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
|
|
876
1290
|
this.broadcastSync();
|
|
877
1291
|
return;
|
|
@@ -890,6 +1304,18 @@ export class WebRepl {
|
|
|
890
1304
|
this.appendLineText(id, event.text);
|
|
891
1305
|
return;
|
|
892
1306
|
}
|
|
1307
|
+
if (event.type === "tool.progress") {
|
|
1308
|
+
this.applyToolProgress(event);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (event.type === "tool.result.available") {
|
|
1312
|
+
this.applyAvailableToolResult(event);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (event.type === "tool.batch.completed") {
|
|
1316
|
+
this.broadcastSync();
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
893
1319
|
if (event.type === "message") {
|
|
894
1320
|
let replacedStreamingContent = false;
|
|
895
1321
|
if (event.message.role === "assistant" && this.assistantLineId !== undefined) {
|
|
@@ -920,12 +1346,19 @@ export class WebRepl {
|
|
|
920
1346
|
if (event.message.role === "tool_result") {
|
|
921
1347
|
renderToolResultMessage(event.message, (line, toolUseId) => {
|
|
922
1348
|
const liveLineId = this.liveToolLineIds.get(toolUseId);
|
|
923
|
-
if (liveLineId === undefined)
|
|
1349
|
+
if (liveLineId === undefined) {
|
|
1350
|
+
const existing = this.lines.find((item) => item.toolUseId === toolUseId);
|
|
1351
|
+
if (existing) {
|
|
1352
|
+
this.replaceLine(existing.id, { ...line, toolStream: existing.toolStream, presentationLevel: mergePresentationLevel(line.presentationLevel, existing.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, existing.toolDisplay) });
|
|
1353
|
+
return existing.id;
|
|
1354
|
+
}
|
|
924
1355
|
return this.append(line);
|
|
925
|
-
|
|
1356
|
+
}
|
|
1357
|
+
const liveLine = this.lines.find((item) => item.id === liveLineId);
|
|
1358
|
+
this.replaceLine(liveLineId, { ...line, toolStream: liveLine?.toolStream, presentationLevel: mergePresentationLevel(line.presentationLevel, liveLine?.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, liveLine?.toolDisplay) });
|
|
926
1359
|
this.liveToolLineIds.delete(toolUseId);
|
|
927
1360
|
return liveLineId;
|
|
928
|
-
});
|
|
1361
|
+
}, runtimeToolResultPresenter(this.runtime));
|
|
929
1362
|
renderMessageImages(event.message, (line) => this.append(line));
|
|
930
1363
|
return;
|
|
931
1364
|
}
|
|
@@ -946,16 +1379,15 @@ export class WebRepl {
|
|
|
946
1379
|
if (event.type === "tool.started") {
|
|
947
1380
|
this.finalizeLiveLine(this.assistantLineId);
|
|
948
1381
|
this.finalizeThinkingLine();
|
|
1382
|
+
this.assistantLineId = undefined;
|
|
949
1383
|
const id = this.append({ ...formatToolUse(event.toolUse), live: true });
|
|
950
1384
|
this.liveToolLineIds.set(event.toolUse.id, id);
|
|
951
1385
|
return;
|
|
952
1386
|
}
|
|
953
1387
|
if (event.type === "tool.finished") {
|
|
954
1388
|
const id = this.liveToolLineIds.get(event.toolUse.id);
|
|
955
|
-
if (id !== undefined)
|
|
1389
|
+
if (id !== undefined)
|
|
956
1390
|
this.finalizeLiveLine(id);
|
|
957
|
-
this.liveToolLineIds.delete(event.toolUse.id);
|
|
958
|
-
}
|
|
959
1391
|
return;
|
|
960
1392
|
}
|
|
961
1393
|
if (event.type === "terminal") {
|
|
@@ -970,13 +1402,13 @@ export class WebRepl {
|
|
|
970
1402
|
this.append({ kind: "error", text: event.error.message });
|
|
971
1403
|
}
|
|
972
1404
|
matchesPendingUserImageEcho(message) {
|
|
973
|
-
const pending = this.
|
|
1405
|
+
const pending = this.pendingUserImageEchoIds;
|
|
974
1406
|
if (!pending?.length || message.role !== "user")
|
|
975
1407
|
return false;
|
|
976
|
-
const
|
|
977
|
-
.filter((block) => block.type === "image" && typeof block.
|
|
978
|
-
.map((block) => block.
|
|
979
|
-
return
|
|
1408
|
+
const imageIds = message.blocks
|
|
1409
|
+
.filter((block) => block.type === "image" && typeof block.imageId === "string")
|
|
1410
|
+
.map((block) => block.imageId);
|
|
1411
|
+
return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
|
|
980
1412
|
}
|
|
981
1413
|
async handleCommandOrPrompt(text, attachments = []) {
|
|
982
1414
|
const command = parseReplCommand(text);
|
|
@@ -1078,11 +1510,11 @@ export class WebRepl {
|
|
|
1078
1510
|
const promptPayload = buildWebPromptPayload(command.text, attachments);
|
|
1079
1511
|
const optimisticMessageId = `web-user-${this.lineId + 1}`;
|
|
1080
1512
|
this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
|
|
1081
|
-
const
|
|
1082
|
-
.filter((block) => block.type === "image" && typeof block.
|
|
1083
|
-
.map((block) => block.
|
|
1084
|
-
this.
|
|
1085
|
-
this.pendingUserImageEchoMessageId =
|
|
1513
|
+
const optimisticImageIds = (promptPayload.blocks ?? [])
|
|
1514
|
+
.filter((block) => block.type === "image" && typeof block.imageId === "string")
|
|
1515
|
+
.map((block) => block.imageId);
|
|
1516
|
+
this.pendingUserImageEchoIds = optimisticImageIds.length ? optimisticImageIds : undefined;
|
|
1517
|
+
this.pendingUserImageEchoMessageId = optimisticImageIds.length ? optimisticMessageId : undefined;
|
|
1086
1518
|
const runToken = ++this.foregroundRunToken;
|
|
1087
1519
|
const abortController = new AbortController();
|
|
1088
1520
|
this.activeAbortController = abortController;
|
|
@@ -1091,7 +1523,12 @@ export class WebRepl {
|
|
|
1091
1523
|
this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
|
|
1092
1524
|
const engine = this.runtime.engine;
|
|
1093
1525
|
try {
|
|
1094
|
-
for await (const event of engine.sendUserText(promptPayload.text, {
|
|
1526
|
+
for await (const event of engine.sendUserText(promptPayload.text, {
|
|
1527
|
+
abortSignal: abortController.signal,
|
|
1528
|
+
blocks: promptPayload.blocks,
|
|
1529
|
+
displayText: promptPayload.displayText,
|
|
1530
|
+
stopAfterTurn: () => this.queuedInput !== undefined,
|
|
1531
|
+
})) {
|
|
1095
1532
|
if (this.foregroundRunToken !== runToken)
|
|
1096
1533
|
continue;
|
|
1097
1534
|
if (this.runtime.engine !== engine)
|
|
@@ -1122,7 +1559,7 @@ export class WebRepl {
|
|
|
1122
1559
|
this.activeAbortController = undefined;
|
|
1123
1560
|
this.interruptArmed = false;
|
|
1124
1561
|
this.finalizeForegroundView();
|
|
1125
|
-
this.
|
|
1562
|
+
this.pendingUserImageEchoIds = undefined;
|
|
1126
1563
|
this.pendingUserImageEchoMessageId = undefined;
|
|
1127
1564
|
const queuedText = this.queuedInput;
|
|
1128
1565
|
const queuedAttach = this.queuedAttachments;
|
|
@@ -1155,7 +1592,10 @@ export class WebRepl {
|
|
|
1155
1592
|
const metrics = await this.runtime.engine.contextMetrics();
|
|
1156
1593
|
if (this.foregroundRunToken !== runToken)
|
|
1157
1594
|
return;
|
|
1158
|
-
|
|
1595
|
+
if (result.report)
|
|
1596
|
+
this.appendCompaction(result.report);
|
|
1597
|
+
else
|
|
1598
|
+
this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
|
|
1159
1599
|
this.handleEvent({ type: "context.metrics", metrics });
|
|
1160
1600
|
}
|
|
1161
1601
|
catch (error) {
|
|
@@ -1174,7 +1614,9 @@ export class WebRepl {
|
|
|
1174
1614
|
}
|
|
1175
1615
|
send(subscriber, event, data) {
|
|
1176
1616
|
if (subscriber.blocked) {
|
|
1177
|
-
if (event === "
|
|
1617
|
+
if (event === "client.reload")
|
|
1618
|
+
subscriber.pendingClientReload = data;
|
|
1619
|
+
else if (event === "runtime.context")
|
|
1178
1620
|
subscriber.needsRuntimeContext = true;
|
|
1179
1621
|
else
|
|
1180
1622
|
subscriber.needsSync = true;
|
|
@@ -1189,6 +1631,11 @@ export class WebRepl {
|
|
|
1189
1631
|
subscriber.blocked = false;
|
|
1190
1632
|
if (subscriber.response.destroyed)
|
|
1191
1633
|
return;
|
|
1634
|
+
if (subscriber.pendingClientReload) {
|
|
1635
|
+
const payload = subscriber.pendingClientReload;
|
|
1636
|
+
subscriber.pendingClientReload = undefined;
|
|
1637
|
+
this.send(subscriber, "client.reload", payload);
|
|
1638
|
+
}
|
|
1192
1639
|
if (subscriber.needsSync) {
|
|
1193
1640
|
subscriber.needsSync = false;
|
|
1194
1641
|
this.send(subscriber, "sync", this.snapshot(false));
|
|
@@ -1204,6 +1651,7 @@ export class WebRepl {
|
|
|
1204
1651
|
return createWebRuntimeContextPayload(snapshot, {
|
|
1205
1652
|
revision,
|
|
1206
1653
|
sessionId: this.runtime.engine.snapshot().session?.sessionId,
|
|
1654
|
+
toolPresentations: Object.fromEntries((this.runtime.toolSupport?.catalog ?? []).map((tool) => [tool.name, tool.presentation])),
|
|
1207
1655
|
});
|
|
1208
1656
|
}
|
|
1209
1657
|
publishRuntimeContext(targets) {
|
|
@@ -1264,6 +1712,39 @@ export class WebRepl {
|
|
|
1264
1712
|
this.pendingDeltaOperations = [];
|
|
1265
1713
|
this.pendingDeltaStatus = false;
|
|
1266
1714
|
}
|
|
1715
|
+
dropInactiveTerminalOutput() {
|
|
1716
|
+
for (const sessionId of this.pendingTerminalOutput.keys()) {
|
|
1717
|
+
if (!this.runtime.execProcessManager.isBackgroundRunning(sessionId))
|
|
1718
|
+
this.pendingTerminalOutput.delete(sessionId);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
queueTerminalOutput(delta) {
|
|
1722
|
+
if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
|
|
1723
|
+
return;
|
|
1724
|
+
const outputStart = Number(delta.outputStart ?? 0);
|
|
1725
|
+
const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
|
|
1726
|
+
const pending = this.pendingTerminalOutput.get(delta.sessionId);
|
|
1727
|
+
if (pending && pending.outputEnd === outputStart) {
|
|
1728
|
+
pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
|
|
1729
|
+
pending.outputEnd = outputEnd;
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
|
|
1733
|
+
}
|
|
1734
|
+
if (this.terminalOutputTimer)
|
|
1735
|
+
return;
|
|
1736
|
+
this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
|
|
1737
|
+
this.terminalOutputTimer.unref?.();
|
|
1738
|
+
}
|
|
1739
|
+
flushTerminalOutput() {
|
|
1740
|
+
this.terminalOutputTimer = undefined;
|
|
1741
|
+
if (!this.pendingTerminalOutput.size)
|
|
1742
|
+
return;
|
|
1743
|
+
const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
|
|
1744
|
+
this.pendingTerminalOutput.clear();
|
|
1745
|
+
for (const subscriber of this.subscribers)
|
|
1746
|
+
this.send(subscriber, "terminal.output", { updates });
|
|
1747
|
+
}
|
|
1267
1748
|
broadcastSync() {
|
|
1268
1749
|
this.discardPendingDeltaOperations();
|
|
1269
1750
|
const payload = this.snapshot(false);
|
|
@@ -1271,6 +1752,10 @@ export class WebRepl {
|
|
|
1271
1752
|
this.send(subscriber, "sync", payload);
|
|
1272
1753
|
}
|
|
1273
1754
|
}
|
|
1755
|
+
function appendBoundedText(current, incoming, limit) {
|
|
1756
|
+
const combined = current + incoming;
|
|
1757
|
+
return combined.length <= limit ? combined : combined.slice(-limit);
|
|
1758
|
+
}
|
|
1274
1759
|
export function compactLineOperations(operations) {
|
|
1275
1760
|
const compacted = [];
|
|
1276
1761
|
for (const operation of operations) {
|
|
@@ -1303,20 +1788,67 @@ async function route(req, res, router) {
|
|
|
1303
1788
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
1304
1789
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
1305
1790
|
return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
|
|
1791
|
+
if (req.method === "GET" && url.pathname === "/api/client-info")
|
|
1792
|
+
return sendJson(res, router.clientInfo());
|
|
1793
|
+
if (req.method === "POST" && url.pathname === "/api/client-reload") {
|
|
1794
|
+
const body = await readJsonBody(req);
|
|
1795
|
+
return sendJson(res, await router.requestClientReload(body));
|
|
1796
|
+
}
|
|
1306
1797
|
const scope = webRuntimeScopeFromUrl(url);
|
|
1307
1798
|
const repl = await router.get(scope);
|
|
1308
1799
|
if (req.method === "GET" && url.pathname === "/events")
|
|
1309
|
-
return repl.subscribe(res);
|
|
1800
|
+
return repl.subscribe(res, router.clientInfo());
|
|
1310
1801
|
if (req.method === "GET" && url.pathname === "/api/state")
|
|
1311
1802
|
return sendJson(res, repl.snapshot(true));
|
|
1312
1803
|
if (req.method === "GET" && url.pathname === "/api/runtime-context")
|
|
1313
1804
|
return sendJson(res, await repl.runtimeContext());
|
|
1805
|
+
const workspaceRepl = repl;
|
|
1806
|
+
if (req.method === "GET" && url.pathname === "/api/cwd") {
|
|
1807
|
+
return sendJson(res, workspaceRepl.browseWorkspace
|
|
1808
|
+
? await workspaceRepl.browseWorkspace(url.searchParams.get("path") ?? undefined)
|
|
1809
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1810
|
+
}
|
|
1811
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/change") {
|
|
1812
|
+
const body = await readJsonBody(req);
|
|
1813
|
+
return sendJson(res, workspaceRepl.changeWorkspace
|
|
1814
|
+
? await workspaceRepl.changeWorkspace(body.path)
|
|
1815
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1816
|
+
}
|
|
1817
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/create") {
|
|
1818
|
+
const body = await readJsonBody(req);
|
|
1819
|
+
return sendJson(res, workspaceRepl.createWorkspaceDirectory
|
|
1820
|
+
? await workspaceRepl.createWorkspaceDirectory(body.path)
|
|
1821
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1822
|
+
}
|
|
1823
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/delete") {
|
|
1824
|
+
const body = await readJsonBody(req);
|
|
1825
|
+
return sendJson(res, workspaceRepl.deleteWorkspaceDirectory
|
|
1826
|
+
? await workspaceRepl.deleteWorkspaceDirectory(body.path)
|
|
1827
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1828
|
+
}
|
|
1829
|
+
if (req.method === "GET" && url.pathname === "/api/tools")
|
|
1830
|
+
return sendJson(res, repl.globalTools());
|
|
1831
|
+
if (req.method === "POST" && url.pathname === "/api/tools/global") {
|
|
1832
|
+
const body = await readJsonBody(req);
|
|
1833
|
+
return sendJson(res, await repl.setGlobalTools(body.overrides));
|
|
1834
|
+
}
|
|
1835
|
+
if (req.method === "GET" && url.pathname === "/api/session-tools")
|
|
1836
|
+
return sendJson(res, repl.sessionTools());
|
|
1837
|
+
if (req.method === "POST" && url.pathname === "/api/session-tools") {
|
|
1838
|
+
const body = await readJsonBody(req);
|
|
1839
|
+
return sendJson(res, await repl.setSessionTools(body.overrides));
|
|
1840
|
+
}
|
|
1314
1841
|
if (req.method === "GET" && url.pathname === "/api/session-plugins")
|
|
1315
1842
|
return sendJson(res, repl.sessionPlugins());
|
|
1316
1843
|
if (req.method === "POST" && url.pathname === "/api/session-plugins") {
|
|
1317
1844
|
const body = await readJsonBody(req);
|
|
1318
1845
|
return sendJson(res, await repl.setSessionPlugins(body.overrides));
|
|
1319
1846
|
}
|
|
1847
|
+
const imageIdMatch = /^\/api\/images\/by-id\/([^/]+)$/u.exec(url.pathname);
|
|
1848
|
+
if (req.method === "GET" && imageIdMatch) {
|
|
1849
|
+
const payload = repl.imagePayloadById(decodeURIComponent(imageIdMatch[1]));
|
|
1850
|
+
return payload ? sendImage(res, payload) : sendJson(res, { error: "image not found" }, 404);
|
|
1851
|
+
}
|
|
1320
1852
|
const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
|
|
1321
1853
|
if (req.method === "GET" && imageMatch) {
|
|
1322
1854
|
const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
|
|
@@ -1332,6 +1864,10 @@ async function route(req, res, router) {
|
|
|
1332
1864
|
const body = await readJsonBody(req);
|
|
1333
1865
|
return sendJson(res, await repl.setFastMode(body.enabled === true));
|
|
1334
1866
|
}
|
|
1867
|
+
if (req.method === "POST" && url.pathname === "/api/context-window") {
|
|
1868
|
+
const body = await readJsonBody(req);
|
|
1869
|
+
return sendJson(res, await repl.setContextWindowK(body.value));
|
|
1870
|
+
}
|
|
1335
1871
|
if (req.method === "POST" && url.pathname === "/api/submit") {
|
|
1336
1872
|
const body = await readJsonBody(req);
|
|
1337
1873
|
return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
@@ -1393,6 +1929,20 @@ function sendJson(res, value, status = 200) {
|
|
|
1393
1929
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
1394
1930
|
res.end(JSON.stringify(value));
|
|
1395
1931
|
}
|
|
1932
|
+
function imagePayloadForBlock(block) {
|
|
1933
|
+
const resolved = resolveImageBlockDataSync(block)?.trim();
|
|
1934
|
+
if (!resolved)
|
|
1935
|
+
return undefined;
|
|
1936
|
+
const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/iu.exec(resolved);
|
|
1937
|
+
const mimeType = (dataUrl?.[1] ?? block.mimeType).toLowerCase();
|
|
1938
|
+
if (!mimeType.startsWith("image/"))
|
|
1939
|
+
return undefined;
|
|
1940
|
+
const base64 = (dataUrl?.[2] ?? resolved).replace(/\s+/gu, "");
|
|
1941
|
+
if (!base64 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
|
|
1942
|
+
return undefined;
|
|
1943
|
+
const body = Buffer.from(base64, "base64");
|
|
1944
|
+
return body.length ? { body, mimeType } : undefined;
|
|
1945
|
+
}
|
|
1396
1946
|
function sendImage(res, payload) {
|
|
1397
1947
|
res.writeHead(200, {
|
|
1398
1948
|
"Content-Type": payload.mimeType,
|
|
@@ -1416,7 +1966,8 @@ function sanitizeWebAttachments(value) {
|
|
|
1416
1966
|
if (!isRecord(item))
|
|
1417
1967
|
return [];
|
|
1418
1968
|
if (item.kind === "image" && typeof item.label === "string" && /^\[img#\d+\]$/.test(item.label) && typeof item.mimeType === "string" && item.mimeType.startsWith("image/") && typeof item.data === "string") {
|
|
1419
|
-
|
|
1969
|
+
const imageId = typeof item.imageId === "string" && /^image_[0-9a-f-]{36}$/iu.test(item.imageId) ? item.imageId : createImageId();
|
|
1970
|
+
return [{ kind: "image", imageId, label: item.label, mimeType: item.mimeType, data: item.data }];
|
|
1420
1971
|
}
|
|
1421
1972
|
if (item.kind === "file" && typeof item.name === "string" && typeof item.mimeType === "string" && typeof item.size === "number" && Number.isFinite(item.size) && typeof item.absolutePath === "string" && path.isAbsolute(item.absolutePath)) {
|
|
1422
1973
|
return [{
|
|
@@ -1454,14 +2005,44 @@ function initialLines(runtime, lineId) {
|
|
|
1454
2005
|
lines.push({ id: ++lineId.current, ...line });
|
|
1455
2006
|
return lines;
|
|
1456
2007
|
}
|
|
2008
|
+
export function restoreWebHistoryLines(runtime) {
|
|
2009
|
+
return restoredHistoryLines(runtime).map((line) => ({ ...line }));
|
|
2010
|
+
}
|
|
1457
2011
|
function restoredHistoryLines(runtime) {
|
|
1458
2012
|
const lines = [];
|
|
2013
|
+
const historyToolLineIndexes = new Map();
|
|
1459
2014
|
const append = (line) => {
|
|
2015
|
+
if (line.toolUseId && line.titleStatus && historyToolLineIndexes.has(line.toolUseId)) {
|
|
2016
|
+
const index = historyToolLineIndexes.get(line.toolUseId);
|
|
2017
|
+
const started = lines[index];
|
|
2018
|
+
lines[index] = { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
|
|
2019
|
+
return index + 1;
|
|
2020
|
+
}
|
|
1460
2021
|
lines.push(line);
|
|
2022
|
+
if (line.toolUseId && !line.titleStatus)
|
|
2023
|
+
historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
|
|
1461
2024
|
return lines.length;
|
|
1462
2025
|
};
|
|
1463
|
-
|
|
1464
|
-
|
|
2026
|
+
const entries = runtime.engine.getDisplayEntries();
|
|
2027
|
+
let lastCompactionIndex = -1;
|
|
2028
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
2029
|
+
if (entries[index].type === "compact") {
|
|
2030
|
+
lastCompactionIndex = index;
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
for (const [entryIndex, entry] of entries.entries()) {
|
|
2035
|
+
if (entry.type === "message") {
|
|
2036
|
+
renderMessage(entry.message, append, undefined, {
|
|
2037
|
+
includeToolUseBlocks: true,
|
|
2038
|
+
includeThinkingBlocks: true,
|
|
2039
|
+
presentToolResult: runtimeToolResultPresenter(runtime),
|
|
2040
|
+
});
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
const report = entry.report ?? legacyCompactionReport(entry.reason);
|
|
2044
|
+
append(compactionLine(report, entry.createdAt, entryIndex === lastCompactionIndex));
|
|
2045
|
+
}
|
|
1465
2046
|
return lines;
|
|
1466
2047
|
}
|
|
1467
2048
|
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
@@ -1471,7 +2052,18 @@ async function resetStatus(runtime) {
|
|
|
1471
2052
|
return initialStatus(runtime, await runtime.engine.contextMetrics());
|
|
1472
2053
|
}
|
|
1473
2054
|
function buildWebPromptPayload(displayText, attachments) {
|
|
1474
|
-
const
|
|
2055
|
+
const candidateAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
|
|
2056
|
+
const labels = new Set();
|
|
2057
|
+
const imageIds = new Set();
|
|
2058
|
+
for (const attachment of candidateAttachments) {
|
|
2059
|
+
if (labels.has(attachment.label))
|
|
2060
|
+
throw new Error(`Duplicate image label in one submission: ${attachment.label}`);
|
|
2061
|
+
if (imageIds.has(attachment.imageId))
|
|
2062
|
+
throw new Error(`Duplicate image identity in one submission: ${attachment.imageId}`);
|
|
2063
|
+
labels.add(attachment.label);
|
|
2064
|
+
imageIds.add(attachment.imageId);
|
|
2065
|
+
}
|
|
2066
|
+
const activeAttachments = candidateAttachments;
|
|
1475
2067
|
const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
|
|
1476
2068
|
if (activeAttachments.length === 0) {
|
|
1477
2069
|
const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
|
|
@@ -1486,7 +2078,7 @@ function buildWebPromptPayload(displayText, attachments) {
|
|
|
1486
2078
|
break;
|
|
1487
2079
|
}
|
|
1488
2080
|
pushTextBlock(blocks, displayText.slice(cursor, next.index));
|
|
1489
|
-
blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
|
|
2081
|
+
blocks.push({ type: "image", imageId: next.attachment.imageId, mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
|
|
1490
2082
|
cursor = next.index + next.attachment.label.length;
|
|
1491
2083
|
}
|
|
1492
2084
|
pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
|
|
@@ -1531,9 +2123,76 @@ function pushTextBlock(blocks, text) {
|
|
|
1531
2123
|
else
|
|
1532
2124
|
blocks.push({ type: "text", text });
|
|
1533
2125
|
}
|
|
2126
|
+
function inferToolStreamChannel(data) {
|
|
2127
|
+
if (data && typeof data === "object" && !Array.isArray(data) && data.type === "terminal.output.delta") {
|
|
2128
|
+
return data.stream === "stderr" ? "stderr" : "stdout";
|
|
2129
|
+
}
|
|
2130
|
+
return "state";
|
|
2131
|
+
}
|
|
2132
|
+
function terminalOutputDelta(data) {
|
|
2133
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
2134
|
+
return undefined;
|
|
2135
|
+
const record = data;
|
|
2136
|
+
if (record.type !== "terminal.output.delta" || typeof record.text !== "string")
|
|
2137
|
+
return undefined;
|
|
2138
|
+
return { stream: record.stream === "stderr" ? "stderr" : "stdout", text: record.text };
|
|
2139
|
+
}
|
|
2140
|
+
function toolStreamTextDelta(data) {
|
|
2141
|
+
if (typeof data === "string")
|
|
2142
|
+
return data;
|
|
2143
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
2144
|
+
return undefined;
|
|
2145
|
+
const record = data;
|
|
2146
|
+
for (const value of [record.text, record.delta, record.patch, record.content]) {
|
|
2147
|
+
if (typeof value === "string")
|
|
2148
|
+
return value;
|
|
2149
|
+
}
|
|
2150
|
+
return undefined;
|
|
2151
|
+
}
|
|
2152
|
+
function childToolName(data) {
|
|
2153
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
2154
|
+
return undefined;
|
|
2155
|
+
const childEvent = data.child_event;
|
|
2156
|
+
if (!childEvent || typeof childEvent !== "object" || Array.isArray(childEvent))
|
|
2157
|
+
return undefined;
|
|
2158
|
+
const toolUse = childEvent.toolUse;
|
|
2159
|
+
if (!toolUse || typeof toolUse !== "object" || Array.isArray(toolUse))
|
|
2160
|
+
return undefined;
|
|
2161
|
+
return stringValue(toolUse.name);
|
|
2162
|
+
}
|
|
2163
|
+
function appendToolStreamChunk(chunks, sequence, stream, text, operation) {
|
|
2164
|
+
const next = operation === "replace" ? [] : [...(chunks ?? [])];
|
|
2165
|
+
next.push({ sequence: sequence ?? (next.at(-1)?.sequence ?? 0) + 1, stream, text });
|
|
2166
|
+
let total = 0;
|
|
2167
|
+
const retained = [];
|
|
2168
|
+
for (let index = next.length - 1; index >= 0; index -= 1) {
|
|
2169
|
+
const chunk = next[index];
|
|
2170
|
+
if (total >= 40_000)
|
|
2171
|
+
break;
|
|
2172
|
+
retained.push(chunk);
|
|
2173
|
+
total += chunk.text.length;
|
|
2174
|
+
}
|
|
2175
|
+
return retained.reverse();
|
|
2176
|
+
}
|
|
2177
|
+
function trimToolStreamText(value, maxChars) {
|
|
2178
|
+
return value.length <= maxChars ? value : value.slice(value.length - maxChars);
|
|
2179
|
+
}
|
|
2180
|
+
function appendToolStreamText(current, delta, maxChars) {
|
|
2181
|
+
return trimToolStreamText(`${current ?? ""}${delta}`, maxChars);
|
|
2182
|
+
}
|
|
2183
|
+
function toolStreamStepStatus(phase) {
|
|
2184
|
+
const normalized = phase?.toLowerCase() ?? "";
|
|
2185
|
+
if (normalized.includes("fail") || normalized.includes("error") || normalized.includes("cancel") || normalized.includes("abort"))
|
|
2186
|
+
return "failed";
|
|
2187
|
+
if (normalized.includes("complete") || normalized.includes("finish") || normalized.includes("done") || normalized.includes("success"))
|
|
2188
|
+
return "completed";
|
|
2189
|
+
return "running";
|
|
2190
|
+
}
|
|
1534
2191
|
function reduceStatus(status, event) {
|
|
1535
2192
|
if (event.type === "state")
|
|
1536
2193
|
return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
|
|
2194
|
+
if (event.type === "context.compacted")
|
|
2195
|
+
return { ...status, phase: "compacting", detail: formatCompactionReportSummary(event.compaction), activityTick: status.activityTick + 1 };
|
|
1537
2196
|
if (event.type === "context.metrics")
|
|
1538
2197
|
return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
|
|
1539
2198
|
if (event.type === "usage")
|
|
@@ -1548,8 +2207,10 @@ function reduceStatus(status, event) {
|
|
|
1548
2207
|
return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
|
|
1549
2208
|
if (event.type === "tool.started")
|
|
1550
2209
|
return { ...status, phase: "running_tools", currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: Date.now() }, activityTick: status.activityTick + 1 };
|
|
1551
|
-
if (event.type === "tool.
|
|
1552
|
-
return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ?
|
|
2210
|
+
if (event.type === "tool.progress")
|
|
2211
|
+
return { ...status, phase: "running_tools", detail: event.progress.message || status.detail, currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: status.currentTool?.id === event.toolUse.id ? status.currentTool.startedAt : Date.now() }, activityTick: status.activityTick + 1 };
|
|
2212
|
+
if (event.type === "tool.result.available" || event.type === "tool.finished" || event.type === "tool.batch.completed")
|
|
2213
|
+
return { ...status, currentTool: "toolUse" in event && status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
|
|
1553
2214
|
if (event.type === "terminal")
|
|
1554
2215
|
return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
|
|
1555
2216
|
if (event.type === "message" || event.type === "error")
|
|
@@ -1862,6 +2523,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1862
2523
|
if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
|
|
1863
2524
|
return true;
|
|
1864
2525
|
const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
|
|
2526
|
+
const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
|
|
1865
2527
|
let rendered = false;
|
|
1866
2528
|
for (const [blockIndex, block] of message.blocks.entries()) {
|
|
1867
2529
|
if (block.type === "text") {
|
|
@@ -1878,7 +2540,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1878
2540
|
const line = imageLineForBlock(message.role, block, message.id, blockIndex);
|
|
1879
2541
|
if (!line)
|
|
1880
2542
|
continue;
|
|
1881
|
-
append({ ...line, messageId: message.id, parentToolName });
|
|
2543
|
+
append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
|
|
1882
2544
|
rendered = true;
|
|
1883
2545
|
}
|
|
1884
2546
|
else if (block.type === "thinking") {
|
|
@@ -1888,22 +2550,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1888
2550
|
rendered = true;
|
|
1889
2551
|
}
|
|
1890
2552
|
else if (block.type === "tool_use" && options.includeToolUseBlocks) {
|
|
1891
|
-
append({ ...formatToolUse(block), messageId: message.id, live: false });
|
|
2553
|
+
append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
|
|
1892
2554
|
rendered = true;
|
|
1893
2555
|
}
|
|
1894
2556
|
else if (block.type === "tool_result") {
|
|
1895
|
-
append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
|
|
2557
|
+
append({ ...formatToolResultLine(block.name, block.output, block.ok, options.presentToolResult), messageId: message.id, toolUseId: block.toolUseId });
|
|
1896
2558
|
rendered = true;
|
|
1897
2559
|
}
|
|
1898
2560
|
}
|
|
1899
2561
|
return rendered;
|
|
1900
2562
|
}
|
|
1901
|
-
function renderToolResultMessage(message, append) {
|
|
2563
|
+
function renderToolResultMessage(message, append, presentToolResult) {
|
|
1902
2564
|
let rendered = false;
|
|
1903
2565
|
for (const block of message.blocks) {
|
|
1904
2566
|
if (block.type !== "tool_result")
|
|
1905
2567
|
continue;
|
|
1906
|
-
append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
|
|
2568
|
+
append({ ...formatToolResultLine(block.name, block.output, block.ok, presentToolResult), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
|
|
1907
2569
|
rendered = true;
|
|
1908
2570
|
}
|
|
1909
2571
|
return rendered;
|
|
@@ -1927,13 +2589,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
|
|
|
1927
2589
|
const kind = kindForRole(role);
|
|
1928
2590
|
if (kind === "meta")
|
|
1929
2591
|
return undefined;
|
|
2592
|
+
const resolution = resolveImageBlockDataResultSync(block);
|
|
1930
2593
|
return {
|
|
1931
2594
|
kind,
|
|
1932
2595
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1933
2596
|
image: {
|
|
1934
|
-
src: `/api/images/${encodeURIComponent(messageId)}/${blockIndex}
|
|
2597
|
+
src: resolution.available ? (block.imageId ? `/api/images/by-id/${encodeURIComponent(block.imageId)}` : `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`) : undefined,
|
|
2598
|
+
imageId: block.imageId,
|
|
1935
2599
|
label: block.label,
|
|
1936
2600
|
mimeType: block.mimeType,
|
|
2601
|
+
available: resolution.available,
|
|
2602
|
+
error: resolution.available ? undefined : resolution.error,
|
|
2603
|
+
blockIndex,
|
|
1937
2604
|
},
|
|
1938
2605
|
};
|
|
1939
2606
|
}
|
|
@@ -1985,39 +2652,192 @@ function titleForKind(kind) {
|
|
|
1985
2652
|
function systemLine(text, summaryMaxLines) {
|
|
1986
2653
|
return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
|
|
1987
2654
|
}
|
|
2655
|
+
function legacyCompactionReport(reason) {
|
|
2656
|
+
return {
|
|
2657
|
+
reason: reason && reason !== "none" ? reason : "manualcompact",
|
|
2658
|
+
summary: "此检查点由旧版 neoctl 持久化,未保存详细压缩报告。",
|
|
2659
|
+
continuationState: "该旧版检查点没有可用的详细续接上下文。",
|
|
2660
|
+
sourceMessages: 0,
|
|
2661
|
+
preservedUserMessages: 0,
|
|
2662
|
+
newWindowMessages: 0,
|
|
2663
|
+
charsFreed: 0,
|
|
2664
|
+
modelDriven: false,
|
|
2665
|
+
imageCount: 0,
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
function compactionLine(report, createdAt = new Date().toISOString(), current = true) {
|
|
2669
|
+
return {
|
|
2670
|
+
kind: "meta",
|
|
2671
|
+
title: "Context compaction",
|
|
2672
|
+
text: formatCompactionReportSummary(report),
|
|
2673
|
+
format: "plain",
|
|
2674
|
+
compaction: { ...report, createdAt, current },
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function compactionReportFromBoundary(message) {
|
|
2678
|
+
if (!message?.metadata?.compactBoundary)
|
|
2679
|
+
return undefined;
|
|
2680
|
+
const stored = message.metadata.compactionReport;
|
|
2681
|
+
if (stored?.continuationState)
|
|
2682
|
+
return { ...stored };
|
|
2683
|
+
const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
2684
|
+
const continuationState = /<compact_state>\s*([\s\S]*?)\s*<\/compact_state>/i.exec(text)?.[1]?.trim();
|
|
2685
|
+
if (!continuationState)
|
|
2686
|
+
return undefined;
|
|
2687
|
+
const reasonValue = typeof message.metadata.compactionReason === "string" ? message.metadata.compactionReason : "autocompact";
|
|
2688
|
+
const reason = isCompactionReason(reasonValue) ? reasonValue : "autocompact";
|
|
2689
|
+
const imageRegistry = message.metadata.imageRegistry;
|
|
2690
|
+
return {
|
|
2691
|
+
reason,
|
|
2692
|
+
summary: continuationState,
|
|
2693
|
+
continuationState,
|
|
2694
|
+
sourceMessages: 0,
|
|
2695
|
+
preservedUserMessages: 0,
|
|
2696
|
+
newWindowMessages: 1,
|
|
2697
|
+
charsFreed: 0,
|
|
2698
|
+
modelDriven: message.metadata.modelDriven === true,
|
|
2699
|
+
imageCount: Array.isArray(imageRegistry?.images) ? imageRegistry.images.length : 0,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function isCompactionReason(value) {
|
|
2703
|
+
return ["snip", "microcompact", "autocompact", "reactive_compact", "manualcompact", "purecompact"].includes(value);
|
|
2704
|
+
}
|
|
2705
|
+
function formatCompactionReportSummary(report) {
|
|
2706
|
+
return `${report.newWindowMessages} message(s) in new window, ${report.preservedUserMessages} user message(s) preserved`;
|
|
2707
|
+
}
|
|
1988
2708
|
function thinkingLine(text, live = false) {
|
|
1989
2709
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1990
2710
|
}
|
|
1991
2711
|
function formatToolUse(toolUse) {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
if (toolUse.name === "
|
|
1995
|
-
return { kind: "tool", toolName: toolUse.name,
|
|
2712
|
+
const toolPresentation = toolPresentationForSource(toolUse.name);
|
|
2713
|
+
const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
|
|
2714
|
+
if (toolUse.name === "plan_update" && isWebPlanPayload(toolUse.input))
|
|
2715
|
+
return { kind: "tool", toolName: toolUse.name, toolPresentation, title: toolPresentation.label, bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
|
|
2716
|
+
if (toolUse.name === "image_create")
|
|
2717
|
+
return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", presentationLevel: "primary", collapsible: true };
|
|
1996
2718
|
const summary = summarizeToolUse(toolUse.name, toolUse.input);
|
|
1997
|
-
return { kind: "tool", toolName: toolUse.name,
|
|
2719
|
+
return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
|
|
2720
|
+
}
|
|
2721
|
+
function formatToolResultLine(toolName, output, ok, presentToolResult) {
|
|
2722
|
+
const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
|
|
2723
|
+
const resources = presentation?.resources ?? toolResources(output);
|
|
2724
|
+
const formatted = resources?.length
|
|
2725
|
+
? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
|
|
2726
|
+
: formatToolResult(toolName, output, ok);
|
|
2727
|
+
return {
|
|
2728
|
+
kind: ok ? "tool" : "error",
|
|
2729
|
+
toolName,
|
|
2730
|
+
toolPresentation: toolPresentationForSource(toolName),
|
|
2731
|
+
title: presentation?.title ?? toolTitle(toolName, "finished"),
|
|
2732
|
+
bodyTitle: presentation?.bodyTitle ?? formatted.bodyTitle,
|
|
2733
|
+
titleStatus: ok ? "success" : "failure",
|
|
2734
|
+
text: presentation?.text ?? formatted.text,
|
|
2735
|
+
toolDisplay: resources?.length ? undefined : buildToolResultDisplay(toolName, output, ok),
|
|
2736
|
+
format: formatted.format,
|
|
2737
|
+
live: false,
|
|
2738
|
+
previewStyle: formatted.full ? undefined : "summary",
|
|
2739
|
+
summaryMaxLines: formatted.summaryMaxLines,
|
|
2740
|
+
presentationLevel: presentation?.presentationLevel ?? toolPresentationLevel(toolName, output),
|
|
2741
|
+
resources,
|
|
2742
|
+
collapsible: true,
|
|
2743
|
+
};
|
|
1998
2744
|
}
|
|
1999
|
-
function
|
|
2000
|
-
|
|
2001
|
-
|
|
2745
|
+
function toolPresentationLevel(toolName, payload) {
|
|
2746
|
+
if (toolName === "image_create")
|
|
2747
|
+
return "primary";
|
|
2748
|
+
if (!isRecord(payload))
|
|
2749
|
+
return "process";
|
|
2750
|
+
const direct = payload.presentationLevel;
|
|
2751
|
+
const ui = isRecord(payload.ui) ? payload.ui.presentationLevel : undefined;
|
|
2752
|
+
const underscoredUi = isRecord(payload._ui) ? payload._ui.presentationLevel : undefined;
|
|
2753
|
+
return [direct, ui, underscoredUi].some((value) => value === "primary") ? "primary" : "process";
|
|
2754
|
+
}
|
|
2755
|
+
function toolUiMetadata(payload) {
|
|
2756
|
+
if (!isRecord(payload))
|
|
2757
|
+
return undefined;
|
|
2758
|
+
const containers = [payload, payload.output, payload.result].filter(isRecord);
|
|
2759
|
+
for (const container of containers) {
|
|
2760
|
+
if (isRecord(container._ui))
|
|
2761
|
+
return container._ui;
|
|
2762
|
+
if (isRecord(container.ui))
|
|
2763
|
+
return container.ui;
|
|
2764
|
+
}
|
|
2765
|
+
return undefined;
|
|
2002
2766
|
}
|
|
2003
|
-
function
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2767
|
+
function toolResources(payload) {
|
|
2768
|
+
const ui = toolUiMetadata(payload);
|
|
2769
|
+
if (!ui || !Array.isArray(ui.resources))
|
|
2770
|
+
return undefined;
|
|
2771
|
+
return normalizeUiResources(ui.resources);
|
|
2772
|
+
}
|
|
2773
|
+
function normalizeUiResources(values) {
|
|
2774
|
+
if (!Array.isArray(values))
|
|
2775
|
+
return undefined;
|
|
2776
|
+
const resources = values.flatMap((value) => {
|
|
2777
|
+
if (!isRecord(value))
|
|
2778
|
+
return [];
|
|
2779
|
+
const kind = stringValue(value.kind);
|
|
2780
|
+
const url = stringValue(value.url);
|
|
2781
|
+
if (!kind || !url || !safeResourceUrl(url))
|
|
2782
|
+
return [];
|
|
2783
|
+
return [{
|
|
2784
|
+
kind,
|
|
2785
|
+
url,
|
|
2786
|
+
reference: stringValue(value.reference) ?? canonicalResourceReference(url),
|
|
2787
|
+
label: stringValue(value.label),
|
|
2788
|
+
downloadName: stringValue(value.downloadName),
|
|
2789
|
+
sizeBytes: numberValue(value.sizeBytes),
|
|
2790
|
+
expiresAt: numberValue(value.expiresAt),
|
|
2791
|
+
mimeType: stringValue(value.mimeType),
|
|
2792
|
+
height: clampResourceHeight(numberValue(value.height)),
|
|
2793
|
+
}];
|
|
2794
|
+
});
|
|
2795
|
+
return resources.length > 0 ? resources : undefined;
|
|
2796
|
+
}
|
|
2797
|
+
function canonicalResourceReference(url) {
|
|
2798
|
+
return url.startsWith("/") ? `sandbox:${url}` : url;
|
|
2799
|
+
}
|
|
2800
|
+
function clampResourceHeight(value) {
|
|
2801
|
+
return value === undefined ? undefined : Math.max(240, Math.min(1600, Math.round(value)));
|
|
2802
|
+
}
|
|
2803
|
+
function normalizeToolResultPresentation(value) {
|
|
2804
|
+
if (!isRecord(value))
|
|
2805
|
+
return undefined;
|
|
2806
|
+
const level = value.presentationLevel === "primary" || value.presentationLevel === "process" ? value.presentationLevel : undefined;
|
|
2807
|
+
return {
|
|
2808
|
+
title: stringValue(value.title),
|
|
2809
|
+
bodyTitle: stringValue(value.bodyTitle),
|
|
2810
|
+
text: stringValue(value.text),
|
|
2811
|
+
presentationLevel: level,
|
|
2812
|
+
resources: normalizeUiResources(value.resources),
|
|
2019
2813
|
};
|
|
2020
|
-
|
|
2814
|
+
}
|
|
2815
|
+
function runtimeToolResultPresenter(runtime) {
|
|
2816
|
+
return (toolName, output, ok) => {
|
|
2817
|
+
const support = runtime.pluginSupport;
|
|
2818
|
+
if (!support)
|
|
2819
|
+
return undefined;
|
|
2820
|
+
const sessionId = runtime.engine.snapshot().session?.sessionId;
|
|
2821
|
+
for (const plugin of activeWebRuntimePlugins(support.catalog, support.overrides)) {
|
|
2822
|
+
if (typeof plugin.presentToolResult !== "function")
|
|
2823
|
+
continue;
|
|
2824
|
+
try {
|
|
2825
|
+
const presentation = plugin.presentToolResult({ toolName, output, ok, sessionId });
|
|
2826
|
+
if (presentation !== undefined && presentation !== null)
|
|
2827
|
+
return presentation;
|
|
2828
|
+
}
|
|
2829
|
+
catch (error) {
|
|
2830
|
+
console.warn(`plugin ${plugin.id} failed to present ${toolName}:`, error);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return undefined;
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
function safeResourceUrl(value) {
|
|
2837
|
+
return /^(?:https?:\/\/|\/)/i.test(value) && !/^\/\//.test(value);
|
|
2838
|
+
}
|
|
2839
|
+
function toolTitle(toolName, _phase) {
|
|
2840
|
+
return builtinToolPresentation(toolName)?.label ?? toolName;
|
|
2021
2841
|
}
|
|
2022
2842
|
function summarizeToolUse(toolName, input) {
|
|
2023
2843
|
const purpose = toolUsePurpose(toolName, input);
|
|
@@ -2032,65 +2852,304 @@ function toolUsePurpose(toolName, input) {
|
|
|
2032
2852
|
const description = typeof input.description === "string" ? input.description.trim() : "";
|
|
2033
2853
|
if (description)
|
|
2034
2854
|
return description;
|
|
2855
|
+
const taskId = stringValue(input.task_id);
|
|
2856
|
+
if (toolName === "subagent_output")
|
|
2857
|
+
return taskId ? `读取子任务 ${taskId} 的输出` : "读取子任务输出";
|
|
2858
|
+
if (toolName === "subagent_list")
|
|
2859
|
+
return "查看子任务状态";
|
|
2860
|
+
if (toolName === "subagent_get")
|
|
2861
|
+
return taskId ? `查看子任务 ${taskId}` : "查看子任务";
|
|
2862
|
+
if (toolName === "subagent_stop")
|
|
2863
|
+
return taskId ? `停止子任务 ${taskId}` : "停止子任务";
|
|
2864
|
+
if (toolName === "subagent_resume")
|
|
2865
|
+
return taskId ? `恢复子任务 ${taskId}` : "恢复子任务";
|
|
2866
|
+
if (toolName === "subagent_message")
|
|
2867
|
+
return stringValue(input.target) ? `向 ${stringValue(input.target)} 发送消息` : "发送子任务消息";
|
|
2035
2868
|
if (toolName === "expose_downloads")
|
|
2036
2869
|
return "准备网页下载链接";
|
|
2037
|
-
if (toolName === "
|
|
2870
|
+
if (toolName === "file_read" && typeof input.path === "string")
|
|
2038
2871
|
return `读取 ${path.basename(input.path)}`;
|
|
2039
|
-
if (toolName === "
|
|
2872
|
+
if (toolName === "file_list" && typeof input.path === "string")
|
|
2040
2873
|
return `查看 ${input.path}`;
|
|
2041
|
-
if (toolName === "
|
|
2874
|
+
if (toolName === "file_search" && typeof input.query === "string")
|
|
2042
2875
|
return `搜索 ${input.query}`;
|
|
2043
|
-
if (toolName === "
|
|
2876
|
+
if (toolName === "file_write" && typeof input.path === "string")
|
|
2044
2877
|
return `写入 ${path.basename(input.path)}`;
|
|
2045
|
-
if (toolName === "
|
|
2878
|
+
if (toolName === "file_edit" && typeof input.path === "string")
|
|
2046
2879
|
return `修改 ${path.basename(input.path)}`;
|
|
2047
2880
|
return undefined;
|
|
2048
2881
|
}
|
|
2882
|
+
function mergePresentationLevel(result, started) {
|
|
2883
|
+
return result === "primary" || started === "primary" ? "primary" : result ?? started;
|
|
2884
|
+
}
|
|
2885
|
+
function mergeToolDisplays(result, started) {
|
|
2886
|
+
if (!result)
|
|
2887
|
+
return started;
|
|
2888
|
+
if (!started)
|
|
2889
|
+
return result;
|
|
2890
|
+
const facts = [...started.facts];
|
|
2891
|
+
for (const fact of result.facts) {
|
|
2892
|
+
const existing = facts.findIndex((item) => item.label === fact.label);
|
|
2893
|
+
if (existing >= 0)
|
|
2894
|
+
facts[existing] = fact;
|
|
2895
|
+
else
|
|
2896
|
+
facts.push(fact);
|
|
2897
|
+
}
|
|
2898
|
+
const subject = result.subject || started.subject;
|
|
2899
|
+
const purpose = result.purpose || started.purpose;
|
|
2900
|
+
return normalizeToolDisplay({
|
|
2901
|
+
subject,
|
|
2902
|
+
purpose,
|
|
2903
|
+
facts,
|
|
2904
|
+
previews: result.previews.length > 0 ? result.previews : started.previews,
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
function normalizeToolDisplay(display) {
|
|
2908
|
+
let subject = display.subject?.trim() || undefined;
|
|
2909
|
+
const purpose = display.purpose?.trim() || undefined;
|
|
2910
|
+
if (subject && purpose && normalizeDisplayText(subject) === normalizeDisplayText(purpose))
|
|
2911
|
+
subject = undefined;
|
|
2912
|
+
const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
|
|
2913
|
+
const facts = display.facts.filter((fact) => {
|
|
2914
|
+
const normalized = normalizeDisplayText(fact.value);
|
|
2915
|
+
if (!normalized || visibleText.has(normalized))
|
|
2916
|
+
return false;
|
|
2917
|
+
visibleText.add(normalized);
|
|
2918
|
+
return true;
|
|
2919
|
+
});
|
|
2920
|
+
const previews = display.previews.filter((preview) => {
|
|
2921
|
+
const normalized = normalizeDisplayText(preview.content);
|
|
2922
|
+
if (!normalized || visibleText.has(normalized))
|
|
2923
|
+
return false;
|
|
2924
|
+
visibleText.add(normalized);
|
|
2925
|
+
return true;
|
|
2926
|
+
});
|
|
2927
|
+
return { subject, purpose, facts, previews };
|
|
2928
|
+
}
|
|
2929
|
+
function normalizeDisplayText(value) {
|
|
2930
|
+
return value.replace(/\s+/gu, " ").trim().toLowerCase();
|
|
2931
|
+
}
|
|
2932
|
+
function buildToolUseDisplay(toolName, input) {
|
|
2933
|
+
const data = isRecord(input) ? input : {};
|
|
2934
|
+
const purpose = toolUsePurpose(toolName, input);
|
|
2935
|
+
const subject = toolName === "terminal_run" ? undefined : toolDisplaySubject(toolName, data);
|
|
2936
|
+
const facts = [];
|
|
2937
|
+
const previews = [];
|
|
2938
|
+
if (toolName === "terminal_run")
|
|
2939
|
+
pushToolPreview(previews, "命令", "code", data.cmd);
|
|
2940
|
+
else if (toolName === "terminal_control") {
|
|
2941
|
+
pushToolFact(facts, "会话", data.session_id, true);
|
|
2942
|
+
pushToolFact(facts, "输入", data.chars, true);
|
|
2943
|
+
}
|
|
2944
|
+
else if (toolName === "file_search")
|
|
2945
|
+
pushToolFact(facts, "范围", data.path, true);
|
|
2946
|
+
else if (toolName === "image_inspect")
|
|
2947
|
+
pushToolFact(facts, "图片", arrayLabel(data.imageRefs), true);
|
|
2948
|
+
else if (toolName === "image_create") {
|
|
2949
|
+
pushToolFact(facts, "模式", data.mode === "edit" ? "编辑" : "生成");
|
|
2950
|
+
pushToolFact(facts, "名称", data.semanticName);
|
|
2951
|
+
}
|
|
2952
|
+
else if (toolName === "expose_downloads")
|
|
2953
|
+
pushToolFact(facts, "文件", arrayLabel(data.paths), true);
|
|
2954
|
+
else if (toolName === "subagent_message")
|
|
2955
|
+
pushToolFact(facts, "接收方", data.target);
|
|
2956
|
+
else if (toolName.startsWith("subagent_"))
|
|
2957
|
+
pushToolFact(facts, "任务", data.task_id, true);
|
|
2958
|
+
else if (toolName.startsWith("secret_")) {
|
|
2959
|
+
pushToolFact(facts, "密钥", data.key, true);
|
|
2960
|
+
pushToolFact(facts, "用途", data.reason);
|
|
2961
|
+
}
|
|
2962
|
+
else if (toolName === "subagent_run") {
|
|
2963
|
+
pushToolFact(facts, "任务", data.description ?? data.prompt);
|
|
2964
|
+
pushToolFact(facts, "类型", data.subagent_type ?? data.mode);
|
|
2965
|
+
}
|
|
2966
|
+
return normalizeToolDisplay({ subject, purpose, facts, previews });
|
|
2967
|
+
}
|
|
2968
|
+
function buildToolResultDisplay(toolName, output, ok) {
|
|
2969
|
+
const data = isRecord(output) ? output : {};
|
|
2970
|
+
const facts = [];
|
|
2971
|
+
const previews = [];
|
|
2972
|
+
let subject = toolDisplaySubject(toolName, data);
|
|
2973
|
+
let purpose = stringValue(data.description);
|
|
2974
|
+
if ((toolName === "file_edit" || toolName === "file_write") && isEditToolOutput(data)) {
|
|
2975
|
+
subject = data.path;
|
|
2976
|
+
pushToolPreview(previews, undefined, "diff", compactEditDiff(data));
|
|
2977
|
+
}
|
|
2978
|
+
else if (isExecOutput(data)) {
|
|
2979
|
+
subject = undefined;
|
|
2980
|
+
purpose = stringValue(data.description);
|
|
2981
|
+
pushToolPreview(previews, "命令", "code", data.command);
|
|
2982
|
+
const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
|
|
2983
|
+
if (outputText)
|
|
2984
|
+
pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
|
|
2985
|
+
if (data.timed_out && !outputText)
|
|
2986
|
+
pushToolFact(facts, "状态", "命令执行超时", false, "warning");
|
|
2987
|
+
else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
|
|
2988
|
+
pushToolFact(facts, "状态", `退出码 ${data.exit_code ?? "未知"}`, false, "danger");
|
|
2989
|
+
}
|
|
2990
|
+
else if (toolName === "file_search") {
|
|
2991
|
+
subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
|
|
2992
|
+
pushToolPreview(previews, undefined, "list", grepPreview(data));
|
|
2993
|
+
}
|
|
2994
|
+
else if (toolName === "web_search") {
|
|
2995
|
+
subject = stringValue(data.query);
|
|
2996
|
+
pushToolPreview(previews, undefined, "list", searchPreview(data));
|
|
2997
|
+
}
|
|
2998
|
+
else if (toolName === "file_read") {
|
|
2999
|
+
subject = stringValue(data.path);
|
|
3000
|
+
if (numberValue(data.startLine) !== undefined && numberValue(data.endLine) !== undefined)
|
|
3001
|
+
pushToolFact(facts, "范围", `${data.startLine}–${data.endLine} 行`);
|
|
3002
|
+
pushToolPreview(previews, undefined, "code", data.content);
|
|
3003
|
+
}
|
|
3004
|
+
else if (toolName === "file_list") {
|
|
3005
|
+
subject = stringValue(data.path);
|
|
3006
|
+
pushToolPreview(previews, undefined, "list", listPreview(data));
|
|
3007
|
+
}
|
|
3008
|
+
else if (toolName === "image_create") {
|
|
3009
|
+
subject = stringValue(data.semanticName);
|
|
3010
|
+
pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
|
|
3011
|
+
pushToolFact(facts, "尺寸", data.size);
|
|
3012
|
+
}
|
|
3013
|
+
else if (toolName === "image_inspect") {
|
|
3014
|
+
subject = arrayLabel(data.imageRefs);
|
|
3015
|
+
}
|
|
3016
|
+
else if (toolResources(output)?.length) {
|
|
3017
|
+
subject = toolResources(output)?.map((item) => item.label || item.downloadName).filter(Boolean).join("、");
|
|
3018
|
+
}
|
|
3019
|
+
else if (toolName.startsWith("subagent_") || toolName === "subagent_run") {
|
|
3020
|
+
subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
|
|
3021
|
+
}
|
|
3022
|
+
else if (toolName === "subagent_message") {
|
|
3023
|
+
subject = stringValue(data.target);
|
|
3024
|
+
}
|
|
3025
|
+
else if (toolName.startsWith("secret_")) {
|
|
3026
|
+
subject = stringValue(data.key);
|
|
3027
|
+
pushToolFact(facts, "密钥", data.key, true);
|
|
3028
|
+
if (toolName === "secret_list")
|
|
3029
|
+
pushToolFact(facts, "数量", `${arrayLength(data.secrets)} 项`);
|
|
3030
|
+
}
|
|
3031
|
+
else if (toolName === "subagent_report") {
|
|
3032
|
+
}
|
|
3033
|
+
else {
|
|
3034
|
+
subject = subject || stringValue(data.path) || stringValue(data.id) || stringValue(data.name);
|
|
3035
|
+
}
|
|
3036
|
+
return normalizeToolDisplay({ subject, purpose, facts, previews });
|
|
3037
|
+
}
|
|
3038
|
+
function toolDisplaySubject(toolName, data) {
|
|
3039
|
+
if (["file_read", "file_write", "file_edit", "file_list"].includes(toolName))
|
|
3040
|
+
return stringValue(data.path);
|
|
3041
|
+
if (toolName === "terminal_run")
|
|
3042
|
+
return stringValue(data.cmd) || stringValue(data.command);
|
|
3043
|
+
if (toolName === "file_search" || toolName === "web_search")
|
|
3044
|
+
return stringValue(data.query);
|
|
3045
|
+
if (toolName === "image_create")
|
|
3046
|
+
return stringValue(data.semanticName);
|
|
3047
|
+
if (toolName === "subagent_message")
|
|
3048
|
+
return stringValue(data.target);
|
|
3049
|
+
if (toolName.startsWith("subagent_"))
|
|
3050
|
+
return stringValue(data.task_id);
|
|
3051
|
+
return undefined;
|
|
3052
|
+
}
|
|
3053
|
+
function pushToolPreview(previews, label, kind, value) {
|
|
3054
|
+
if (typeof value !== "string" || !value.trim())
|
|
3055
|
+
return;
|
|
3056
|
+
const content = value.replace(/\r\n/g, "\n").trim();
|
|
3057
|
+
previews.push({ label, kind, content: truncate(content, 1400) });
|
|
3058
|
+
}
|
|
3059
|
+
function compactEditDiff(output) {
|
|
3060
|
+
const lines = [];
|
|
3061
|
+
for (const hunk of output.patch.slice(0, 3)) {
|
|
3062
|
+
lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
3063
|
+
lines.push(...hunk.lines.slice(0, 14));
|
|
3064
|
+
}
|
|
3065
|
+
return lines.join("\n");
|
|
3066
|
+
}
|
|
3067
|
+
function grepPreview(data) {
|
|
3068
|
+
if (!Array.isArray(data.matches))
|
|
3069
|
+
return "";
|
|
3070
|
+
return data.matches.slice(0, 5).filter(isRecord).map((match) => {
|
|
3071
|
+
const file = stringValue(match.file) || "";
|
|
3072
|
+
const line = numberValue(match.line);
|
|
3073
|
+
const text = stringValue(match.text) || "";
|
|
3074
|
+
return `${file}${line !== undefined ? `:${line}` : ""} ${text}`.trim();
|
|
3075
|
+
}).filter(Boolean).join("\n");
|
|
3076
|
+
}
|
|
3077
|
+
function searchPreview(data) {
|
|
3078
|
+
if (!Array.isArray(data.results))
|
|
3079
|
+
return "";
|
|
3080
|
+
return data.results.slice(0, 4).filter(isRecord).map((result) => {
|
|
3081
|
+
const title = stringValue(result.title) || stringValue(result.url) || "";
|
|
3082
|
+
return title;
|
|
3083
|
+
}).filter(Boolean).join("\n");
|
|
3084
|
+
}
|
|
3085
|
+
function listPreview(data) {
|
|
3086
|
+
if (!Array.isArray(data.entries))
|
|
3087
|
+
return "";
|
|
3088
|
+
return data.entries.slice(0, 8).filter(isRecord).map((entry) => stringValue(entry.path) || stringValue(entry.name) || "").filter(Boolean).join("\n");
|
|
3089
|
+
}
|
|
3090
|
+
function pushToolFact(facts, label, value, code = false, tone = "neutral") {
|
|
3091
|
+
const normalized = stringValue(value);
|
|
3092
|
+
if (!normalized || facts.some((fact) => fact.label === label && fact.value === normalized))
|
|
3093
|
+
return;
|
|
3094
|
+
facts.push({ label, value: truncate(normalized, 220), code, tone });
|
|
3095
|
+
}
|
|
3096
|
+
function stringValue(value) {
|
|
3097
|
+
if (typeof value === "string" && value.trim())
|
|
3098
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
3099
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
3100
|
+
return String(value);
|
|
3101
|
+
return undefined;
|
|
3102
|
+
}
|
|
3103
|
+
function numberValue(value) {
|
|
3104
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3105
|
+
}
|
|
3106
|
+
function arrayLength(value) {
|
|
3107
|
+
return Array.isArray(value) ? value.length : 0;
|
|
3108
|
+
}
|
|
3109
|
+
function arrayLabel(value) {
|
|
3110
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0).join("、") : undefined;
|
|
3111
|
+
}
|
|
3112
|
+
function editOperationLabel(operation) {
|
|
3113
|
+
if (operation === "create")
|
|
3114
|
+
return "新建";
|
|
3115
|
+
if (operation === "edit")
|
|
3116
|
+
return "修改";
|
|
3117
|
+
if (operation === "write")
|
|
3118
|
+
return "写入";
|
|
3119
|
+
return operation;
|
|
3120
|
+
}
|
|
3121
|
+
function formatDuration(durationMs) {
|
|
3122
|
+
return durationMs >= 1000 ? `${(durationMs / 1000).toFixed(durationMs >= 10000 ? 0 : 1)} 秒` : `${durationMs} 毫秒`;
|
|
3123
|
+
}
|
|
3124
|
+
function taskStatusDisplay(value, ok) {
|
|
3125
|
+
const labels = { pending: "排队中", queued: "排队中", running: "运行中", completed: "已完成", failed: "失败", killed: "已停止", stopped: "已停止" };
|
|
3126
|
+
const status = stringValue(value);
|
|
3127
|
+
return status ? labels[status] ?? status : ok ? "已完成" : "失败";
|
|
3128
|
+
}
|
|
2049
3129
|
function formatToolResult(toolName, output, ok) {
|
|
2050
|
-
if ((toolName === "
|
|
3130
|
+
if ((toolName === "file_edit" || toolName === "file_write") && isRecord(output) && isEditToolOutput(output))
|
|
2051
3131
|
return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
|
|
2052
3132
|
if (isExecOutput(output))
|
|
2053
3133
|
return { text: formatExecToolResult(output, ok), format: "plain", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2054
|
-
if (toolName === "
|
|
3134
|
+
if (toolName === "file_list" && isRecord(output))
|
|
2055
3135
|
return { text: formatListToolResult(output, ok) };
|
|
2056
|
-
if (toolName === "
|
|
3136
|
+
if (toolName === "file_read" && isRecord(output))
|
|
2057
3137
|
return { text: formatReadToolResult(output, ok) };
|
|
2058
|
-
if (toolName === "
|
|
3138
|
+
if (toolName === "file_search" && isRecord(output))
|
|
2059
3139
|
return { text: formatGrepToolResult(output, ok) };
|
|
2060
|
-
if (toolName === "
|
|
3140
|
+
if (toolName === "web_search" && isRecord(output))
|
|
2061
3141
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2062
|
-
if (toolName === "
|
|
3142
|
+
if (toolName === "image_create" && isRecord(output))
|
|
2063
3143
|
return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
3144
|
+
const resources = toolResources(output);
|
|
3145
|
+
if (resources?.length)
|
|
3146
|
+
return { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" };
|
|
3147
|
+
if (toolName === "plan_update" && isWebPlanPayload(output))
|
|
2067
3148
|
return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
|
|
2068
3149
|
if (typeof output === "string")
|
|
2069
3150
|
return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2070
3151
|
return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2071
3152
|
}
|
|
2072
|
-
function formatExposeDownloadsToolResult(output, ok) {
|
|
2073
|
-
const downloads = extractDownloadEntries(output);
|
|
2074
|
-
if (!ok)
|
|
2075
|
-
return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
|
|
2076
|
-
if (!downloads.length)
|
|
2077
|
-
return "文件已准备好,请点击下载。";
|
|
2078
|
-
return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
|
|
2079
|
-
}
|
|
2080
|
-
function formatDownloadEntries(downloads) {
|
|
2081
|
-
return downloads.map((entry) => {
|
|
2082
|
-
const filename = entry.filename || entry.name || "下载文件";
|
|
2083
|
-
const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
|
|
2084
|
-
return url ? `- ${filename}: ${url}` : `- ${filename}`;
|
|
2085
|
-
}).join("\n");
|
|
2086
|
-
}
|
|
2087
|
-
function extractDownloadEntries(output) {
|
|
2088
|
-
if (!isRecord(output))
|
|
2089
|
-
return [];
|
|
2090
|
-
const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
|
|
2091
|
-
const downloads = candidates.find(Array.isArray);
|
|
2092
|
-
return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
|
|
2093
|
-
}
|
|
2094
3153
|
function isEditToolOutput(value) {
|
|
2095
3154
|
return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
|
|
2096
3155
|
}
|
|
@@ -2395,8 +3454,9 @@ function formatUsageTotals(totals) {
|
|
|
2395
3454
|
}
|
|
2396
3455
|
function formatManualCompaction(result) {
|
|
2397
3456
|
if (!result.changed)
|
|
2398
|
-
return "No context
|
|
2399
|
-
|
|
3457
|
+
return "No conversation context available to compact.";
|
|
3458
|
+
const preservedUsers = result.report?.preservedUserMessages ?? result.messages.filter((message) => message.metadata?.compactPreservedUser === true).length;
|
|
3459
|
+
return `context compacted: ${result.messages.length} message(s) in new window, ${preservedUsers} user message(s) preserved`;
|
|
2400
3460
|
}
|
|
2401
3461
|
function formatPureCompaction(result) {
|
|
2402
3462
|
if (!result.changed)
|
|
@@ -2406,6 +3466,8 @@ function formatPureCompaction(result) {
|
|
|
2406
3466
|
function formatNumber(value) {
|
|
2407
3467
|
return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2408
3468
|
}
|
|
3469
|
+
const TERMINAL_OUTPUT_FLUSH_MS = 100;
|
|
3470
|
+
const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
|
|
2409
3471
|
const THINKING_SUMMARY_MAX_LINES = 1000;
|
|
2410
3472
|
const EXPANDED_SUMMARY_MAX_LINES = 1000;
|
|
2411
3473
|
const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
|