neoctl 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -4
- package/dist/context/compaction.d.ts +20 -4
- package/dist/context/compaction.js +377 -186
- 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 +2 -0
- package/dist/context/prompts.js.map +1 -1
- package/dist/context/smoke-context.js +266 -17
- 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 +57 -8
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +13 -2
- package/dist/core/query.js +56 -21
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +68 -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 +1 -0
- package/dist/index.js +1 -0
- 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 +8 -3
- package/dist/repl/index.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/tools/builtins/exec-process-manager.d.ts +16 -2
- package/dist/tools/builtins/exec-process-manager.js +119 -25
- package/dist/tools/builtins/exec-process-manager.js.map +1 -1
- package/dist/tools/builtins/exec-tool.js +11 -2
- package/dist/tools/builtins/exec-tool.js.map +1 -1
- package/dist/tools/builtins/image-generation-tool.js +32 -42
- package/dist/tools/builtins/image-generation-tool.js.map +1 -1
- package/dist/tools/builtins/image-loader-tool.js +10 -4
- package/dist/tools/builtins/image-loader-tool.js.map +1 -1
- package/dist/tools/builtins/image-note-tool.js +8 -3
- package/dist/tools/builtins/image-note-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/smoke-exec-process.js +88 -4
- package/dist/tools/smoke-exec-process.js.map +1 -1
- package/dist/tools/smoke-tool-system.js +24 -6
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/types/events.d.ts +5 -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 +176 -11
- package/dist/web/index.js +986 -106
- package/dist/web/index.js.map +1 -1
- package/dist/web/runtime-context-protocol.d.ts +12 -1
- package/dist/web/runtime-context-protocol.js +16 -1
- package/dist/web/runtime-context-protocol.js.map +1 -1
- package/dist/web/smoke-runtime-context-protocol.js +3 -0
- 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 +121 -0
- package/dist/web/smoke-web-history.js.map +1 -0
- package/dist/web/smoke-web-terminal-tasks.d.ts +1 -0
- package/dist/web/smoke-web-terminal-tasks.js +90 -0
- package/dist/web/smoke-web-terminal-tasks.js.map +1 -0
- package/package.json +5 -1
package/dist/web/index.js
CHANGED
|
@@ -29,12 +29,15 @@ import { TaskStore } from "../tasks/task-store.js";
|
|
|
29
29
|
import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
|
|
30
30
|
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
31
31
|
import { DefaultContextManager } from "../context/context-manager.js";
|
|
32
|
+
import { createImageId } from "../types/messages.js";
|
|
32
33
|
import { WEB_HTML } from "./html.js";
|
|
33
34
|
import { openDirectory } from "../open-directory.js";
|
|
34
|
-
import { resolveImageBlockDataSync } from "../core/image-storage.js";
|
|
35
|
+
import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
|
|
35
36
|
import { isWebPlanPayload, serializeWebPlanPayload, webPlanBodyTitle } from "./plan-payload.js";
|
|
36
37
|
import { createWebRuntimeContextPayload } from "./runtime-context-protocol.js";
|
|
37
38
|
const require = createRequire(import.meta.url);
|
|
39
|
+
const corePackage = require("../../package.json");
|
|
40
|
+
const CORE_VERSION = String(corePackage.version || "unknown");
|
|
38
41
|
const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
|
|
39
42
|
const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
|
|
40
43
|
const markedAssetPath = path.join(markedPackageDir, "lib", "marked.esm.js");
|
|
@@ -127,26 +130,42 @@ export async function createWebRuntime(options = {}) {
|
|
|
127
130
|
const pluginOverrides = normalizePluginOverrides(options.sessionPluginOverrides, pluginCatalog);
|
|
128
131
|
const activePlugins = activeWebRuntimePlugins(pluginCatalog, pluginOverrides);
|
|
129
132
|
const tools = new ToolRegistry();
|
|
133
|
+
const toolCatalog = [];
|
|
134
|
+
const catalogedToolNames = new Set();
|
|
135
|
+
const catalogTool = (tool, item) => {
|
|
136
|
+
if (catalogedToolNames.has(tool.name))
|
|
137
|
+
return;
|
|
138
|
+
catalogedToolNames.add(tool.name);
|
|
139
|
+
toolCatalog.push({ name: tool.name, ...item });
|
|
140
|
+
};
|
|
141
|
+
const registerTool = (tool, item) => {
|
|
142
|
+
tools.register(tool);
|
|
143
|
+
catalogTool(tool, item);
|
|
144
|
+
};
|
|
130
145
|
const execProcessManager = new ExecProcessManager();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
registerTool(editTool, { source: "builtin" });
|
|
147
|
+
registerTool(writeTool, { source: "builtin" });
|
|
148
|
+
registerTool(createExecTool({ processManager: execProcessManager }), { source: "builtin" });
|
|
149
|
+
registerTool(createWriteStdinTool(execProcessManager), { source: "builtin" });
|
|
150
|
+
registerTool(listDirectoryTool, { source: "builtin" });
|
|
151
|
+
registerTool(readFileTool, { source: "builtin" });
|
|
152
|
+
registerTool(grepTool, { source: "builtin" });
|
|
153
|
+
registerTool(searchTool, { source: "builtin" });
|
|
154
|
+
registerTool(createLoadImageTool(), { source: "builtin" });
|
|
140
155
|
if (modelConfig?.provider === "openai")
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
registerTool(createOpenAIImageGenerationTool(), { source: "builtin" });
|
|
157
|
+
registerTool(planTool, { source: "builtin" });
|
|
143
158
|
for (const tool of options.externalTools ?? [])
|
|
144
|
-
|
|
159
|
+
registerTool(tool, { source: "external" });
|
|
160
|
+
for (const plugin of pluginCatalog) {
|
|
161
|
+
for (const tool of plugin.tools)
|
|
162
|
+
catalogTool(tool, { source: "plugin", pluginId: plugin.id, pluginName: plugin.name });
|
|
163
|
+
}
|
|
145
164
|
for (const plugin of activePlugins)
|
|
146
165
|
for (const tool of plugin.tools)
|
|
147
166
|
tools.register(tool);
|
|
148
167
|
const agentRuntime = { modelGateway, tools, taskStore };
|
|
149
|
-
|
|
168
|
+
registerTool(createAgentTool(agentRuntime), { source: "builtin" });
|
|
150
169
|
const resumeHandler = async (taskId, directive) => {
|
|
151
170
|
const dummyContext = {
|
|
152
171
|
agentId: "main",
|
|
@@ -157,7 +176,10 @@ export async function createWebRuntime(options = {}) {
|
|
|
157
176
|
return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
|
|
158
177
|
};
|
|
159
178
|
for (const tool of createTaskTools(taskStore, resumeHandler))
|
|
160
|
-
|
|
179
|
+
registerTool(tool, { source: "builtin" });
|
|
180
|
+
const globalToolOverrides = normalizeToolOverrides(options.globalToolOverrides, toolCatalog);
|
|
181
|
+
const sessionToolOverrides = normalizeToolOverrides(options.sessionToolOverrides, toolCatalog);
|
|
182
|
+
applyToolOverrides(tools, toolCatalog, globalToolOverrides, sessionToolOverrides);
|
|
161
183
|
const appPromptStore = new InMemoryAppPromptStore();
|
|
162
184
|
const engine = new QueryEngine({
|
|
163
185
|
agentId: options.agentId ?? "main",
|
|
@@ -176,6 +198,8 @@ export async function createWebRuntime(options = {}) {
|
|
|
176
198
|
taskNotificationSource: createTaskNotificationSource(taskStore),
|
|
177
199
|
commands: replCommandDefinitions.map((command) => command.usage),
|
|
178
200
|
plugins: [...(options.plugins ?? []), ...activePlugins.map((plugin) => plugin.id)],
|
|
201
|
+
cwdTransitionPaths: options.cwdTransitionPaths,
|
|
202
|
+
onCwdTransitionConsumed: options.onCwdTransitionConsumed,
|
|
179
203
|
session: {
|
|
180
204
|
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
181
205
|
sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
|
|
@@ -208,6 +232,14 @@ export async function createWebRuntime(options = {}) {
|
|
|
208
232
|
persist: options.persistSessionPluginOverrides,
|
|
209
233
|
resolve: options.resolveSessionPluginOverrides,
|
|
210
234
|
} : undefined,
|
|
235
|
+
toolSupport: {
|
|
236
|
+
catalog: toolCatalog,
|
|
237
|
+
globalOverrides: globalToolOverrides,
|
|
238
|
+
sessionOverrides: sessionToolOverrides,
|
|
239
|
+
persistGlobal: options.persistGlobalToolOverrides,
|
|
240
|
+
persistSession: options.persistSessionToolOverrides,
|
|
241
|
+
resolveSession: options.resolveSessionToolOverrides,
|
|
242
|
+
},
|
|
211
243
|
};
|
|
212
244
|
}
|
|
213
245
|
function normalizeWebRuntimePlugins(value) {
|
|
@@ -233,6 +265,14 @@ function normalizePluginOverrides(value, catalog) {
|
|
|
233
265
|
function activeWebRuntimePlugins(catalog, overrides) {
|
|
234
266
|
return catalog.filter((plugin) => plugin.globallyEnabled && overrides[plugin.id] !== false);
|
|
235
267
|
}
|
|
268
|
+
function normalizeToolOverrides(value, catalog) {
|
|
269
|
+
const available = new Set(catalog.map((tool) => tool.name));
|
|
270
|
+
return Object.fromEntries(Object.entries(value ?? {}).filter(([name, enabled]) => available.has(name) && typeof enabled === "boolean"));
|
|
271
|
+
}
|
|
272
|
+
function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides) {
|
|
273
|
+
for (const tool of catalog)
|
|
274
|
+
registry.setEnabled(tool.name, sessionOverrides[tool.name] ?? globalOverrides[tool.name] ?? true);
|
|
275
|
+
}
|
|
236
276
|
function createTaskNotificationSource(taskStore) {
|
|
237
277
|
return {
|
|
238
278
|
collectUnnotifiedCompletions() {
|
|
@@ -266,6 +306,8 @@ export class WebRuntimeRouter {
|
|
|
266
306
|
repls = new Map();
|
|
267
307
|
createRuntime;
|
|
268
308
|
createRepl;
|
|
309
|
+
clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
310
|
+
coreVersion = process.env.NEO_CORE_VERSION?.trim() || CORE_VERSION;
|
|
269
311
|
constructor(options = {}) {
|
|
270
312
|
this.createRuntime = options.createRuntime ?? createWebRuntime;
|
|
271
313
|
this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
|
|
@@ -286,6 +328,24 @@ export class WebRuntimeRouter {
|
|
|
286
328
|
activeScopes() {
|
|
287
329
|
return [...this.repls.keys()];
|
|
288
330
|
}
|
|
331
|
+
clientInfo() {
|
|
332
|
+
return {
|
|
333
|
+
protocolVersion: 1,
|
|
334
|
+
revision: this.clientRevision,
|
|
335
|
+
coreVersion: this.coreVersion,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async requestClientReload(options = {}) {
|
|
339
|
+
const payload = {
|
|
340
|
+
...this.clientInfo(),
|
|
341
|
+
requestId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
342
|
+
clearCache: options.clearCache !== false,
|
|
343
|
+
reason: options.reason?.trim() || "deployment",
|
|
344
|
+
};
|
|
345
|
+
const repls = await Promise.all([...this.repls.values()]);
|
|
346
|
+
const clients = repls.reduce((total, repl) => total + repl.requestClientReload(payload), 0);
|
|
347
|
+
return { ...payload, clients };
|
|
348
|
+
}
|
|
289
349
|
}
|
|
290
350
|
export async function createWebRuntimeRouter(options = {}) {
|
|
291
351
|
const router = new WebRuntimeRouter(options);
|
|
@@ -308,6 +368,8 @@ export class WebRepl {
|
|
|
308
368
|
pendingDeltaOperations = [];
|
|
309
369
|
pendingDeltaStatus = false;
|
|
310
370
|
pendingDeltaTimer;
|
|
371
|
+
pendingTerminalOutput = new Map();
|
|
372
|
+
terminalOutputTimer;
|
|
311
373
|
lineId = 0;
|
|
312
374
|
assistantLineId;
|
|
313
375
|
thinkingLineId;
|
|
@@ -322,29 +384,29 @@ export class WebRepl {
|
|
|
322
384
|
queuedAttachments;
|
|
323
385
|
foregroundRun;
|
|
324
386
|
foregroundRunToken = 0;
|
|
325
|
-
|
|
387
|
+
pendingUserImageEchoIds;
|
|
326
388
|
pendingUserImageEchoMessageId;
|
|
327
389
|
runtimeContextRevision = 0;
|
|
328
390
|
runtimeContextPublishQueue = Promise.resolve();
|
|
329
391
|
backgroundSessionRuns = new Map();
|
|
330
392
|
suppressReattachedStreaming = new Set();
|
|
331
|
-
backgroundTaskCount;
|
|
332
393
|
constructor(runtime) {
|
|
333
394
|
this.runtime = runtime;
|
|
334
|
-
|
|
395
|
+
const lineId = { current: 0 };
|
|
396
|
+
this.lines = initialLines(runtime, lineId);
|
|
397
|
+
this.lineId = lineId.current;
|
|
335
398
|
this.status = initialStatus(runtime);
|
|
336
|
-
this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
|
|
337
399
|
runtime.taskStore.subscribe(() => {
|
|
338
|
-
this.backgroundTaskCount = runtime.taskStore.activeCount() + runtime.execProcessManager.activeCount();
|
|
339
400
|
this.broadcastSync();
|
|
340
401
|
});
|
|
341
402
|
runtime.execProcessManager.subscribe(() => {
|
|
342
|
-
this.
|
|
403
|
+
this.dropInactiveTerminalOutput();
|
|
343
404
|
this.broadcastSync();
|
|
344
405
|
});
|
|
406
|
+
runtime.execProcessManager.subscribeOutput((delta) => this.queueTerminalOutput(delta));
|
|
345
407
|
runtime.engine.onSessionTitleChange(() => this.broadcastSync());
|
|
346
408
|
}
|
|
347
|
-
subscribe(res) {
|
|
409
|
+
subscribe(res, clientInfo) {
|
|
348
410
|
const subscriber = { response: res, blocked: false, needsSync: false, needsRuntimeContext: false };
|
|
349
411
|
this.subscribers.add(subscriber);
|
|
350
412
|
res.writeHead(200, {
|
|
@@ -353,19 +415,31 @@ export class WebRepl {
|
|
|
353
415
|
Connection: "keep-alive",
|
|
354
416
|
"X-Accel-Buffering": "no",
|
|
355
417
|
});
|
|
418
|
+
if (clientInfo)
|
|
419
|
+
this.send(subscriber, "client.version", clientInfo);
|
|
356
420
|
this.send(subscriber, "sync", this.snapshot(true));
|
|
357
421
|
this.publishRuntimeContext([subscriber]);
|
|
358
422
|
reqKeepAlive(res);
|
|
359
423
|
res.on("close", () => this.subscribers.delete(subscriber));
|
|
360
424
|
}
|
|
425
|
+
get backgroundTaskCount() {
|
|
426
|
+
return this.backgroundTasks().length;
|
|
427
|
+
}
|
|
428
|
+
requestClientReload(payload) {
|
|
429
|
+
const subscribers = [...this.subscribers].filter((subscriber) => !subscriber.response.destroyed);
|
|
430
|
+
for (const subscriber of subscribers)
|
|
431
|
+
this.send(subscriber, "client.reload", payload);
|
|
432
|
+
return subscribers.length;
|
|
433
|
+
}
|
|
361
434
|
snapshot(includeCatalog = false) {
|
|
435
|
+
const backgroundTasks = this.backgroundTasks();
|
|
362
436
|
return {
|
|
363
437
|
lines: this.lines,
|
|
364
438
|
status: this.status,
|
|
365
439
|
busy: this.busy,
|
|
366
440
|
queuedInput: this.queuedInput,
|
|
367
|
-
backgroundTaskCount:
|
|
368
|
-
backgroundTasks
|
|
441
|
+
backgroundTaskCount: backgroundTasks.length,
|
|
442
|
+
backgroundTasks,
|
|
369
443
|
backgroundSessionRunCount: this.backgroundSessionRuns.size,
|
|
370
444
|
runningSessionIds: [...this.backgroundSessionRuns.keys()],
|
|
371
445
|
session: this.runtime.engine.snapshot().session,
|
|
@@ -401,6 +475,91 @@ export class WebRepl {
|
|
|
401
475
|
}),
|
|
402
476
|
};
|
|
403
477
|
}
|
|
478
|
+
globalTools() {
|
|
479
|
+
const support = this.runtime.toolSupport;
|
|
480
|
+
if (!support)
|
|
481
|
+
return { items: [] };
|
|
482
|
+
return {
|
|
483
|
+
items: support.catalog.map((tool) => ({
|
|
484
|
+
...tool,
|
|
485
|
+
configuredEnabled: support.globalOverrides[tool.name] ?? true,
|
|
486
|
+
})),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
sessionTools() {
|
|
490
|
+
const support = this.runtime.toolSupport;
|
|
491
|
+
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
492
|
+
if (!support)
|
|
493
|
+
return { sessionId, busy: this.busy, items: [] };
|
|
494
|
+
const activePluginIds = new Set(this.runtime.pluginSupport
|
|
495
|
+
? activeWebRuntimePlugins(this.runtime.pluginSupport.catalog, this.runtime.pluginSupport.overrides).map((plugin) => plugin.id)
|
|
496
|
+
: []);
|
|
497
|
+
return {
|
|
498
|
+
sessionId,
|
|
499
|
+
busy: this.busy,
|
|
500
|
+
items: support.catalog.map((tool) => {
|
|
501
|
+
const override = support.sessionOverrides[tool.name];
|
|
502
|
+
const globallyEnabled = support.globalOverrides[tool.name] ?? true;
|
|
503
|
+
const available = tool.source !== "plugin" || Boolean(tool.pluginId && activePluginIds.has(tool.pluginId));
|
|
504
|
+
return {
|
|
505
|
+
...tool,
|
|
506
|
+
available,
|
|
507
|
+
globallyEnabled,
|
|
508
|
+
mode: override === undefined ? "inherit" : override ? "enabled" : "disabled",
|
|
509
|
+
effectiveEnabled: available && (override ?? globallyEnabled),
|
|
510
|
+
};
|
|
511
|
+
}),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async setGlobalTools(value) {
|
|
515
|
+
if (this.busy)
|
|
516
|
+
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change global tools while a response is running");
|
|
517
|
+
if (!isRecord(value))
|
|
518
|
+
return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
|
|
519
|
+
const support = this.runtime.toolSupport;
|
|
520
|
+
if (!support)
|
|
521
|
+
return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
|
|
522
|
+
const normalized = this.parseToolOverrides(value, support);
|
|
523
|
+
if (!normalized.ok)
|
|
524
|
+
return normalized;
|
|
525
|
+
try {
|
|
526
|
+
support.globalOverrides = normalized.value;
|
|
527
|
+
this.syncPluginToolRegistration();
|
|
528
|
+
applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
|
|
529
|
+
if (support.persistGlobal)
|
|
530
|
+
await support.persistGlobal(support.globalOverrides);
|
|
531
|
+
await this.refreshToolConfiguration();
|
|
532
|
+
return { ok: true, state: this.globalTools() };
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async setSessionTools(value) {
|
|
539
|
+
if (this.busy)
|
|
540
|
+
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
|
|
541
|
+
if (!isRecord(value))
|
|
542
|
+
return actionFailure("INVALID_REQUEST", "tool overrides must be an object");
|
|
543
|
+
const support = this.runtime.toolSupport;
|
|
544
|
+
if (!support)
|
|
545
|
+
return actionFailure("TOOL_NOT_CONFIGURED", "web tools are not configured");
|
|
546
|
+
const normalized = this.parseToolModes(value, support);
|
|
547
|
+
if (!normalized.ok)
|
|
548
|
+
return normalized;
|
|
549
|
+
try {
|
|
550
|
+
await this.applySessionToolOverrides(normalized.value, true);
|
|
551
|
+
return { ok: true, state: this.sessionTools() };
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async loadSessionTools(sessionId) {
|
|
558
|
+
const support = this.runtime.toolSupport;
|
|
559
|
+
if (!support?.resolveSession || !sessionId)
|
|
560
|
+
return;
|
|
561
|
+
await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
|
|
562
|
+
}
|
|
404
563
|
async setSessionPlugins(value) {
|
|
405
564
|
if (this.busy)
|
|
406
565
|
return actionFailure("PLUGIN_UPDATE_BLOCKED", "cannot change session plugins while a response is running");
|
|
@@ -435,6 +594,64 @@ export class WebRepl {
|
|
|
435
594
|
return;
|
|
436
595
|
await this.applySessionPluginOverrides(await support.resolve(sessionId), false);
|
|
437
596
|
}
|
|
597
|
+
parseToolOverrides(value, support) {
|
|
598
|
+
const available = new Set(support.catalog.map((tool) => tool.name));
|
|
599
|
+
const overrides = {};
|
|
600
|
+
for (const [name, enabled] of Object.entries(value)) {
|
|
601
|
+
if (!available.has(name))
|
|
602
|
+
return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
|
|
603
|
+
if (typeof enabled !== "boolean")
|
|
604
|
+
return actionFailure("TOOL_INVALID", `invalid tool state: ${name}`);
|
|
605
|
+
overrides[name] = enabled;
|
|
606
|
+
}
|
|
607
|
+
return { ok: true, value: overrides };
|
|
608
|
+
}
|
|
609
|
+
parseToolModes(value, support) {
|
|
610
|
+
const available = new Set(support.catalog.map((tool) => tool.name));
|
|
611
|
+
const overrides = {};
|
|
612
|
+
for (const [name, mode] of Object.entries(value)) {
|
|
613
|
+
if (!available.has(name))
|
|
614
|
+
return actionFailure("TOOL_INVALID", `unknown tool: ${name}`);
|
|
615
|
+
if (mode === "enabled" || mode === true)
|
|
616
|
+
overrides[name] = true;
|
|
617
|
+
else if (mode === "disabled" || mode === false)
|
|
618
|
+
overrides[name] = false;
|
|
619
|
+
else if (mode !== "inherit" && mode !== null && mode !== undefined)
|
|
620
|
+
return actionFailure("TOOL_INVALID", `invalid tool mode: ${name}`);
|
|
621
|
+
}
|
|
622
|
+
return { ok: true, value: overrides };
|
|
623
|
+
}
|
|
624
|
+
async applySessionToolOverrides(overrides, persist) {
|
|
625
|
+
const support = this.runtime.toolSupport;
|
|
626
|
+
if (!support)
|
|
627
|
+
return;
|
|
628
|
+
support.sessionOverrides = normalizeToolOverrides(overrides, support.catalog);
|
|
629
|
+
this.syncPluginToolRegistration();
|
|
630
|
+
applyToolOverrides(this.runtime.tools, support.catalog, support.globalOverrides, support.sessionOverrides);
|
|
631
|
+
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
632
|
+
if (persist && sessionId && support.persistSession)
|
|
633
|
+
await support.persistSession(sessionId, support.sessionOverrides);
|
|
634
|
+
await this.refreshToolConfiguration();
|
|
635
|
+
}
|
|
636
|
+
syncPluginToolRegistration() {
|
|
637
|
+
const pluginSupport = this.runtime.pluginSupport;
|
|
638
|
+
if (!pluginSupport)
|
|
639
|
+
return;
|
|
640
|
+
for (const plugin of pluginSupport.catalog)
|
|
641
|
+
for (const tool of plugin.tools)
|
|
642
|
+
this.runtime.tools.unregister(tool.name);
|
|
643
|
+
for (const plugin of activeWebRuntimePlugins(pluginSupport.catalog, pluginSupport.overrides)) {
|
|
644
|
+
for (const tool of plugin.tools)
|
|
645
|
+
this.runtime.tools.register(tool);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async refreshToolConfiguration() {
|
|
649
|
+
const metrics = await this.runtime.engine.contextMetrics();
|
|
650
|
+
this.runtime.initialMetrics = metrics;
|
|
651
|
+
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
652
|
+
this.broadcastSync();
|
|
653
|
+
this.publishRuntimeContext();
|
|
654
|
+
}
|
|
438
655
|
async applySessionPluginOverrides(overrides, persist) {
|
|
439
656
|
const support = this.runtime.pluginSupport;
|
|
440
657
|
if (!support)
|
|
@@ -448,6 +665,9 @@ export class WebRepl {
|
|
|
448
665
|
for (const tool of plugin.tools)
|
|
449
666
|
this.runtime.tools.register(tool);
|
|
450
667
|
support.overrides = normalized;
|
|
668
|
+
const toolSupport = this.runtime.toolSupport;
|
|
669
|
+
if (toolSupport)
|
|
670
|
+
applyToolOverrides(this.runtime.tools, toolSupport.catalog, toolSupport.globalOverrides, toolSupport.sessionOverrides);
|
|
451
671
|
this.runtime.engine.setRuntimePlugins([...support.basePluginIds, ...activePlugins.map((plugin) => plugin.id)], [...support.basePromptSections, ...activePlugins.flatMap((plugin) => plugin.promptSections ?? [])]);
|
|
452
672
|
const sessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
453
673
|
if (persist && sessionId && support.persist)
|
|
@@ -474,6 +694,7 @@ export class WebRepl {
|
|
|
474
694
|
this.broadcastSync();
|
|
475
695
|
return { ok: true };
|
|
476
696
|
}
|
|
697
|
+
this.publishRuntimeContext();
|
|
477
698
|
this.startRun(text, attachments);
|
|
478
699
|
return { ok: true };
|
|
479
700
|
}
|
|
@@ -532,7 +753,7 @@ export class WebRepl {
|
|
|
532
753
|
const snapshot = this.runtime.engine.snapshot().session;
|
|
533
754
|
if (!snapshot)
|
|
534
755
|
throw new Error("session transcripts are disabled");
|
|
535
|
-
await this.loadSessionPlugins(snapshot.sessionId);
|
|
756
|
+
await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
|
|
536
757
|
await this.refreshSessionView();
|
|
537
758
|
return { ok: true };
|
|
538
759
|
}
|
|
@@ -549,7 +770,7 @@ export class WebRepl {
|
|
|
549
770
|
const snapshot = this.runtime.engine.snapshot().session;
|
|
550
771
|
if (!snapshot)
|
|
551
772
|
throw new Error("session transcripts are disabled");
|
|
552
|
-
await this.loadSessionPlugins(snapshot.sessionId);
|
|
773
|
+
await Promise.all([this.loadSessionPlugins(snapshot.sessionId), this.loadSessionTools(snapshot.sessionId)]);
|
|
553
774
|
await this.refreshSessionView();
|
|
554
775
|
return { ok: true };
|
|
555
776
|
}
|
|
@@ -628,22 +849,28 @@ export class WebRepl {
|
|
|
628
849
|
imagePayload(messageId, blockIndex) {
|
|
629
850
|
if (!messageId || !Number.isInteger(blockIndex) || blockIndex < 0)
|
|
630
851
|
return undefined;
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (!
|
|
852
|
+
let block;
|
|
853
|
+
for (const entry of this.runtime.engine.getDisplayEntries()) {
|
|
854
|
+
if (entry.type !== "message" || entry.message.id !== messageId)
|
|
855
|
+
continue;
|
|
856
|
+
const candidate = entry.message.blocks[blockIndex];
|
|
857
|
+
if (candidate?.type === "image")
|
|
858
|
+
block = candidate;
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
return block ? imagePayloadForBlock(block) : undefined;
|
|
862
|
+
}
|
|
863
|
+
imagePayloadById(imageId) {
|
|
864
|
+
if (!imageId)
|
|
644
865
|
return undefined;
|
|
645
|
-
const
|
|
646
|
-
|
|
866
|
+
for (const entry of this.runtime.engine.getDisplayEntries()) {
|
|
867
|
+
if (entry.type !== "message")
|
|
868
|
+
continue;
|
|
869
|
+
const block = entry.message.blocks.find((candidate) => candidate.type === "image" && candidate.imageId === imageId);
|
|
870
|
+
if (block)
|
|
871
|
+
return imagePayloadForBlock(block);
|
|
872
|
+
}
|
|
873
|
+
return undefined;
|
|
647
874
|
}
|
|
648
875
|
async setFastMode(enabled) {
|
|
649
876
|
try {
|
|
@@ -655,6 +882,25 @@ export class WebRepl {
|
|
|
655
882
|
return actionFailure("FAST_MODE_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
656
883
|
}
|
|
657
884
|
}
|
|
885
|
+
async setContextWindowK(value) {
|
|
886
|
+
if (this.busy)
|
|
887
|
+
return actionFailure("CONTEXT_WINDOW_UPDATE_BLOCKED", "cannot change context window while a response is running");
|
|
888
|
+
if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0) {
|
|
889
|
+
return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
|
|
890
|
+
}
|
|
891
|
+
const contextWindowK = Number(value);
|
|
892
|
+
const contextWindowTokens = contextWindowK * 1000;
|
|
893
|
+
try {
|
|
894
|
+
await this.runtime.engine.setContextWindowTokensOverride(contextWindowTokens);
|
|
895
|
+
const metrics = await this.runtime.engine.contextMetrics();
|
|
896
|
+
this.runtime.initialMetrics = metrics;
|
|
897
|
+
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
898
|
+
return { ok: true, contextWindowK, contextWindowTokens };
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
return actionFailure("CONTEXT_WINDOW_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
658
904
|
loginForm(providerValue) {
|
|
659
905
|
const provider = parseLoginProvider(providerValue);
|
|
660
906
|
return createLoginFormPayload(this.runtime.envPath, provider);
|
|
@@ -711,6 +957,12 @@ export class WebRepl {
|
|
|
711
957
|
this.broadcastSync();
|
|
712
958
|
return id;
|
|
713
959
|
}
|
|
960
|
+
appendCompaction(report, createdAt) {
|
|
961
|
+
this.lines = this.lines.map((line) => line.compaction
|
|
962
|
+
? { ...line, compaction: { ...line.compaction, current: false } }
|
|
963
|
+
: line);
|
|
964
|
+
return this.append(compactionLine(report, createdAt, true));
|
|
965
|
+
}
|
|
714
966
|
appendLineText(id, text) {
|
|
715
967
|
this.lines = this.lines.map((line) => line.id === id ? { ...line, text: line.text + text } : line);
|
|
716
968
|
this.broadcastDelta({ type: "line.text.append", id, text });
|
|
@@ -749,7 +1001,7 @@ export class WebRepl {
|
|
|
749
1001
|
this.interruptArmed = false;
|
|
750
1002
|
this.queuedInput = undefined;
|
|
751
1003
|
this.queuedAttachments = undefined;
|
|
752
|
-
this.
|
|
1004
|
+
this.pendingUserImageEchoIds = undefined;
|
|
753
1005
|
this.pendingUserImageEchoMessageId = undefined;
|
|
754
1006
|
this.finalizeForegroundView();
|
|
755
1007
|
this.finalizeLiveToolLines();
|
|
@@ -759,16 +1011,58 @@ export class WebRepl {
|
|
|
759
1011
|
return runWasActive;
|
|
760
1012
|
}
|
|
761
1013
|
backgroundTasks() {
|
|
762
|
-
|
|
1014
|
+
const agentTasks = this.runtime.taskStore.list()
|
|
763
1015
|
.filter((task) => !this.runtime.taskStore.isTerminal(task))
|
|
764
1016
|
.map((task) => ({
|
|
1017
|
+
kind: "agent",
|
|
765
1018
|
taskId: task.taskId,
|
|
766
1019
|
agentId: task.agentId,
|
|
1020
|
+
agentType: task.agentType,
|
|
767
1021
|
type: task.type,
|
|
768
1022
|
status: task.status,
|
|
769
1023
|
description: task.description,
|
|
1024
|
+
prompt: task.prompt,
|
|
1025
|
+
progress: task.progress,
|
|
1026
|
+
outputFile: task.outputFile,
|
|
770
1027
|
createdAt: task.createdAt,
|
|
1028
|
+
updatedAt: task.updatedAt,
|
|
1029
|
+
}));
|
|
1030
|
+
const terminalTasks = this.runtime.execProcessManager.list()
|
|
1031
|
+
.filter((terminal) => terminal.status === "running" && terminal.backgrounded)
|
|
1032
|
+
.map((terminal) => ({
|
|
1033
|
+
kind: "terminal",
|
|
1034
|
+
taskId: `terminal:${terminal.session_id}`,
|
|
1035
|
+
type: "终端",
|
|
1036
|
+
status: terminal.status,
|
|
1037
|
+
description: terminal.description || terminal.command,
|
|
1038
|
+
createdAt: Date.now() - terminal.duration_ms,
|
|
1039
|
+
sessionId: terminal.session_id,
|
|
1040
|
+
processId: terminal.process_id,
|
|
1041
|
+
command: terminal.command,
|
|
1042
|
+
cwd: terminal.cwd,
|
|
1043
|
+
shell: terminal.shell,
|
|
1044
|
+
tty: terminal.tty,
|
|
1045
|
+
durationMs: terminal.duration_ms,
|
|
1046
|
+
output: terminal.output,
|
|
1047
|
+
outputEnd: terminal.outputEnd,
|
|
771
1048
|
}));
|
|
1049
|
+
const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
|
|
1050
|
+
kind: "session",
|
|
1051
|
+
taskId: `session:${run.sessionId}`,
|
|
1052
|
+
type: "会话",
|
|
1053
|
+
status: "running",
|
|
1054
|
+
description: run.title || "未命名会话",
|
|
1055
|
+
createdAt: run.startedAt,
|
|
1056
|
+
sessionId: run.sessionId,
|
|
1057
|
+
}));
|
|
1058
|
+
const createdAtTime = (value) => {
|
|
1059
|
+
if (typeof value === "number")
|
|
1060
|
+
return value;
|
|
1061
|
+
const parsed = Date.parse(value);
|
|
1062
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1063
|
+
};
|
|
1064
|
+
return [...agentTasks, ...terminalTasks, ...sessionTasks]
|
|
1065
|
+
.sort((left, right) => createdAtTime(right.createdAt) - createdAtTime(left.createdAt));
|
|
772
1066
|
}
|
|
773
1067
|
async detachRunningForeground(reason) {
|
|
774
1068
|
if (!this.busy)
|
|
@@ -797,7 +1091,7 @@ export class WebRepl {
|
|
|
797
1091
|
this.interruptArmed = false;
|
|
798
1092
|
this.queuedInput = undefined;
|
|
799
1093
|
this.queuedAttachments = undefined;
|
|
800
|
-
this.
|
|
1094
|
+
this.pendingUserImageEchoIds = undefined;
|
|
801
1095
|
this.pendingUserImageEchoMessageId = undefined;
|
|
802
1096
|
this.busy = false;
|
|
803
1097
|
this.status = { ...this.status, phase: "ready", detail: undefined };
|
|
@@ -843,7 +1137,7 @@ export class WebRepl {
|
|
|
843
1137
|
this.reduce(event);
|
|
844
1138
|
if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
|
|
845
1139
|
const messageId = this.pendingUserImageEchoMessageId;
|
|
846
|
-
this.
|
|
1140
|
+
this.pendingUserImageEchoIds = undefined;
|
|
847
1141
|
this.pendingUserImageEchoMessageId = undefined;
|
|
848
1142
|
renderMessageImages(event.message, (line) => this.append(line), messageId);
|
|
849
1143
|
this.broadcastSync();
|
|
@@ -853,6 +1147,10 @@ export class WebRepl {
|
|
|
853
1147
|
this.queueDeltaStatus();
|
|
854
1148
|
return;
|
|
855
1149
|
}
|
|
1150
|
+
if (event.type === "context.compacted") {
|
|
1151
|
+
this.appendCompaction(event.compaction);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
856
1154
|
if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
|
|
857
1155
|
this.broadcastSync();
|
|
858
1156
|
return;
|
|
@@ -903,10 +1201,11 @@ export class WebRepl {
|
|
|
903
1201
|
const liveLineId = this.liveToolLineIds.get(toolUseId);
|
|
904
1202
|
if (liveLineId === undefined)
|
|
905
1203
|
return this.append(line);
|
|
906
|
-
this.
|
|
1204
|
+
const liveLine = this.lines.find((item) => item.id === liveLineId);
|
|
1205
|
+
this.replaceLine(liveLineId, { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, liveLine?.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, liveLine?.toolDisplay) });
|
|
907
1206
|
this.liveToolLineIds.delete(toolUseId);
|
|
908
1207
|
return liveLineId;
|
|
909
|
-
});
|
|
1208
|
+
}, runtimeToolResultPresenter(this.runtime));
|
|
910
1209
|
renderMessageImages(event.message, (line) => this.append(line));
|
|
911
1210
|
return;
|
|
912
1211
|
}
|
|
@@ -927,6 +1226,7 @@ export class WebRepl {
|
|
|
927
1226
|
if (event.type === "tool.started") {
|
|
928
1227
|
this.finalizeLiveLine(this.assistantLineId);
|
|
929
1228
|
this.finalizeThinkingLine();
|
|
1229
|
+
this.assistantLineId = undefined;
|
|
930
1230
|
const id = this.append({ ...formatToolUse(event.toolUse), live: true });
|
|
931
1231
|
this.liveToolLineIds.set(event.toolUse.id, id);
|
|
932
1232
|
return;
|
|
@@ -951,13 +1251,13 @@ export class WebRepl {
|
|
|
951
1251
|
this.append({ kind: "error", text: event.error.message });
|
|
952
1252
|
}
|
|
953
1253
|
matchesPendingUserImageEcho(message) {
|
|
954
|
-
const pending = this.
|
|
1254
|
+
const pending = this.pendingUserImageEchoIds;
|
|
955
1255
|
if (!pending?.length || message.role !== "user")
|
|
956
1256
|
return false;
|
|
957
|
-
const
|
|
958
|
-
.filter((block) => block.type === "image" && typeof block.
|
|
959
|
-
.map((block) => block.
|
|
960
|
-
return
|
|
1257
|
+
const imageIds = message.blocks
|
|
1258
|
+
.filter((block) => block.type === "image" && typeof block.imageId === "string")
|
|
1259
|
+
.map((block) => block.imageId);
|
|
1260
|
+
return imageIds.length === pending.length && imageIds.every((imageId, index) => imageId === pending[index]);
|
|
961
1261
|
}
|
|
962
1262
|
async handleCommandOrPrompt(text, attachments = []) {
|
|
963
1263
|
const command = parseReplCommand(text);
|
|
@@ -1059,11 +1359,11 @@ export class WebRepl {
|
|
|
1059
1359
|
const promptPayload = buildWebPromptPayload(command.text, attachments);
|
|
1060
1360
|
const optimisticMessageId = `web-user-${this.lineId + 1}`;
|
|
1061
1361
|
this.append({ kind: "user", text: promptPayload.displayText, messageId: optimisticMessageId });
|
|
1062
|
-
const
|
|
1063
|
-
.filter((block) => block.type === "image" && typeof block.
|
|
1064
|
-
.map((block) => block.
|
|
1065
|
-
this.
|
|
1066
|
-
this.pendingUserImageEchoMessageId =
|
|
1362
|
+
const optimisticImageIds = (promptPayload.blocks ?? [])
|
|
1363
|
+
.filter((block) => block.type === "image" && typeof block.imageId === "string")
|
|
1364
|
+
.map((block) => block.imageId);
|
|
1365
|
+
this.pendingUserImageEchoIds = optimisticImageIds.length ? optimisticImageIds : undefined;
|
|
1366
|
+
this.pendingUserImageEchoMessageId = optimisticImageIds.length ? optimisticMessageId : undefined;
|
|
1067
1367
|
const runToken = ++this.foregroundRunToken;
|
|
1068
1368
|
const abortController = new AbortController();
|
|
1069
1369
|
this.activeAbortController = abortController;
|
|
@@ -1072,7 +1372,12 @@ export class WebRepl {
|
|
|
1072
1372
|
this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
|
|
1073
1373
|
const engine = this.runtime.engine;
|
|
1074
1374
|
try {
|
|
1075
|
-
for await (const event of engine.sendUserText(promptPayload.text, {
|
|
1375
|
+
for await (const event of engine.sendUserText(promptPayload.text, {
|
|
1376
|
+
abortSignal: abortController.signal,
|
|
1377
|
+
blocks: promptPayload.blocks,
|
|
1378
|
+
displayText: promptPayload.displayText,
|
|
1379
|
+
stopAfterTurn: () => this.queuedInput !== undefined,
|
|
1380
|
+
})) {
|
|
1076
1381
|
if (this.foregroundRunToken !== runToken)
|
|
1077
1382
|
continue;
|
|
1078
1383
|
if (this.runtime.engine !== engine)
|
|
@@ -1103,7 +1408,7 @@ export class WebRepl {
|
|
|
1103
1408
|
this.activeAbortController = undefined;
|
|
1104
1409
|
this.interruptArmed = false;
|
|
1105
1410
|
this.finalizeForegroundView();
|
|
1106
|
-
this.
|
|
1411
|
+
this.pendingUserImageEchoIds = undefined;
|
|
1107
1412
|
this.pendingUserImageEchoMessageId = undefined;
|
|
1108
1413
|
const queuedText = this.queuedInput;
|
|
1109
1414
|
const queuedAttach = this.queuedAttachments;
|
|
@@ -1136,7 +1441,10 @@ export class WebRepl {
|
|
|
1136
1441
|
const metrics = await this.runtime.engine.contextMetrics();
|
|
1137
1442
|
if (this.foregroundRunToken !== runToken)
|
|
1138
1443
|
return;
|
|
1139
|
-
|
|
1444
|
+
if (result.report)
|
|
1445
|
+
this.appendCompaction(result.report);
|
|
1446
|
+
else
|
|
1447
|
+
this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
|
|
1140
1448
|
this.handleEvent({ type: "context.metrics", metrics });
|
|
1141
1449
|
}
|
|
1142
1450
|
catch (error) {
|
|
@@ -1155,7 +1463,9 @@ export class WebRepl {
|
|
|
1155
1463
|
}
|
|
1156
1464
|
send(subscriber, event, data) {
|
|
1157
1465
|
if (subscriber.blocked) {
|
|
1158
|
-
if (event === "
|
|
1466
|
+
if (event === "client.reload")
|
|
1467
|
+
subscriber.pendingClientReload = data;
|
|
1468
|
+
else if (event === "runtime.context")
|
|
1159
1469
|
subscriber.needsRuntimeContext = true;
|
|
1160
1470
|
else
|
|
1161
1471
|
subscriber.needsSync = true;
|
|
@@ -1170,6 +1480,11 @@ export class WebRepl {
|
|
|
1170
1480
|
subscriber.blocked = false;
|
|
1171
1481
|
if (subscriber.response.destroyed)
|
|
1172
1482
|
return;
|
|
1483
|
+
if (subscriber.pendingClientReload) {
|
|
1484
|
+
const payload = subscriber.pendingClientReload;
|
|
1485
|
+
subscriber.pendingClientReload = undefined;
|
|
1486
|
+
this.send(subscriber, "client.reload", payload);
|
|
1487
|
+
}
|
|
1173
1488
|
if (subscriber.needsSync) {
|
|
1174
1489
|
subscriber.needsSync = false;
|
|
1175
1490
|
this.send(subscriber, "sync", this.snapshot(false));
|
|
@@ -1245,6 +1560,39 @@ export class WebRepl {
|
|
|
1245
1560
|
this.pendingDeltaOperations = [];
|
|
1246
1561
|
this.pendingDeltaStatus = false;
|
|
1247
1562
|
}
|
|
1563
|
+
dropInactiveTerminalOutput() {
|
|
1564
|
+
for (const sessionId of this.pendingTerminalOutput.keys()) {
|
|
1565
|
+
if (!this.runtime.execProcessManager.isBackgroundRunning(sessionId))
|
|
1566
|
+
this.pendingTerminalOutput.delete(sessionId);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
queueTerminalOutput(delta) {
|
|
1570
|
+
if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
|
|
1571
|
+
return;
|
|
1572
|
+
const outputStart = Number(delta.outputStart ?? 0);
|
|
1573
|
+
const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
|
|
1574
|
+
const pending = this.pendingTerminalOutput.get(delta.sessionId);
|
|
1575
|
+
if (pending && pending.outputEnd === outputStart) {
|
|
1576
|
+
pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
|
|
1577
|
+
pending.outputEnd = outputEnd;
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
|
|
1581
|
+
}
|
|
1582
|
+
if (this.terminalOutputTimer)
|
|
1583
|
+
return;
|
|
1584
|
+
this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
|
|
1585
|
+
this.terminalOutputTimer.unref?.();
|
|
1586
|
+
}
|
|
1587
|
+
flushTerminalOutput() {
|
|
1588
|
+
this.terminalOutputTimer = undefined;
|
|
1589
|
+
if (!this.pendingTerminalOutput.size)
|
|
1590
|
+
return;
|
|
1591
|
+
const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
|
|
1592
|
+
this.pendingTerminalOutput.clear();
|
|
1593
|
+
for (const subscriber of this.subscribers)
|
|
1594
|
+
this.send(subscriber, "terminal.output", { updates });
|
|
1595
|
+
}
|
|
1248
1596
|
broadcastSync() {
|
|
1249
1597
|
this.discardPendingDeltaOperations();
|
|
1250
1598
|
const payload = this.snapshot(false);
|
|
@@ -1252,6 +1600,10 @@ export class WebRepl {
|
|
|
1252
1600
|
this.send(subscriber, "sync", payload);
|
|
1253
1601
|
}
|
|
1254
1602
|
}
|
|
1603
|
+
function appendBoundedText(current, incoming, limit) {
|
|
1604
|
+
const combined = current + incoming;
|
|
1605
|
+
return combined.length <= limit ? combined : combined.slice(-limit);
|
|
1606
|
+
}
|
|
1255
1607
|
export function compactLineOperations(operations) {
|
|
1256
1608
|
const compacted = [];
|
|
1257
1609
|
for (const operation of operations) {
|
|
@@ -1284,20 +1636,67 @@ async function route(req, res, router) {
|
|
|
1284
1636
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
1285
1637
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
1286
1638
|
return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
|
|
1639
|
+
if (req.method === "GET" && url.pathname === "/api/client-info")
|
|
1640
|
+
return sendJson(res, router.clientInfo());
|
|
1641
|
+
if (req.method === "POST" && url.pathname === "/api/client-reload") {
|
|
1642
|
+
const body = await readJsonBody(req);
|
|
1643
|
+
return sendJson(res, await router.requestClientReload(body));
|
|
1644
|
+
}
|
|
1287
1645
|
const scope = webRuntimeScopeFromUrl(url);
|
|
1288
1646
|
const repl = await router.get(scope);
|
|
1289
1647
|
if (req.method === "GET" && url.pathname === "/events")
|
|
1290
|
-
return repl.subscribe(res);
|
|
1648
|
+
return repl.subscribe(res, router.clientInfo());
|
|
1291
1649
|
if (req.method === "GET" && url.pathname === "/api/state")
|
|
1292
1650
|
return sendJson(res, repl.snapshot(true));
|
|
1293
1651
|
if (req.method === "GET" && url.pathname === "/api/runtime-context")
|
|
1294
1652
|
return sendJson(res, await repl.runtimeContext());
|
|
1653
|
+
const workspaceRepl = repl;
|
|
1654
|
+
if (req.method === "GET" && url.pathname === "/api/cwd") {
|
|
1655
|
+
return sendJson(res, workspaceRepl.browseWorkspace
|
|
1656
|
+
? await workspaceRepl.browseWorkspace(url.searchParams.get("path") ?? undefined)
|
|
1657
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1658
|
+
}
|
|
1659
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/change") {
|
|
1660
|
+
const body = await readJsonBody(req);
|
|
1661
|
+
return sendJson(res, workspaceRepl.changeWorkspace
|
|
1662
|
+
? await workspaceRepl.changeWorkspace(body.path)
|
|
1663
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1664
|
+
}
|
|
1665
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/create") {
|
|
1666
|
+
const body = await readJsonBody(req);
|
|
1667
|
+
return sendJson(res, workspaceRepl.createWorkspaceDirectory
|
|
1668
|
+
? await workspaceRepl.createWorkspaceDirectory(body.path)
|
|
1669
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1670
|
+
}
|
|
1671
|
+
if (req.method === "POST" && url.pathname === "/api/cwd/delete") {
|
|
1672
|
+
const body = await readJsonBody(req);
|
|
1673
|
+
return sendJson(res, workspaceRepl.deleteWorkspaceDirectory
|
|
1674
|
+
? await workspaceRepl.deleteWorkspaceDirectory(body.path)
|
|
1675
|
+
: actionFailure("CWD_NOT_CONFIGURED", "workspace navigation is not configured"));
|
|
1676
|
+
}
|
|
1677
|
+
if (req.method === "GET" && url.pathname === "/api/tools")
|
|
1678
|
+
return sendJson(res, repl.globalTools());
|
|
1679
|
+
if (req.method === "POST" && url.pathname === "/api/tools/global") {
|
|
1680
|
+
const body = await readJsonBody(req);
|
|
1681
|
+
return sendJson(res, await repl.setGlobalTools(body.overrides));
|
|
1682
|
+
}
|
|
1683
|
+
if (req.method === "GET" && url.pathname === "/api/session-tools")
|
|
1684
|
+
return sendJson(res, repl.sessionTools());
|
|
1685
|
+
if (req.method === "POST" && url.pathname === "/api/session-tools") {
|
|
1686
|
+
const body = await readJsonBody(req);
|
|
1687
|
+
return sendJson(res, await repl.setSessionTools(body.overrides));
|
|
1688
|
+
}
|
|
1295
1689
|
if (req.method === "GET" && url.pathname === "/api/session-plugins")
|
|
1296
1690
|
return sendJson(res, repl.sessionPlugins());
|
|
1297
1691
|
if (req.method === "POST" && url.pathname === "/api/session-plugins") {
|
|
1298
1692
|
const body = await readJsonBody(req);
|
|
1299
1693
|
return sendJson(res, await repl.setSessionPlugins(body.overrides));
|
|
1300
1694
|
}
|
|
1695
|
+
const imageIdMatch = /^\/api\/images\/by-id\/([^/]+)$/u.exec(url.pathname);
|
|
1696
|
+
if (req.method === "GET" && imageIdMatch) {
|
|
1697
|
+
const payload = repl.imagePayloadById(decodeURIComponent(imageIdMatch[1]));
|
|
1698
|
+
return payload ? sendImage(res, payload) : sendJson(res, { error: "image not found" }, 404);
|
|
1699
|
+
}
|
|
1301
1700
|
const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
|
|
1302
1701
|
if (req.method === "GET" && imageMatch) {
|
|
1303
1702
|
const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
|
|
@@ -1313,6 +1712,10 @@ async function route(req, res, router) {
|
|
|
1313
1712
|
const body = await readJsonBody(req);
|
|
1314
1713
|
return sendJson(res, await repl.setFastMode(body.enabled === true));
|
|
1315
1714
|
}
|
|
1715
|
+
if (req.method === "POST" && url.pathname === "/api/context-window") {
|
|
1716
|
+
const body = await readJsonBody(req);
|
|
1717
|
+
return sendJson(res, await repl.setContextWindowK(body.value));
|
|
1718
|
+
}
|
|
1316
1719
|
if (req.method === "POST" && url.pathname === "/api/submit") {
|
|
1317
1720
|
const body = await readJsonBody(req);
|
|
1318
1721
|
return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
@@ -1374,6 +1777,20 @@ function sendJson(res, value, status = 200) {
|
|
|
1374
1777
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
1375
1778
|
res.end(JSON.stringify(value));
|
|
1376
1779
|
}
|
|
1780
|
+
function imagePayloadForBlock(block) {
|
|
1781
|
+
const resolved = resolveImageBlockDataSync(block)?.trim();
|
|
1782
|
+
if (!resolved)
|
|
1783
|
+
return undefined;
|
|
1784
|
+
const dataUrl = /^data:([^;,]+);base64,([\s\S]*)$/iu.exec(resolved);
|
|
1785
|
+
const mimeType = (dataUrl?.[1] ?? block.mimeType).toLowerCase();
|
|
1786
|
+
if (!mimeType.startsWith("image/"))
|
|
1787
|
+
return undefined;
|
|
1788
|
+
const base64 = (dataUrl?.[2] ?? resolved).replace(/\s+/gu, "");
|
|
1789
|
+
if (!base64 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
|
|
1790
|
+
return undefined;
|
|
1791
|
+
const body = Buffer.from(base64, "base64");
|
|
1792
|
+
return body.length ? { body, mimeType } : undefined;
|
|
1793
|
+
}
|
|
1377
1794
|
function sendImage(res, payload) {
|
|
1378
1795
|
res.writeHead(200, {
|
|
1379
1796
|
"Content-Type": payload.mimeType,
|
|
@@ -1397,7 +1814,8 @@ function sanitizeWebAttachments(value) {
|
|
|
1397
1814
|
if (!isRecord(item))
|
|
1398
1815
|
return [];
|
|
1399
1816
|
if (item.kind === "image" && typeof item.label === "string" && /^\[img#\d+\]$/.test(item.label) && typeof item.mimeType === "string" && item.mimeType.startsWith("image/") && typeof item.data === "string") {
|
|
1400
|
-
|
|
1817
|
+
const imageId = typeof item.imageId === "string" && /^image_[0-9a-f-]{36}$/iu.test(item.imageId) ? item.imageId : createImageId();
|
|
1818
|
+
return [{ kind: "image", imageId, label: item.label, mimeType: item.mimeType, data: item.data }];
|
|
1401
1819
|
}
|
|
1402
1820
|
if (item.kind === "file" && typeof item.name === "string" && typeof item.mimeType === "string" && typeof item.size === "number" && Number.isFinite(item.size) && typeof item.absolutePath === "string" && path.isAbsolute(item.absolutePath)) {
|
|
1403
1821
|
return [{
|
|
@@ -1435,14 +1853,44 @@ function initialLines(runtime, lineId) {
|
|
|
1435
1853
|
lines.push({ id: ++lineId.current, ...line });
|
|
1436
1854
|
return lines;
|
|
1437
1855
|
}
|
|
1856
|
+
export function restoreWebHistoryLines(runtime) {
|
|
1857
|
+
return restoredHistoryLines(runtime).map((line) => ({ ...line }));
|
|
1858
|
+
}
|
|
1438
1859
|
function restoredHistoryLines(runtime) {
|
|
1439
1860
|
const lines = [];
|
|
1861
|
+
const historyToolLineIndexes = new Map();
|
|
1440
1862
|
const append = (line) => {
|
|
1863
|
+
if (line.toolUseId && line.titleStatus && historyToolLineIndexes.has(line.toolUseId)) {
|
|
1864
|
+
const index = historyToolLineIndexes.get(line.toolUseId);
|
|
1865
|
+
const started = lines[index];
|
|
1866
|
+
lines[index] = { ...line, presentationLevel: mergePresentationLevel(line.presentationLevel, started.presentationLevel), toolDisplay: mergeToolDisplays(line.toolDisplay, started.toolDisplay) };
|
|
1867
|
+
return index + 1;
|
|
1868
|
+
}
|
|
1441
1869
|
lines.push(line);
|
|
1870
|
+
if (line.toolUseId && !line.titleStatus)
|
|
1871
|
+
historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
|
|
1442
1872
|
return lines.length;
|
|
1443
1873
|
};
|
|
1444
|
-
|
|
1445
|
-
|
|
1874
|
+
const entries = runtime.engine.getDisplayEntries();
|
|
1875
|
+
let lastCompactionIndex = -1;
|
|
1876
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
1877
|
+
if (entries[index].type === "compact") {
|
|
1878
|
+
lastCompactionIndex = index;
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
for (const [entryIndex, entry] of entries.entries()) {
|
|
1883
|
+
if (entry.type === "message") {
|
|
1884
|
+
renderMessage(entry.message, append, undefined, {
|
|
1885
|
+
includeToolUseBlocks: true,
|
|
1886
|
+
includeThinkingBlocks: true,
|
|
1887
|
+
presentToolResult: runtimeToolResultPresenter(runtime),
|
|
1888
|
+
});
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
const report = entry.report ?? legacyCompactionReport(entry.reason);
|
|
1892
|
+
append(compactionLine(report, entry.createdAt, entryIndex === lastCompactionIndex));
|
|
1893
|
+
}
|
|
1446
1894
|
return lines;
|
|
1447
1895
|
}
|
|
1448
1896
|
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
@@ -1452,7 +1900,18 @@ async function resetStatus(runtime) {
|
|
|
1452
1900
|
return initialStatus(runtime, await runtime.engine.contextMetrics());
|
|
1453
1901
|
}
|
|
1454
1902
|
function buildWebPromptPayload(displayText, attachments) {
|
|
1455
|
-
const
|
|
1903
|
+
const candidateAttachments = attachments.filter((attachment) => attachment.kind === "image" && displayText.includes(attachment.label));
|
|
1904
|
+
const labels = new Set();
|
|
1905
|
+
const imageIds = new Set();
|
|
1906
|
+
for (const attachment of candidateAttachments) {
|
|
1907
|
+
if (labels.has(attachment.label))
|
|
1908
|
+
throw new Error(`Duplicate image label in one submission: ${attachment.label}`);
|
|
1909
|
+
if (imageIds.has(attachment.imageId))
|
|
1910
|
+
throw new Error(`Duplicate image identity in one submission: ${attachment.imageId}`);
|
|
1911
|
+
labels.add(attachment.label);
|
|
1912
|
+
imageIds.add(attachment.imageId);
|
|
1913
|
+
}
|
|
1914
|
+
const activeAttachments = candidateAttachments;
|
|
1456
1915
|
const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
|
|
1457
1916
|
if (activeAttachments.length === 0) {
|
|
1458
1917
|
const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
|
|
@@ -1467,7 +1926,7 @@ function buildWebPromptPayload(displayText, attachments) {
|
|
|
1467
1926
|
break;
|
|
1468
1927
|
}
|
|
1469
1928
|
pushTextBlock(blocks, displayText.slice(cursor, next.index));
|
|
1470
|
-
blocks.push({ type: "image", mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
|
|
1929
|
+
blocks.push({ type: "image", imageId: next.attachment.imageId, mimeType: next.attachment.mimeType, data: next.attachment.data, label: next.attachment.label });
|
|
1471
1930
|
cursor = next.index + next.attachment.label.length;
|
|
1472
1931
|
}
|
|
1473
1932
|
pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
|
|
@@ -1515,6 +1974,8 @@ function pushTextBlock(blocks, text) {
|
|
|
1515
1974
|
function reduceStatus(status, event) {
|
|
1516
1975
|
if (event.type === "state")
|
|
1517
1976
|
return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
|
|
1977
|
+
if (event.type === "context.compacted")
|
|
1978
|
+
return { ...status, phase: "compacting", detail: formatCompactionReportSummary(event.compaction), activityTick: status.activityTick + 1 };
|
|
1518
1979
|
if (event.type === "context.metrics")
|
|
1519
1980
|
return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
|
|
1520
1981
|
if (event.type === "usage")
|
|
@@ -1843,6 +2304,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1843
2304
|
if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
|
|
1844
2305
|
return true;
|
|
1845
2306
|
const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
|
|
2307
|
+
const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
|
|
1846
2308
|
let rendered = false;
|
|
1847
2309
|
for (const [blockIndex, block] of message.blocks.entries()) {
|
|
1848
2310
|
if (block.type === "text") {
|
|
@@ -1859,7 +2321,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1859
2321
|
const line = imageLineForBlock(message.role, block, message.id, blockIndex);
|
|
1860
2322
|
if (!line)
|
|
1861
2323
|
continue;
|
|
1862
|
-
append({ ...line, messageId: message.id, parentToolName });
|
|
2324
|
+
append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
|
|
1863
2325
|
rendered = true;
|
|
1864
2326
|
}
|
|
1865
2327
|
else if (block.type === "thinking") {
|
|
@@ -1869,22 +2331,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1869
2331
|
rendered = true;
|
|
1870
2332
|
}
|
|
1871
2333
|
else if (block.type === "tool_use" && options.includeToolUseBlocks) {
|
|
1872
|
-
append({ ...formatToolUse(block), messageId: message.id, live: false });
|
|
2334
|
+
append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
|
|
1873
2335
|
rendered = true;
|
|
1874
2336
|
}
|
|
1875
2337
|
else if (block.type === "tool_result") {
|
|
1876
|
-
append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id });
|
|
2338
|
+
append({ ...formatToolResultLine(block.name, block.output, block.ok, options.presentToolResult), messageId: message.id, toolUseId: block.toolUseId });
|
|
1877
2339
|
rendered = true;
|
|
1878
2340
|
}
|
|
1879
2341
|
}
|
|
1880
2342
|
return rendered;
|
|
1881
2343
|
}
|
|
1882
|
-
function renderToolResultMessage(message, append) {
|
|
2344
|
+
function renderToolResultMessage(message, append, presentToolResult) {
|
|
1883
2345
|
let rendered = false;
|
|
1884
2346
|
for (const block of message.blocks) {
|
|
1885
2347
|
if (block.type !== "tool_result")
|
|
1886
2348
|
continue;
|
|
1887
|
-
append({ ...formatToolResultLine(block.name, block.output, block.ok), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
|
|
2349
|
+
append({ ...formatToolResultLine(block.name, block.output, block.ok, presentToolResult), messageId: message.id, toolUseId: block.toolUseId }, block.toolUseId);
|
|
1888
2350
|
rendered = true;
|
|
1889
2351
|
}
|
|
1890
2352
|
return rendered;
|
|
@@ -1908,13 +2370,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
|
|
|
1908
2370
|
const kind = kindForRole(role);
|
|
1909
2371
|
if (kind === "meta")
|
|
1910
2372
|
return undefined;
|
|
2373
|
+
const resolution = resolveImageBlockDataResultSync(block);
|
|
1911
2374
|
return {
|
|
1912
2375
|
kind,
|
|
1913
2376
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1914
2377
|
image: {
|
|
1915
|
-
src: `/api/images/${encodeURIComponent(messageId)}/${blockIndex}
|
|
2378
|
+
src: resolution.available ? (block.imageId ? `/api/images/by-id/${encodeURIComponent(block.imageId)}` : `/api/images/${encodeURIComponent(messageId)}/${blockIndex}`) : undefined,
|
|
2379
|
+
imageId: block.imageId,
|
|
1916
2380
|
label: block.label,
|
|
1917
2381
|
mimeType: block.mimeType,
|
|
2382
|
+
available: resolution.available,
|
|
2383
|
+
error: resolution.available ? undefined : resolution.error,
|
|
2384
|
+
blockIndex,
|
|
1918
2385
|
},
|
|
1919
2386
|
};
|
|
1920
2387
|
}
|
|
@@ -1966,20 +2433,187 @@ function titleForKind(kind) {
|
|
|
1966
2433
|
function systemLine(text, summaryMaxLines) {
|
|
1967
2434
|
return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
|
|
1968
2435
|
}
|
|
2436
|
+
function legacyCompactionReport(reason) {
|
|
2437
|
+
return {
|
|
2438
|
+
reason: reason && reason !== "none" ? reason : "manualcompact",
|
|
2439
|
+
summary: "此检查点由旧版 neoctl 持久化,未保存详细压缩报告。",
|
|
2440
|
+
continuationState: "该旧版检查点没有可用的详细续接上下文。",
|
|
2441
|
+
sourceMessages: 0,
|
|
2442
|
+
preservedUserMessages: 0,
|
|
2443
|
+
newWindowMessages: 0,
|
|
2444
|
+
charsFreed: 0,
|
|
2445
|
+
modelDriven: false,
|
|
2446
|
+
imageCount: 0,
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function compactionLine(report, createdAt = new Date().toISOString(), current = true) {
|
|
2450
|
+
return {
|
|
2451
|
+
kind: "meta",
|
|
2452
|
+
title: "Context compaction",
|
|
2453
|
+
text: formatCompactionReportSummary(report),
|
|
2454
|
+
format: "plain",
|
|
2455
|
+
compaction: { ...report, createdAt, current },
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function compactionReportFromBoundary(message) {
|
|
2459
|
+
if (!message?.metadata?.compactBoundary)
|
|
2460
|
+
return undefined;
|
|
2461
|
+
const stored = message.metadata.compactionReport;
|
|
2462
|
+
if (stored?.continuationState)
|
|
2463
|
+
return { ...stored };
|
|
2464
|
+
const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
2465
|
+
const continuationState = /<compact_state>\s*([\s\S]*?)\s*<\/compact_state>/i.exec(text)?.[1]?.trim();
|
|
2466
|
+
if (!continuationState)
|
|
2467
|
+
return undefined;
|
|
2468
|
+
const reasonValue = typeof message.metadata.compactionReason === "string" ? message.metadata.compactionReason : "autocompact";
|
|
2469
|
+
const reason = isCompactionReason(reasonValue) ? reasonValue : "autocompact";
|
|
2470
|
+
const imageRegistry = message.metadata.imageRegistry;
|
|
2471
|
+
return {
|
|
2472
|
+
reason,
|
|
2473
|
+
summary: continuationState,
|
|
2474
|
+
continuationState,
|
|
2475
|
+
sourceMessages: 0,
|
|
2476
|
+
preservedUserMessages: 0,
|
|
2477
|
+
newWindowMessages: 1,
|
|
2478
|
+
charsFreed: 0,
|
|
2479
|
+
modelDriven: message.metadata.modelDriven === true,
|
|
2480
|
+
imageCount: Array.isArray(imageRegistry?.images) ? imageRegistry.images.length : 0,
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
function isCompactionReason(value) {
|
|
2484
|
+
return ["snip", "microcompact", "autocompact", "reactive_compact", "manualcompact", "purecompact"].includes(value);
|
|
2485
|
+
}
|
|
2486
|
+
function formatCompactionReportSummary(report) {
|
|
2487
|
+
return `${report.newWindowMessages} message(s) in new window, ${report.preservedUserMessages} user message(s) preserved`;
|
|
2488
|
+
}
|
|
1969
2489
|
function thinkingLine(text, live = false) {
|
|
1970
2490
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1971
2491
|
}
|
|
1972
2492
|
function formatToolUse(toolUse) {
|
|
2493
|
+
const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
|
|
1973
2494
|
if (toolUse.name === "plan" && isWebPlanPayload(toolUse.input))
|
|
1974
|
-
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), collapsible: true };
|
|
2495
|
+
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: webPlanBodyTitle(toolUse.input), text: serializeWebPlanPayload(toolUse.input), presentationLevel, collapsible: true };
|
|
1975
2496
|
if (toolUse.name === "image2")
|
|
1976
|
-
return { kind: "tool", toolName: toolUse.name, toolUseId: toolUse.id, title: toolTitle(toolUse.name, "running"), bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", collapsible: true };
|
|
2497
|
+
return { kind: "tool", toolName: toolUse.name, toolUseId: toolUse.id, title: toolTitle(toolUse.name, "running"), bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", presentationLevel: "primary", collapsible: true };
|
|
1977
2498
|
const summary = summarizeToolUse(toolUse.name, toolUse.input);
|
|
1978
|
-
return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
|
|
2499
|
+
return { kind: "tool", toolName: toolUse.name, toolUseId: toolUse.id, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
|
|
2500
|
+
}
|
|
2501
|
+
function formatToolResultLine(toolName, output, ok, presentToolResult) {
|
|
2502
|
+
const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
|
|
2503
|
+
const resources = presentation?.resources ?? toolResources(output);
|
|
2504
|
+
const formatted = resources?.length
|
|
2505
|
+
? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
|
|
2506
|
+
: formatToolResult(toolName, output, ok);
|
|
2507
|
+
return {
|
|
2508
|
+
kind: ok ? "tool" : "error",
|
|
2509
|
+
toolName,
|
|
2510
|
+
title: presentation?.title ?? toolTitle(toolName, "finished"),
|
|
2511
|
+
bodyTitle: presentation?.bodyTitle ?? formatted.bodyTitle,
|
|
2512
|
+
titleStatus: ok ? "success" : "failure",
|
|
2513
|
+
text: presentation?.text ?? formatted.text,
|
|
2514
|
+
toolDisplay: resources?.length ? undefined : buildToolResultDisplay(toolName, output, ok),
|
|
2515
|
+
format: formatted.format,
|
|
2516
|
+
live: false,
|
|
2517
|
+
previewStyle: formatted.full ? undefined : "summary",
|
|
2518
|
+
summaryMaxLines: formatted.summaryMaxLines,
|
|
2519
|
+
presentationLevel: presentation?.presentationLevel ?? toolPresentationLevel(toolName, output),
|
|
2520
|
+
resources,
|
|
2521
|
+
collapsible: true,
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function toolPresentationLevel(toolName, payload) {
|
|
2525
|
+
if (toolName === "image2")
|
|
2526
|
+
return "primary";
|
|
2527
|
+
if (!isRecord(payload))
|
|
2528
|
+
return "process";
|
|
2529
|
+
const direct = payload.presentationLevel;
|
|
2530
|
+
const ui = isRecord(payload.ui) ? payload.ui.presentationLevel : undefined;
|
|
2531
|
+
const underscoredUi = isRecord(payload._ui) ? payload._ui.presentationLevel : undefined;
|
|
2532
|
+
return [direct, ui, underscoredUi].some((value) => value === "primary") ? "primary" : "process";
|
|
2533
|
+
}
|
|
2534
|
+
function toolUiMetadata(payload) {
|
|
2535
|
+
if (!isRecord(payload))
|
|
2536
|
+
return undefined;
|
|
2537
|
+
const containers = [payload, payload.output, payload.result].filter(isRecord);
|
|
2538
|
+
for (const container of containers) {
|
|
2539
|
+
if (isRecord(container._ui))
|
|
2540
|
+
return container._ui;
|
|
2541
|
+
if (isRecord(container.ui))
|
|
2542
|
+
return container.ui;
|
|
2543
|
+
}
|
|
2544
|
+
return undefined;
|
|
2545
|
+
}
|
|
2546
|
+
function toolResources(payload) {
|
|
2547
|
+
const ui = toolUiMetadata(payload);
|
|
2548
|
+
if (!ui || !Array.isArray(ui.resources))
|
|
2549
|
+
return undefined;
|
|
2550
|
+
return normalizeUiResources(ui.resources);
|
|
2551
|
+
}
|
|
2552
|
+
function normalizeUiResources(values) {
|
|
2553
|
+
if (!Array.isArray(values))
|
|
2554
|
+
return undefined;
|
|
2555
|
+
const resources = values.flatMap((value) => {
|
|
2556
|
+
if (!isRecord(value))
|
|
2557
|
+
return [];
|
|
2558
|
+
const kind = stringValue(value.kind);
|
|
2559
|
+
const url = stringValue(value.url);
|
|
2560
|
+
if (!kind || !url || !safeResourceUrl(url))
|
|
2561
|
+
return [];
|
|
2562
|
+
return [{
|
|
2563
|
+
kind,
|
|
2564
|
+
url,
|
|
2565
|
+
reference: stringValue(value.reference) ?? canonicalResourceReference(url),
|
|
2566
|
+
label: stringValue(value.label),
|
|
2567
|
+
downloadName: stringValue(value.downloadName),
|
|
2568
|
+
sizeBytes: numberValue(value.sizeBytes),
|
|
2569
|
+
expiresAt: numberValue(value.expiresAt),
|
|
2570
|
+
mimeType: stringValue(value.mimeType),
|
|
2571
|
+
height: clampResourceHeight(numberValue(value.height)),
|
|
2572
|
+
}];
|
|
2573
|
+
});
|
|
2574
|
+
return resources.length > 0 ? resources : undefined;
|
|
1979
2575
|
}
|
|
1980
|
-
function
|
|
1981
|
-
|
|
1982
|
-
|
|
2576
|
+
function canonicalResourceReference(url) {
|
|
2577
|
+
return url.startsWith("/") ? `sandbox:${url}` : url;
|
|
2578
|
+
}
|
|
2579
|
+
function clampResourceHeight(value) {
|
|
2580
|
+
return value === undefined ? undefined : Math.max(240, Math.min(1600, Math.round(value)));
|
|
2581
|
+
}
|
|
2582
|
+
function normalizeToolResultPresentation(value) {
|
|
2583
|
+
if (!isRecord(value))
|
|
2584
|
+
return undefined;
|
|
2585
|
+
const level = value.presentationLevel === "primary" || value.presentationLevel === "process" ? value.presentationLevel : undefined;
|
|
2586
|
+
return {
|
|
2587
|
+
title: stringValue(value.title),
|
|
2588
|
+
bodyTitle: stringValue(value.bodyTitle),
|
|
2589
|
+
text: stringValue(value.text),
|
|
2590
|
+
presentationLevel: level,
|
|
2591
|
+
resources: normalizeUiResources(value.resources),
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
function runtimeToolResultPresenter(runtime) {
|
|
2595
|
+
return (toolName, output, ok) => {
|
|
2596
|
+
const support = runtime.pluginSupport;
|
|
2597
|
+
if (!support)
|
|
2598
|
+
return undefined;
|
|
2599
|
+
const sessionId = runtime.engine.snapshot().session?.sessionId;
|
|
2600
|
+
for (const plugin of activeWebRuntimePlugins(support.catalog, support.overrides)) {
|
|
2601
|
+
if (typeof plugin.presentToolResult !== "function")
|
|
2602
|
+
continue;
|
|
2603
|
+
try {
|
|
2604
|
+
const presentation = plugin.presentToolResult({ toolName, output, ok, sessionId });
|
|
2605
|
+
if (presentation !== undefined && presentation !== null)
|
|
2606
|
+
return presentation;
|
|
2607
|
+
}
|
|
2608
|
+
catch (error) {
|
|
2609
|
+
console.warn(`plugin ${plugin.id} failed to present ${toolName}:`, error);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return undefined;
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
function safeResourceUrl(value) {
|
|
2616
|
+
return /^(?:https?:\/\/|\/)/i.test(value) && !/^\/\//.test(value);
|
|
1983
2617
|
}
|
|
1984
2618
|
function toolTitle(toolName, _phase) {
|
|
1985
2619
|
const labels = {
|
|
@@ -1990,13 +2624,30 @@ function toolTitle(toolName, _phase) {
|
|
|
1990
2624
|
expose_downloads: "文件下载",
|
|
1991
2625
|
grep: "搜索文本",
|
|
1992
2626
|
image2: "图片生成",
|
|
1993
|
-
image_note: "
|
|
2627
|
+
image_note: "记录图片",
|
|
1994
2628
|
list: "列出文件",
|
|
1995
2629
|
load_image: "读取图片",
|
|
1996
2630
|
plan: "任务计划",
|
|
1997
2631
|
read: "读取文件",
|
|
1998
2632
|
search: "网络搜索",
|
|
1999
2633
|
write: "写入文件",
|
|
2634
|
+
SendMessage: "发送协作消息",
|
|
2635
|
+
sendmessage: "发送协作消息",
|
|
2636
|
+
TaskGet: "读取后台任务",
|
|
2637
|
+
taskget: "读取后台任务",
|
|
2638
|
+
TaskList: "后台任务列表",
|
|
2639
|
+
tasklist: "后台任务列表",
|
|
2640
|
+
TaskOutput: "读取任务输出",
|
|
2641
|
+
taskoutput: "读取任务输出",
|
|
2642
|
+
TaskResume: "继续后台任务",
|
|
2643
|
+
taskresume: "继续后台任务",
|
|
2644
|
+
TaskStop: "停止后台任务",
|
|
2645
|
+
taskstop: "停止后台任务",
|
|
2646
|
+
multi_tool_use: "并行执行工具",
|
|
2647
|
+
secret_list: "密钥列表",
|
|
2648
|
+
secret_info: "查看密钥",
|
|
2649
|
+
secret_request: "申请密钥",
|
|
2650
|
+
agent_report: "子任务报告",
|
|
2000
2651
|
};
|
|
2001
2652
|
return labels[toolName] ?? toolName;
|
|
2002
2653
|
}
|
|
@@ -2027,6 +2678,253 @@ function toolUsePurpose(toolName, input) {
|
|
|
2027
2678
|
return `修改 ${path.basename(input.path)}`;
|
|
2028
2679
|
return undefined;
|
|
2029
2680
|
}
|
|
2681
|
+
function mergePresentationLevel(result, started) {
|
|
2682
|
+
return result === "primary" || started === "primary" ? "primary" : result ?? started;
|
|
2683
|
+
}
|
|
2684
|
+
function mergeToolDisplays(result, started) {
|
|
2685
|
+
if (!result)
|
|
2686
|
+
return started;
|
|
2687
|
+
if (!started)
|
|
2688
|
+
return result;
|
|
2689
|
+
const facts = [...started.facts];
|
|
2690
|
+
for (const fact of result.facts) {
|
|
2691
|
+
const existing = facts.findIndex((item) => item.label === fact.label);
|
|
2692
|
+
if (existing >= 0)
|
|
2693
|
+
facts[existing] = fact;
|
|
2694
|
+
else
|
|
2695
|
+
facts.push(fact);
|
|
2696
|
+
}
|
|
2697
|
+
const subject = result.subject || started.subject;
|
|
2698
|
+
const purpose = result.purpose || started.purpose;
|
|
2699
|
+
return normalizeToolDisplay({
|
|
2700
|
+
subject,
|
|
2701
|
+
purpose,
|
|
2702
|
+
facts,
|
|
2703
|
+
previews: result.previews.length > 0 ? result.previews : started.previews,
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
function normalizeToolDisplay(display) {
|
|
2707
|
+
let subject = display.subject?.trim() || undefined;
|
|
2708
|
+
const purpose = display.purpose?.trim() || undefined;
|
|
2709
|
+
if (subject && purpose && normalizeDisplayText(subject) === normalizeDisplayText(purpose))
|
|
2710
|
+
subject = undefined;
|
|
2711
|
+
const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
|
|
2712
|
+
const facts = display.facts.filter((fact) => {
|
|
2713
|
+
const normalized = normalizeDisplayText(fact.value);
|
|
2714
|
+
if (!normalized || visibleText.has(normalized))
|
|
2715
|
+
return false;
|
|
2716
|
+
visibleText.add(normalized);
|
|
2717
|
+
return true;
|
|
2718
|
+
});
|
|
2719
|
+
const previews = display.previews.filter((preview) => {
|
|
2720
|
+
const normalized = normalizeDisplayText(preview.content);
|
|
2721
|
+
if (!normalized || visibleText.has(normalized))
|
|
2722
|
+
return false;
|
|
2723
|
+
visibleText.add(normalized);
|
|
2724
|
+
return true;
|
|
2725
|
+
});
|
|
2726
|
+
return { subject, purpose, facts, previews };
|
|
2727
|
+
}
|
|
2728
|
+
function normalizeDisplayText(value) {
|
|
2729
|
+
return value.replace(/\s+/gu, " ").trim().toLowerCase();
|
|
2730
|
+
}
|
|
2731
|
+
function buildToolUseDisplay(toolName, input) {
|
|
2732
|
+
const data = isRecord(input) ? input : {};
|
|
2733
|
+
const purpose = stringValue(data.description);
|
|
2734
|
+
const subject = toolName === "exec_command" ? undefined : toolDisplaySubject(toolName, data);
|
|
2735
|
+
const facts = [];
|
|
2736
|
+
const previews = [];
|
|
2737
|
+
if (toolName === "exec_command")
|
|
2738
|
+
pushToolPreview(previews, "命令", "code", data.cmd);
|
|
2739
|
+
else if (toolName === "write_stdin") {
|
|
2740
|
+
pushToolFact(facts, "会话", data.session_id, true);
|
|
2741
|
+
pushToolFact(facts, "输入", data.chars, true);
|
|
2742
|
+
}
|
|
2743
|
+
else if (toolName === "grep")
|
|
2744
|
+
pushToolFact(facts, "范围", data.path, true);
|
|
2745
|
+
else if (toolName === "load_image")
|
|
2746
|
+
pushToolFact(facts, "图片", arrayLabel(data.imageRefs), true);
|
|
2747
|
+
else if (toolName === "image2") {
|
|
2748
|
+
pushToolFact(facts, "模式", data.mode === "edit" ? "编辑" : "生成");
|
|
2749
|
+
pushToolFact(facts, "名称", data.semanticName);
|
|
2750
|
+
}
|
|
2751
|
+
else if (toolName === "expose_downloads")
|
|
2752
|
+
pushToolFact(facts, "文件", arrayLabel(data.paths), true);
|
|
2753
|
+
else if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage")
|
|
2754
|
+
pushToolFact(facts, "接收方", data.target);
|
|
2755
|
+
else if (toolName.toLowerCase().startsWith("task"))
|
|
2756
|
+
pushToolFact(facts, "任务", data.task_id, true);
|
|
2757
|
+
else if (toolName.startsWith("secret_")) {
|
|
2758
|
+
pushToolFact(facts, "密钥", data.key, true);
|
|
2759
|
+
pushToolFact(facts, "用途", data.reason);
|
|
2760
|
+
}
|
|
2761
|
+
else if (toolName === "agent") {
|
|
2762
|
+
pushToolFact(facts, "任务", data.description ?? data.prompt);
|
|
2763
|
+
pushToolFact(facts, "类型", data.subagent_type ?? data.mode);
|
|
2764
|
+
}
|
|
2765
|
+
return normalizeToolDisplay({ subject, purpose, facts, previews });
|
|
2766
|
+
}
|
|
2767
|
+
function buildToolResultDisplay(toolName, output, ok) {
|
|
2768
|
+
const data = isRecord(output) ? output : {};
|
|
2769
|
+
const facts = [];
|
|
2770
|
+
const previews = [];
|
|
2771
|
+
let subject = toolDisplaySubject(toolName, data);
|
|
2772
|
+
let purpose = stringValue(data.description);
|
|
2773
|
+
if ((toolName === "edit" || toolName === "write") && isEditToolOutput(data)) {
|
|
2774
|
+
subject = data.path;
|
|
2775
|
+
pushToolPreview(previews, undefined, "diff", compactEditDiff(data));
|
|
2776
|
+
}
|
|
2777
|
+
else if (isExecOutput(data)) {
|
|
2778
|
+
subject = undefined;
|
|
2779
|
+
purpose = stringValue(data.description);
|
|
2780
|
+
pushToolPreview(previews, "命令", "code", data.command);
|
|
2781
|
+
const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
|
|
2782
|
+
if (outputText)
|
|
2783
|
+
pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
|
|
2784
|
+
if (data.timed_out && !outputText)
|
|
2785
|
+
pushToolPreview(previews, undefined, "text", "命令执行超时");
|
|
2786
|
+
else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
|
|
2787
|
+
pushToolPreview(previews, undefined, "text", `命令退出码:${data.exit_code ?? "未知"}`);
|
|
2788
|
+
}
|
|
2789
|
+
else if (toolName === "grep") {
|
|
2790
|
+
subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
|
|
2791
|
+
pushToolPreview(previews, undefined, "list", grepPreview(data));
|
|
2792
|
+
}
|
|
2793
|
+
else if (toolName === "search") {
|
|
2794
|
+
subject = stringValue(data.query);
|
|
2795
|
+
pushToolPreview(previews, undefined, "list", searchPreview(data));
|
|
2796
|
+
}
|
|
2797
|
+
else if (toolName === "read") {
|
|
2798
|
+
subject = stringValue(data.path);
|
|
2799
|
+
if (numberValue(data.startLine) !== undefined && numberValue(data.endLine) !== undefined)
|
|
2800
|
+
pushToolFact(facts, "范围", `${data.startLine}–${data.endLine} 行`);
|
|
2801
|
+
pushToolPreview(previews, undefined, "code", data.content);
|
|
2802
|
+
}
|
|
2803
|
+
else if (toolName === "list") {
|
|
2804
|
+
subject = stringValue(data.path);
|
|
2805
|
+
pushToolPreview(previews, undefined, "list", listPreview(data));
|
|
2806
|
+
}
|
|
2807
|
+
else if (toolName === "image2") {
|
|
2808
|
+
subject = stringValue(data.semanticName);
|
|
2809
|
+
pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
|
|
2810
|
+
pushToolFact(facts, "尺寸", data.size);
|
|
2811
|
+
}
|
|
2812
|
+
else if (toolName === "load_image") {
|
|
2813
|
+
subject = arrayLabel(data.imageRefs);
|
|
2814
|
+
}
|
|
2815
|
+
else if (toolResources(output)?.length) {
|
|
2816
|
+
subject = toolResources(output)?.map((item) => item.label || item.downloadName).filter(Boolean).join("、");
|
|
2817
|
+
}
|
|
2818
|
+
else if (toolName.toLowerCase().startsWith("task") || toolName === "agent") {
|
|
2819
|
+
subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
|
|
2820
|
+
}
|
|
2821
|
+
else if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage") {
|
|
2822
|
+
subject = stringValue(data.target);
|
|
2823
|
+
}
|
|
2824
|
+
else if (toolName.startsWith("secret_")) {
|
|
2825
|
+
subject = stringValue(data.key);
|
|
2826
|
+
pushToolFact(facts, "密钥", data.key, true);
|
|
2827
|
+
if (toolName === "secret_list")
|
|
2828
|
+
pushToolFact(facts, "数量", `${arrayLength(data.secrets)} 项`);
|
|
2829
|
+
}
|
|
2830
|
+
else if (toolName === "agent_report") {
|
|
2831
|
+
}
|
|
2832
|
+
else {
|
|
2833
|
+
subject = subject || stringValue(data.path) || stringValue(data.id) || stringValue(data.name);
|
|
2834
|
+
}
|
|
2835
|
+
return normalizeToolDisplay({ subject, purpose, facts, previews });
|
|
2836
|
+
}
|
|
2837
|
+
function toolDisplaySubject(toolName, data) {
|
|
2838
|
+
if (["read", "write", "edit", "list"].includes(toolName))
|
|
2839
|
+
return stringValue(data.path);
|
|
2840
|
+
if (toolName === "exec_command")
|
|
2841
|
+
return stringValue(data.cmd) || stringValue(data.command);
|
|
2842
|
+
if (toolName === "grep" || toolName === "search")
|
|
2843
|
+
return stringValue(data.query);
|
|
2844
|
+
if (toolName === "image2")
|
|
2845
|
+
return stringValue(data.semanticName);
|
|
2846
|
+
if (toolName === "SendMessage" || toolName.toLowerCase() === "sendmessage")
|
|
2847
|
+
return stringValue(data.target);
|
|
2848
|
+
if (toolName.toLowerCase().startsWith("task"))
|
|
2849
|
+
return stringValue(data.task_id);
|
|
2850
|
+
return undefined;
|
|
2851
|
+
}
|
|
2852
|
+
function pushToolPreview(previews, label, kind, value) {
|
|
2853
|
+
if (typeof value !== "string" || !value.trim())
|
|
2854
|
+
return;
|
|
2855
|
+
const content = value.replace(/\r\n/g, "\n").trim();
|
|
2856
|
+
previews.push({ label, kind, content: truncate(content, 1400) });
|
|
2857
|
+
}
|
|
2858
|
+
function compactEditDiff(output) {
|
|
2859
|
+
const lines = [];
|
|
2860
|
+
for (const hunk of output.patch.slice(0, 3)) {
|
|
2861
|
+
lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
2862
|
+
lines.push(...hunk.lines.slice(0, 14));
|
|
2863
|
+
}
|
|
2864
|
+
return lines.join("\n");
|
|
2865
|
+
}
|
|
2866
|
+
function grepPreview(data) {
|
|
2867
|
+
if (!Array.isArray(data.matches))
|
|
2868
|
+
return "";
|
|
2869
|
+
return data.matches.slice(0, 5).filter(isRecord).map((match) => {
|
|
2870
|
+
const file = stringValue(match.file) || "";
|
|
2871
|
+
const line = numberValue(match.line);
|
|
2872
|
+
const text = stringValue(match.text) || "";
|
|
2873
|
+
return `${file}${line !== undefined ? `:${line}` : ""} ${text}`.trim();
|
|
2874
|
+
}).filter(Boolean).join("\n");
|
|
2875
|
+
}
|
|
2876
|
+
function searchPreview(data) {
|
|
2877
|
+
if (!Array.isArray(data.results))
|
|
2878
|
+
return "";
|
|
2879
|
+
return data.results.slice(0, 4).filter(isRecord).map((result) => {
|
|
2880
|
+
const title = stringValue(result.title) || stringValue(result.url) || "";
|
|
2881
|
+
return title;
|
|
2882
|
+
}).filter(Boolean).join("\n");
|
|
2883
|
+
}
|
|
2884
|
+
function listPreview(data) {
|
|
2885
|
+
if (!Array.isArray(data.entries))
|
|
2886
|
+
return "";
|
|
2887
|
+
return data.entries.slice(0, 8).filter(isRecord).map((entry) => stringValue(entry.path) || stringValue(entry.name) || "").filter(Boolean).join("\n");
|
|
2888
|
+
}
|
|
2889
|
+
function pushToolFact(facts, label, value, code = false, tone = "neutral") {
|
|
2890
|
+
const normalized = stringValue(value);
|
|
2891
|
+
if (!normalized || facts.some((fact) => fact.label === label && fact.value === normalized))
|
|
2892
|
+
return;
|
|
2893
|
+
facts.push({ label, value: truncate(normalized, 220), code, tone });
|
|
2894
|
+
}
|
|
2895
|
+
function stringValue(value) {
|
|
2896
|
+
if (typeof value === "string" && value.trim())
|
|
2897
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
2898
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
2899
|
+
return String(value);
|
|
2900
|
+
return undefined;
|
|
2901
|
+
}
|
|
2902
|
+
function numberValue(value) {
|
|
2903
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2904
|
+
}
|
|
2905
|
+
function arrayLength(value) {
|
|
2906
|
+
return Array.isArray(value) ? value.length : 0;
|
|
2907
|
+
}
|
|
2908
|
+
function arrayLabel(value) {
|
|
2909
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0).join("、") : undefined;
|
|
2910
|
+
}
|
|
2911
|
+
function editOperationLabel(operation) {
|
|
2912
|
+
if (operation === "create")
|
|
2913
|
+
return "新建";
|
|
2914
|
+
if (operation === "edit")
|
|
2915
|
+
return "修改";
|
|
2916
|
+
if (operation === "write")
|
|
2917
|
+
return "写入";
|
|
2918
|
+
return operation;
|
|
2919
|
+
}
|
|
2920
|
+
function formatDuration(durationMs) {
|
|
2921
|
+
return durationMs >= 1000 ? `${(durationMs / 1000).toFixed(durationMs >= 10000 ? 0 : 1)} 秒` : `${durationMs} 毫秒`;
|
|
2922
|
+
}
|
|
2923
|
+
function taskStatusDisplay(value, ok) {
|
|
2924
|
+
const labels = { pending: "排队中", queued: "排队中", running: "运行中", completed: "已完成", failed: "失败", killed: "已停止", stopped: "已停止" };
|
|
2925
|
+
const status = stringValue(value);
|
|
2926
|
+
return status ? labels[status] ?? status : ok ? "已完成" : "失败";
|
|
2927
|
+
}
|
|
2030
2928
|
function formatToolResult(toolName, output, ok) {
|
|
2031
2929
|
if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
|
|
2032
2930
|
return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
|
|
@@ -2042,36 +2940,15 @@ function formatToolResult(toolName, output, ok) {
|
|
|
2042
2940
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2043
2941
|
if (toolName === "image2" && isRecord(output))
|
|
2044
2942
|
return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
|
|
2045
|
-
|
|
2046
|
-
|
|
2943
|
+
const resources = toolResources(output);
|
|
2944
|
+
if (resources?.length)
|
|
2945
|
+
return { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" };
|
|
2047
2946
|
if (toolName === "plan" && isWebPlanPayload(output))
|
|
2048
2947
|
return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
|
|
2049
2948
|
if (typeof output === "string")
|
|
2050
2949
|
return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2051
2950
|
return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2052
2951
|
}
|
|
2053
|
-
function formatExposeDownloadsToolResult(output, ok) {
|
|
2054
|
-
const downloads = extractDownloadEntries(output);
|
|
2055
|
-
if (!ok)
|
|
2056
|
-
return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
|
|
2057
|
-
if (!downloads.length)
|
|
2058
|
-
return "文件已准备好,请点击下载。";
|
|
2059
|
-
return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
|
|
2060
|
-
}
|
|
2061
|
-
function formatDownloadEntries(downloads) {
|
|
2062
|
-
return downloads.map((entry) => {
|
|
2063
|
-
const filename = entry.filename || entry.name || "下载文件";
|
|
2064
|
-
const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
|
|
2065
|
-
return url ? `- ${filename}: ${url}` : `- ${filename}`;
|
|
2066
|
-
}).join("\n");
|
|
2067
|
-
}
|
|
2068
|
-
function extractDownloadEntries(output) {
|
|
2069
|
-
if (!isRecord(output))
|
|
2070
|
-
return [];
|
|
2071
|
-
const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
|
|
2072
|
-
const downloads = candidates.find(Array.isArray);
|
|
2073
|
-
return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
|
|
2074
|
-
}
|
|
2075
2952
|
function isEditToolOutput(value) {
|
|
2076
2953
|
return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
|
|
2077
2954
|
}
|
|
@@ -2376,8 +3253,9 @@ function formatUsageTotals(totals) {
|
|
|
2376
3253
|
}
|
|
2377
3254
|
function formatManualCompaction(result) {
|
|
2378
3255
|
if (!result.changed)
|
|
2379
|
-
return "No context
|
|
2380
|
-
|
|
3256
|
+
return "No conversation context available to compact.";
|
|
3257
|
+
const preservedUsers = result.report?.preservedUserMessages ?? result.messages.filter((message) => message.metadata?.compactPreservedUser === true).length;
|
|
3258
|
+
return `context compacted: ${result.messages.length} message(s) in new window, ${preservedUsers} user message(s) preserved`;
|
|
2381
3259
|
}
|
|
2382
3260
|
function formatPureCompaction(result) {
|
|
2383
3261
|
if (!result.changed)
|
|
@@ -2387,6 +3265,8 @@ function formatPureCompaction(result) {
|
|
|
2387
3265
|
function formatNumber(value) {
|
|
2388
3266
|
return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2389
3267
|
}
|
|
3268
|
+
const TERMINAL_OUTPUT_FLUSH_MS = 100;
|
|
3269
|
+
const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
|
|
2390
3270
|
const THINKING_SUMMARY_MAX_LINES = 1000;
|
|
2391
3271
|
const EXPANDED_SUMMARY_MAX_LINES = 1000;
|
|
2392
3272
|
const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
|