neoctl 0.2.30 → 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 +37 -3
- 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 +9 -0
- package/dist/tools/builtins/exec-process-manager.js +45 -4
- package/dist/tools/builtins/exec-process-manager.js.map +1 -1
- package/dist/tools/builtins/exec-tool.js +10 -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 +12 -0
- 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 +159 -9
- package/dist/web/index.js +967 -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.js +34 -3
- package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
- package/package.json +4 -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();
|
|
@@ -765,10 +1017,15 @@ export class WebRepl {
|
|
|
765
1017
|
kind: "agent",
|
|
766
1018
|
taskId: task.taskId,
|
|
767
1019
|
agentId: task.agentId,
|
|
1020
|
+
agentType: task.agentType,
|
|
768
1021
|
type: task.type,
|
|
769
1022
|
status: task.status,
|
|
770
1023
|
description: task.description,
|
|
1024
|
+
prompt: task.prompt,
|
|
1025
|
+
progress: task.progress,
|
|
1026
|
+
outputFile: task.outputFile,
|
|
771
1027
|
createdAt: task.createdAt,
|
|
1028
|
+
updatedAt: task.updatedAt,
|
|
772
1029
|
}));
|
|
773
1030
|
const terminalTasks = this.runtime.execProcessManager.list()
|
|
774
1031
|
.filter((terminal) => terminal.status === "running" && terminal.backgrounded)
|
|
@@ -786,8 +1043,26 @@ export class WebRepl {
|
|
|
786
1043
|
shell: terminal.shell,
|
|
787
1044
|
tty: terminal.tty,
|
|
788
1045
|
durationMs: terminal.duration_ms,
|
|
1046
|
+
output: terminal.output,
|
|
1047
|
+
outputEnd: terminal.outputEnd,
|
|
789
1048
|
}));
|
|
790
|
-
|
|
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));
|
|
791
1066
|
}
|
|
792
1067
|
async detachRunningForeground(reason) {
|
|
793
1068
|
if (!this.busy)
|
|
@@ -816,7 +1091,7 @@ export class WebRepl {
|
|
|
816
1091
|
this.interruptArmed = false;
|
|
817
1092
|
this.queuedInput = undefined;
|
|
818
1093
|
this.queuedAttachments = undefined;
|
|
819
|
-
this.
|
|
1094
|
+
this.pendingUserImageEchoIds = undefined;
|
|
820
1095
|
this.pendingUserImageEchoMessageId = undefined;
|
|
821
1096
|
this.busy = false;
|
|
822
1097
|
this.status = { ...this.status, phase: "ready", detail: undefined };
|
|
@@ -862,7 +1137,7 @@ export class WebRepl {
|
|
|
862
1137
|
this.reduce(event);
|
|
863
1138
|
if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
|
|
864
1139
|
const messageId = this.pendingUserImageEchoMessageId;
|
|
865
|
-
this.
|
|
1140
|
+
this.pendingUserImageEchoIds = undefined;
|
|
866
1141
|
this.pendingUserImageEchoMessageId = undefined;
|
|
867
1142
|
renderMessageImages(event.message, (line) => this.append(line), messageId);
|
|
868
1143
|
this.broadcastSync();
|
|
@@ -872,6 +1147,10 @@ export class WebRepl {
|
|
|
872
1147
|
this.queueDeltaStatus();
|
|
873
1148
|
return;
|
|
874
1149
|
}
|
|
1150
|
+
if (event.type === "context.compacted") {
|
|
1151
|
+
this.appendCompaction(event.compaction);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
875
1154
|
if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "retrying") {
|
|
876
1155
|
this.broadcastSync();
|
|
877
1156
|
return;
|
|
@@ -922,10 +1201,11 @@ export class WebRepl {
|
|
|
922
1201
|
const liveLineId = this.liveToolLineIds.get(toolUseId);
|
|
923
1202
|
if (liveLineId === undefined)
|
|
924
1203
|
return this.append(line);
|
|
925
|
-
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) });
|
|
926
1206
|
this.liveToolLineIds.delete(toolUseId);
|
|
927
1207
|
return liveLineId;
|
|
928
|
-
});
|
|
1208
|
+
}, runtimeToolResultPresenter(this.runtime));
|
|
929
1209
|
renderMessageImages(event.message, (line) => this.append(line));
|
|
930
1210
|
return;
|
|
931
1211
|
}
|
|
@@ -946,6 +1226,7 @@ export class WebRepl {
|
|
|
946
1226
|
if (event.type === "tool.started") {
|
|
947
1227
|
this.finalizeLiveLine(this.assistantLineId);
|
|
948
1228
|
this.finalizeThinkingLine();
|
|
1229
|
+
this.assistantLineId = undefined;
|
|
949
1230
|
const id = this.append({ ...formatToolUse(event.toolUse), live: true });
|
|
950
1231
|
this.liveToolLineIds.set(event.toolUse.id, id);
|
|
951
1232
|
return;
|
|
@@ -970,13 +1251,13 @@ export class WebRepl {
|
|
|
970
1251
|
this.append({ kind: "error", text: event.error.message });
|
|
971
1252
|
}
|
|
972
1253
|
matchesPendingUserImageEcho(message) {
|
|
973
|
-
const pending = this.
|
|
1254
|
+
const pending = this.pendingUserImageEchoIds;
|
|
974
1255
|
if (!pending?.length || message.role !== "user")
|
|
975
1256
|
return false;
|
|
976
|
-
const
|
|
977
|
-
.filter((block) => block.type === "image" && typeof block.
|
|
978
|
-
.map((block) => block.
|
|
979
|
-
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]);
|
|
980
1261
|
}
|
|
981
1262
|
async handleCommandOrPrompt(text, attachments = []) {
|
|
982
1263
|
const command = parseReplCommand(text);
|
|
@@ -1078,11 +1359,11 @@ export class WebRepl {
|
|
|
1078
1359
|
const promptPayload = buildWebPromptPayload(command.text, attachments);
|
|
1079
1360
|
const optimisticMessageId = `web-user-${this.lineId + 1}`;
|
|
1080
1361
|
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 =
|
|
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;
|
|
1086
1367
|
const runToken = ++this.foregroundRunToken;
|
|
1087
1368
|
const abortController = new AbortController();
|
|
1088
1369
|
this.activeAbortController = abortController;
|
|
@@ -1091,7 +1372,12 @@ export class WebRepl {
|
|
|
1091
1372
|
this.setStatus({ ...this.status, phase: "running", detail: "working", usage: undefined, streamedOutputTokens: 0, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined });
|
|
1092
1373
|
const engine = this.runtime.engine;
|
|
1093
1374
|
try {
|
|
1094
|
-
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
|
+
})) {
|
|
1095
1381
|
if (this.foregroundRunToken !== runToken)
|
|
1096
1382
|
continue;
|
|
1097
1383
|
if (this.runtime.engine !== engine)
|
|
@@ -1122,7 +1408,7 @@ export class WebRepl {
|
|
|
1122
1408
|
this.activeAbortController = undefined;
|
|
1123
1409
|
this.interruptArmed = false;
|
|
1124
1410
|
this.finalizeForegroundView();
|
|
1125
|
-
this.
|
|
1411
|
+
this.pendingUserImageEchoIds = undefined;
|
|
1126
1412
|
this.pendingUserImageEchoMessageId = undefined;
|
|
1127
1413
|
const queuedText = this.queuedInput;
|
|
1128
1414
|
const queuedAttach = this.queuedAttachments;
|
|
@@ -1155,7 +1441,10 @@ export class WebRepl {
|
|
|
1155
1441
|
const metrics = await this.runtime.engine.contextMetrics();
|
|
1156
1442
|
if (this.foregroundRunToken !== runToken)
|
|
1157
1443
|
return;
|
|
1158
|
-
|
|
1444
|
+
if (result.report)
|
|
1445
|
+
this.appendCompaction(result.report);
|
|
1446
|
+
else
|
|
1447
|
+
this.append(systemLine(type === "compact" ? formatManualCompaction(result) : formatPureCompaction(result)));
|
|
1159
1448
|
this.handleEvent({ type: "context.metrics", metrics });
|
|
1160
1449
|
}
|
|
1161
1450
|
catch (error) {
|
|
@@ -1174,7 +1463,9 @@ export class WebRepl {
|
|
|
1174
1463
|
}
|
|
1175
1464
|
send(subscriber, event, data) {
|
|
1176
1465
|
if (subscriber.blocked) {
|
|
1177
|
-
if (event === "
|
|
1466
|
+
if (event === "client.reload")
|
|
1467
|
+
subscriber.pendingClientReload = data;
|
|
1468
|
+
else if (event === "runtime.context")
|
|
1178
1469
|
subscriber.needsRuntimeContext = true;
|
|
1179
1470
|
else
|
|
1180
1471
|
subscriber.needsSync = true;
|
|
@@ -1189,6 +1480,11 @@ export class WebRepl {
|
|
|
1189
1480
|
subscriber.blocked = false;
|
|
1190
1481
|
if (subscriber.response.destroyed)
|
|
1191
1482
|
return;
|
|
1483
|
+
if (subscriber.pendingClientReload) {
|
|
1484
|
+
const payload = subscriber.pendingClientReload;
|
|
1485
|
+
subscriber.pendingClientReload = undefined;
|
|
1486
|
+
this.send(subscriber, "client.reload", payload);
|
|
1487
|
+
}
|
|
1192
1488
|
if (subscriber.needsSync) {
|
|
1193
1489
|
subscriber.needsSync = false;
|
|
1194
1490
|
this.send(subscriber, "sync", this.snapshot(false));
|
|
@@ -1264,6 +1560,39 @@ export class WebRepl {
|
|
|
1264
1560
|
this.pendingDeltaOperations = [];
|
|
1265
1561
|
this.pendingDeltaStatus = false;
|
|
1266
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
|
+
}
|
|
1267
1596
|
broadcastSync() {
|
|
1268
1597
|
this.discardPendingDeltaOperations();
|
|
1269
1598
|
const payload = this.snapshot(false);
|
|
@@ -1271,6 +1600,10 @@ export class WebRepl {
|
|
|
1271
1600
|
this.send(subscriber, "sync", payload);
|
|
1272
1601
|
}
|
|
1273
1602
|
}
|
|
1603
|
+
function appendBoundedText(current, incoming, limit) {
|
|
1604
|
+
const combined = current + incoming;
|
|
1605
|
+
return combined.length <= limit ? combined : combined.slice(-limit);
|
|
1606
|
+
}
|
|
1274
1607
|
export function compactLineOperations(operations) {
|
|
1275
1608
|
const compacted = [];
|
|
1276
1609
|
for (const operation of operations) {
|
|
@@ -1303,20 +1636,67 @@ async function route(req, res, router) {
|
|
|
1303
1636
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
1304
1637
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
1305
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
|
+
}
|
|
1306
1645
|
const scope = webRuntimeScopeFromUrl(url);
|
|
1307
1646
|
const repl = await router.get(scope);
|
|
1308
1647
|
if (req.method === "GET" && url.pathname === "/events")
|
|
1309
|
-
return repl.subscribe(res);
|
|
1648
|
+
return repl.subscribe(res, router.clientInfo());
|
|
1310
1649
|
if (req.method === "GET" && url.pathname === "/api/state")
|
|
1311
1650
|
return sendJson(res, repl.snapshot(true));
|
|
1312
1651
|
if (req.method === "GET" && url.pathname === "/api/runtime-context")
|
|
1313
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
|
+
}
|
|
1314
1689
|
if (req.method === "GET" && url.pathname === "/api/session-plugins")
|
|
1315
1690
|
return sendJson(res, repl.sessionPlugins());
|
|
1316
1691
|
if (req.method === "POST" && url.pathname === "/api/session-plugins") {
|
|
1317
1692
|
const body = await readJsonBody(req);
|
|
1318
1693
|
return sendJson(res, await repl.setSessionPlugins(body.overrides));
|
|
1319
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
|
+
}
|
|
1320
1700
|
const imageMatch = /^\/api\/images\/([^/]+)\/(\d+)$/u.exec(url.pathname);
|
|
1321
1701
|
if (req.method === "GET" && imageMatch) {
|
|
1322
1702
|
const payload = repl.imagePayload(decodeURIComponent(imageMatch[1]), Number(imageMatch[2]));
|
|
@@ -1332,6 +1712,10 @@ async function route(req, res, router) {
|
|
|
1332
1712
|
const body = await readJsonBody(req);
|
|
1333
1713
|
return sendJson(res, await repl.setFastMode(body.enabled === true));
|
|
1334
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
|
+
}
|
|
1335
1719
|
if (req.method === "POST" && url.pathname === "/api/submit") {
|
|
1336
1720
|
const body = await readJsonBody(req);
|
|
1337
1721
|
return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
|
|
@@ -1393,6 +1777,20 @@ function sendJson(res, value, status = 200) {
|
|
|
1393
1777
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
1394
1778
|
res.end(JSON.stringify(value));
|
|
1395
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
|
+
}
|
|
1396
1794
|
function sendImage(res, payload) {
|
|
1397
1795
|
res.writeHead(200, {
|
|
1398
1796
|
"Content-Type": payload.mimeType,
|
|
@@ -1416,7 +1814,8 @@ function sanitizeWebAttachments(value) {
|
|
|
1416
1814
|
if (!isRecord(item))
|
|
1417
1815
|
return [];
|
|
1418
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") {
|
|
1419
|
-
|
|
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 }];
|
|
1420
1819
|
}
|
|
1421
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)) {
|
|
1422
1821
|
return [{
|
|
@@ -1454,14 +1853,44 @@ function initialLines(runtime, lineId) {
|
|
|
1454
1853
|
lines.push({ id: ++lineId.current, ...line });
|
|
1455
1854
|
return lines;
|
|
1456
1855
|
}
|
|
1856
|
+
export function restoreWebHistoryLines(runtime) {
|
|
1857
|
+
return restoredHistoryLines(runtime).map((line) => ({ ...line }));
|
|
1858
|
+
}
|
|
1457
1859
|
function restoredHistoryLines(runtime) {
|
|
1458
1860
|
const lines = [];
|
|
1861
|
+
const historyToolLineIndexes = new Map();
|
|
1459
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
|
+
}
|
|
1460
1869
|
lines.push(line);
|
|
1870
|
+
if (line.toolUseId && !line.titleStatus)
|
|
1871
|
+
historyToolLineIndexes.set(line.toolUseId, lines.length - 1);
|
|
1461
1872
|
return lines.length;
|
|
1462
1873
|
};
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
}
|
|
1465
1894
|
return lines;
|
|
1466
1895
|
}
|
|
1467
1896
|
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
@@ -1471,7 +1900,18 @@ async function resetStatus(runtime) {
|
|
|
1471
1900
|
return initialStatus(runtime, await runtime.engine.contextMetrics());
|
|
1472
1901
|
}
|
|
1473
1902
|
function buildWebPromptPayload(displayText, attachments) {
|
|
1474
|
-
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;
|
|
1475
1915
|
const fileManifest = buildWebFileAttachmentManifest(attachments.filter((attachment) => attachment.kind === "file"));
|
|
1476
1916
|
if (activeAttachments.length === 0) {
|
|
1477
1917
|
const text = [displayText.trim(), fileManifest].filter(Boolean).join("\n\n");
|
|
@@ -1486,7 +1926,7 @@ function buildWebPromptPayload(displayText, attachments) {
|
|
|
1486
1926
|
break;
|
|
1487
1927
|
}
|
|
1488
1928
|
pushTextBlock(blocks, displayText.slice(cursor, next.index));
|
|
1489
|
-
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 });
|
|
1490
1930
|
cursor = next.index + next.attachment.label.length;
|
|
1491
1931
|
}
|
|
1492
1932
|
pushTextBlock(blocks, fileManifest ? `\n\n${fileManifest}` : "");
|
|
@@ -1534,6 +1974,8 @@ function pushTextBlock(blocks, text) {
|
|
|
1534
1974
|
function reduceStatus(status, event) {
|
|
1535
1975
|
if (event.type === "state")
|
|
1536
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 };
|
|
1537
1979
|
if (event.type === "context.metrics")
|
|
1538
1980
|
return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
|
|
1539
1981
|
if (event.type === "usage")
|
|
@@ -1862,6 +2304,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1862
2304
|
if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
|
|
1863
2305
|
return true;
|
|
1864
2306
|
const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
|
|
2307
|
+
const parentToolUseId = message.blocks.find((block) => block.type === "tool_result")?.toolUseId;
|
|
1865
2308
|
let rendered = false;
|
|
1866
2309
|
for (const [blockIndex, block] of message.blocks.entries()) {
|
|
1867
2310
|
if (block.type === "text") {
|
|
@@ -1878,7 +2321,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1878
2321
|
const line = imageLineForBlock(message.role, block, message.id, blockIndex);
|
|
1879
2322
|
if (!line)
|
|
1880
2323
|
continue;
|
|
1881
|
-
append({ ...line, messageId: message.id, parentToolName });
|
|
2324
|
+
append({ ...line, messageId: message.id, parentToolName, parentToolUseId });
|
|
1882
2325
|
rendered = true;
|
|
1883
2326
|
}
|
|
1884
2327
|
else if (block.type === "thinking") {
|
|
@@ -1888,22 +2331,22 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
|
1888
2331
|
rendered = true;
|
|
1889
2332
|
}
|
|
1890
2333
|
else if (block.type === "tool_use" && options.includeToolUseBlocks) {
|
|
1891
|
-
append({ ...formatToolUse(block), messageId: message.id, live: false });
|
|
2334
|
+
append({ ...formatToolUse(block), messageId: message.id, toolUseId: block.id, live: false });
|
|
1892
2335
|
rendered = true;
|
|
1893
2336
|
}
|
|
1894
2337
|
else if (block.type === "tool_result") {
|
|
1895
|
-
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 });
|
|
1896
2339
|
rendered = true;
|
|
1897
2340
|
}
|
|
1898
2341
|
}
|
|
1899
2342
|
return rendered;
|
|
1900
2343
|
}
|
|
1901
|
-
function renderToolResultMessage(message, append) {
|
|
2344
|
+
function renderToolResultMessage(message, append, presentToolResult) {
|
|
1902
2345
|
let rendered = false;
|
|
1903
2346
|
for (const block of message.blocks) {
|
|
1904
2347
|
if (block.type !== "tool_result")
|
|
1905
2348
|
continue;
|
|
1906
|
-
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);
|
|
1907
2350
|
rendered = true;
|
|
1908
2351
|
}
|
|
1909
2352
|
return rendered;
|
|
@@ -1927,13 +2370,18 @@ function imageLineForBlock(role, block, messageId, blockIndex) {
|
|
|
1927
2370
|
const kind = kindForRole(role);
|
|
1928
2371
|
if (kind === "meta")
|
|
1929
2372
|
return undefined;
|
|
2373
|
+
const resolution = resolveImageBlockDataResultSync(block);
|
|
1930
2374
|
return {
|
|
1931
2375
|
kind,
|
|
1932
2376
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1933
2377
|
image: {
|
|
1934
|
-
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,
|
|
1935
2380
|
label: block.label,
|
|
1936
2381
|
mimeType: block.mimeType,
|
|
2382
|
+
available: resolution.available,
|
|
2383
|
+
error: resolution.available ? undefined : resolution.error,
|
|
2384
|
+
blockIndex,
|
|
1937
2385
|
},
|
|
1938
2386
|
};
|
|
1939
2387
|
}
|
|
@@ -1985,20 +2433,187 @@ function titleForKind(kind) {
|
|
|
1985
2433
|
function systemLine(text, summaryMaxLines) {
|
|
1986
2434
|
return { kind: "system", title: "System", text, previewStyle: "summary", summaryMaxLines };
|
|
1987
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
|
+
}
|
|
1988
2489
|
function thinkingLine(text, live = false) {
|
|
1989
2490
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1990
2491
|
}
|
|
1991
2492
|
function formatToolUse(toolUse) {
|
|
2493
|
+
const presentationLevel = toolPresentationLevel(toolUse.name, toolUse.input);
|
|
1992
2494
|
if (toolUse.name === "plan" && isWebPlanPayload(toolUse.input))
|
|
1993
|
-
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 };
|
|
1994
2496
|
if (toolUse.name === "image2")
|
|
1995
|
-
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 };
|
|
1996
2498
|
const summary = summarizeToolUse(toolUse.name, toolUse.input);
|
|
1997
|
-
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;
|
|
1998
2575
|
}
|
|
1999
|
-
function
|
|
2000
|
-
|
|
2001
|
-
|
|
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);
|
|
2002
2617
|
}
|
|
2003
2618
|
function toolTitle(toolName, _phase) {
|
|
2004
2619
|
const labels = {
|
|
@@ -2009,13 +2624,30 @@ function toolTitle(toolName, _phase) {
|
|
|
2009
2624
|
expose_downloads: "文件下载",
|
|
2010
2625
|
grep: "搜索文本",
|
|
2011
2626
|
image2: "图片生成",
|
|
2012
|
-
image_note: "
|
|
2627
|
+
image_note: "记录图片",
|
|
2013
2628
|
list: "列出文件",
|
|
2014
2629
|
load_image: "读取图片",
|
|
2015
2630
|
plan: "任务计划",
|
|
2016
2631
|
read: "读取文件",
|
|
2017
2632
|
search: "网络搜索",
|
|
2018
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: "子任务报告",
|
|
2019
2651
|
};
|
|
2020
2652
|
return labels[toolName] ?? toolName;
|
|
2021
2653
|
}
|
|
@@ -2046,6 +2678,253 @@ function toolUsePurpose(toolName, input) {
|
|
|
2046
2678
|
return `修改 ${path.basename(input.path)}`;
|
|
2047
2679
|
return undefined;
|
|
2048
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
|
+
}
|
|
2049
2928
|
function formatToolResult(toolName, output, ok) {
|
|
2050
2929
|
if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output))
|
|
2051
2930
|
return { text: formatEditToolDiff(output, ok), format: "diff", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
|
|
@@ -2061,36 +2940,15 @@ function formatToolResult(toolName, output, ok) {
|
|
|
2061
2940
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2062
2941
|
if (toolName === "image2" && isRecord(output))
|
|
2063
2942
|
return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
|
|
2064
|
-
|
|
2065
|
-
|
|
2943
|
+
const resources = toolResources(output);
|
|
2944
|
+
if (resources?.length)
|
|
2945
|
+
return { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" };
|
|
2066
2946
|
if (toolName === "plan" && isWebPlanPayload(output))
|
|
2067
2947
|
return { text: serializeWebPlanPayload(output), full: true, bodyTitle: webPlanBodyTitle(output) };
|
|
2068
2948
|
if (typeof output === "string")
|
|
2069
2949
|
return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2070
2950
|
return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2071
2951
|
}
|
|
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
2952
|
function isEditToolOutput(value) {
|
|
2095
2953
|
return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
|
|
2096
2954
|
}
|
|
@@ -2395,8 +3253,9 @@ function formatUsageTotals(totals) {
|
|
|
2395
3253
|
}
|
|
2396
3254
|
function formatManualCompaction(result) {
|
|
2397
3255
|
if (!result.changed)
|
|
2398
|
-
return "No context
|
|
2399
|
-
|
|
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`;
|
|
2400
3259
|
}
|
|
2401
3260
|
function formatPureCompaction(result) {
|
|
2402
3261
|
if (!result.changed)
|
|
@@ -2406,6 +3265,8 @@ function formatPureCompaction(result) {
|
|
|
2406
3265
|
function formatNumber(value) {
|
|
2407
3266
|
return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2408
3267
|
}
|
|
3268
|
+
const TERMINAL_OUTPUT_FLUSH_MS = 100;
|
|
3269
|
+
const TERMINAL_OUTPUT_EVENT_MAX_CHARS = 40_000;
|
|
2409
3270
|
const THINKING_SUMMARY_MAX_LINES = 1000;
|
|
2410
3271
|
const EXPANDED_SUMMARY_MAX_LINES = 1000;
|
|
2411
3272
|
const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
|