neoctl 0.2.15 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/context/prompts.js +1 -5
  2. package/dist/context/prompts.js.map +1 -1
  3. package/dist/core/query-engine.d.ts +4 -2
  4. package/dist/core/query-engine.js +22 -11
  5. package/dist/core/query-engine.js.map +1 -1
  6. package/dist/core/query.d.ts +1 -0
  7. package/dist/core/query.js +3 -120
  8. package/dist/core/query.js.map +1 -1
  9. package/dist/core/smoke-core-loop.js +9 -1
  10. package/dist/core/smoke-core-loop.js.map +1 -1
  11. package/dist/model/anthropic-mapper.js +1 -3
  12. package/dist/model/anthropic-mapper.js.map +1 -1
  13. package/dist/model/model-gateway.d.ts +1 -0
  14. package/dist/model/model-gateway.js.map +1 -1
  15. package/dist/model/openai-chat-mapper.js +1 -0
  16. package/dist/model/openai-chat-mapper.js.map +1 -1
  17. package/dist/model/openai-mappers.js +1 -3
  18. package/dist/model/openai-mappers.js.map +1 -1
  19. package/dist/model/openai-responses-mapper.js +1 -0
  20. package/dist/model/openai-responses-mapper.js.map +1 -1
  21. package/dist/model/smoke-responses-mapper.js +5 -3
  22. package/dist/model/smoke-responses-mapper.js.map +1 -1
  23. package/dist/repl/index.js +5 -38
  24. package/dist/repl/index.js.map +1 -1
  25. package/dist/session/session-store.d.ts +10 -0
  26. package/dist/session/session-store.js +22 -1
  27. package/dist/session/session-store.js.map +1 -1
  28. package/dist/session/smoke-session.js +3 -0
  29. package/dist/session/smoke-session.js.map +1 -1
  30. package/dist/web/index.d.ts +10 -16
  31. package/dist/web/index.js +139 -193
  32. package/dist/web/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/scripts/install-ripgrep.cjs +196 -196
  35. package/vendor/ripgrep/darwin-arm64/COPYING +3 -3
  36. package/vendor/ripgrep/darwin-arm64/LICENSE-MIT +21 -21
  37. package/vendor/ripgrep/darwin-arm64/UNLICENSE +24 -24
  38. package/vendor/ripgrep/darwin-arm64/manifest.json +7 -7
  39. package/vendor/ripgrep/darwin-x64/COPYING +3 -3
  40. package/vendor/ripgrep/darwin-x64/LICENSE-MIT +21 -21
  41. package/vendor/ripgrep/darwin-x64/UNLICENSE +24 -24
  42. package/vendor/ripgrep/darwin-x64/manifest.json +7 -7
  43. package/vendor/ripgrep/linux-arm64/COPYING +3 -3
  44. package/vendor/ripgrep/linux-arm64/LICENSE-MIT +21 -21
  45. package/vendor/ripgrep/linux-arm64/UNLICENSE +24 -24
  46. package/vendor/ripgrep/linux-arm64/manifest.json +7 -7
  47. package/vendor/ripgrep/linux-x64/COPYING +3 -3
  48. package/vendor/ripgrep/linux-x64/LICENSE-MIT +21 -21
  49. package/vendor/ripgrep/linux-x64/UNLICENSE +24 -24
  50. package/vendor/ripgrep/linux-x64/manifest.json +7 -7
  51. package/vendor/ripgrep/win32-arm64/COPYING +3 -3
  52. package/vendor/ripgrep/win32-arm64/LICENSE-MIT +21 -21
  53. package/vendor/ripgrep/win32-arm64/UNLICENSE +24 -24
  54. package/vendor/ripgrep/win32-arm64/manifest.json +7 -7
package/dist/web/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import http from "node:http";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { createReadStream, existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createRequire } from "node:module";
8
8
  import { QueryEngine } from "../core/query-engine.js";
@@ -22,20 +22,14 @@ import { searchTool } from "../tools/builtins/search-tool.js";
22
22
  import { planTool } from "../tools/builtins/plan-tool.js";
23
23
  import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
24
24
  import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
25
- import { createImageNoteTool } from "../tools/builtins/image-note-tool.js";
26
25
  import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
27
26
  import { createTaskTools } from "../tasks/task-tools.js";
28
27
  import { TaskStore } from "../tasks/task-store.js";
29
28
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
30
29
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
- import { DefaultContextManager } from "../context/context-manager.js";
32
- import { buildEffectiveSystemPrompt } from "../context/prompts.js";
33
30
  import { WEB_HTML } from "./html.js";
34
31
  import { openDirectory } from "../open-directory.js";
35
- import { getNeoctlHome } from "../paths.js";
36
- import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
37
- import { createSkillTool } from "../skills/skill-tool.js";
38
- import { createSkillManagementTools } from "../skills/skill-management-tools.js";
32
+ import { resolveImageBlockDataSync } from "../core/image-storage.js";
39
33
  const require = createRequire(import.meta.url);
40
34
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
41
35
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
@@ -114,98 +108,13 @@ function parseWebArgs(argv) {
114
108
  port = 3000;
115
109
  return { host, port: Math.round(port) };
116
110
  }
117
- class SkillCatalogContextManager {
118
- catalog;
119
- base;
120
- constructor(catalog, base = new DefaultContextManager()) {
121
- this.catalog = catalog;
122
- this.base = base;
123
- }
124
- async build(input) {
125
- const runtimeContext = await this.base.build(input);
126
- const skillSection = await buildSkillCatalogPromptSection(this.catalog);
127
- if (!skillSection)
128
- return runtimeContext;
129
- const promptSections = [...runtimeContext.promptSections, skillSection];
130
- return {
131
- ...runtimeContext,
132
- promptSections,
133
- systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
134
- };
135
- }
136
- }
137
- async function buildSkillCatalogPromptSection(catalog) {
138
- const skills = await catalog.list();
139
- if (skills.length === 0)
140
- return undefined;
141
- const visible = skills.slice(0, 80);
142
- const lines = visible.map((skill) => {
143
- const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
144
- const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
145
- return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
146
- });
147
- if (skills.length > visible.length)
148
- lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
149
- return {
150
- name: "Available Skills",
151
- cacheStable: false,
152
- content: [
153
- "Reusable skills are available through the `skill` tool and the /skill UI command.",
154
- "When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
155
- "Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
156
- "Available skill catalog:",
157
- ...lines,
158
- ].join("\n"),
159
- };
160
- }
161
- function resolveSkillCatalogRoots(cwd, extraRoots = [], createRoot) {
162
- const userRoot = path.resolve(process.env.NEO_SKILL_CREATE_ROOT || createRoot || path.join(getNeoctlHome(), "skills"));
163
- const configuredRoots = splitPathList(process.env.NEO_SKILL_ROOTS);
164
- const workspaceRoot = path.resolve(cwd, ".neo", "skills");
165
- const roots = uniquePaths([
166
- userRoot,
167
- ...extraRoots,
168
- ...configuredRoots,
169
- workspaceRoot,
170
- ]).map((root) => ({
171
- root,
172
- kind: path.resolve(root) === userRoot ? "user" : "workspace",
173
- }));
174
- return { roots, createRoot: userRoot };
175
- }
176
- function splitPathList(value) {
177
- return String(value || "")
178
- .split(path.delimiter)
179
- .map((item) => item.trim())
180
- .filter(Boolean)
181
- .map((item) => path.resolve(item));
182
- }
183
- function uniquePaths(values) {
184
- const seen = new Set();
185
- const result = [];
186
- for (const value of values) {
187
- const resolved = path.resolve(value);
188
- const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
189
- if (seen.has(key))
190
- continue;
191
- seen.add(key);
192
- result.push(resolved);
193
- }
194
- return result;
195
- }
196
111
  export async function createWebRuntime(options = {}) {
197
112
  const envLoad = loadDefaultDotEnvFiles({ override: true });
198
- const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
199
113
  const modelConfig = readModelProviderConfig(process.env);
200
114
  const communicationLogger = new CommunicationLogger();
201
115
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
202
116
  const taskStore = new TaskStore();
203
117
  const tools = new ToolRegistry();
204
- const { roots: skillRoots, createRoot: skillCreateRoot } = resolveSkillCatalogRoots(cwd, options.skillRoots, options.skillCreateRoot);
205
- const skills = new FileSystemSkillCatalog({
206
- roots: skillRoots,
207
- createRoot: skillCreateRoot,
208
- });
209
118
  tools.register(editTool);
210
119
  tools.register(writeTool);
211
120
  tools.register(createExecTool({ taskStore }));
@@ -214,13 +123,9 @@ export async function createWebRuntime(options = {}) {
214
123
  tools.register(grepTool);
215
124
  tools.register(searchTool);
216
125
  tools.register(createLoadImageTool());
217
- tools.register(createImageNoteTool());
218
126
  if (modelConfig?.provider === "openai")
219
127
  tools.register(createOpenAIImageGenerationTool());
220
128
  tools.register(planTool);
221
- tools.register(createSkillTool(skills));
222
- for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
223
- tools.register(tool);
224
129
  for (const tool of options.externalTools ?? [])
225
130
  tools.register(tool);
226
131
  const agentRuntime = { modelGateway, tools, taskStore };
@@ -229,7 +134,7 @@ export async function createWebRuntime(options = {}) {
229
134
  const dummyContext = {
230
135
  agentId: "main",
231
136
  tools,
232
- appState: new InMemoryAppState("main", cwd),
137
+ appState: new InMemoryAppState("main"),
233
138
  emit: () => undefined,
234
139
  };
235
140
  return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
@@ -239,7 +144,6 @@ export async function createWebRuntime(options = {}) {
239
144
  const appPromptStore = new InMemoryAppPromptStore();
240
145
  const engine = new QueryEngine({
241
146
  agentId: options.agentId ?? "main",
242
- cwd,
243
147
  model: modelConfig?.model,
244
148
  fallbackModel: modelConfig?.fallbackModel,
245
149
  reasoning: modelConfig?.defaultReasoning,
@@ -247,8 +151,6 @@ export async function createWebRuntime(options = {}) {
247
151
  modelGateway,
248
152
  tools,
249
153
  appPromptStore,
250
- contextManager: new SkillCatalogContextManager(skills),
251
- skills: (await skills.list()).map((skill) => skill.name),
252
154
  taskNotificationSource: createTaskNotificationSource(taskStore),
253
155
  commands: replCommandDefinitions.map((command) => command.usage),
254
156
  session: {
@@ -360,6 +262,7 @@ export class WebRepl {
360
262
  queuedAttachments;
361
263
  foregroundRun;
362
264
  foregroundRunToken = 0;
265
+ pendingUserImageEchoLabels;
363
266
  backgroundSessionRuns = new Map();
364
267
  suppressReattachedStreaming = new Set();
365
268
  backgroundTaskCount;
@@ -397,6 +300,7 @@ export class WebRepl {
397
300
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
398
301
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
399
302
  session: this.runtime.engine.snapshot().session,
303
+ fastMode: this.runtime.engine.isFastMode(),
400
304
  appPrompt: this.runtime.engine.getAppPrompt(),
401
305
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
402
306
  interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
@@ -532,6 +436,16 @@ export class WebRepl {
532
436
  return { ok: false, error: message };
533
437
  }
534
438
  }
439
+ async setFastMode(enabled) {
440
+ try {
441
+ const fastMode = await this.runtime.engine.setFastMode(enabled);
442
+ this.broadcastSync();
443
+ return { ok: true, fastMode };
444
+ }
445
+ catch (error) {
446
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
447
+ }
448
+ }
535
449
  loginForm(providerValue) {
536
450
  const provider = parseLoginProvider(providerValue);
537
451
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -602,7 +516,7 @@ export class WebRepl {
602
516
  this.broadcastSync();
603
517
  }
604
518
  setStatus(next) {
605
- this.status = next.phase === "running_tools" ? next : { ...next, currentTool: undefined };
519
+ this.status = next;
606
520
  this.broadcastSync();
607
521
  }
608
522
  finalizeForegroundView() {
@@ -705,6 +619,11 @@ export class WebRepl {
705
619
  }
706
620
  handleEvent(event) {
707
621
  this.reduce(event);
622
+ if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
623
+ this.pendingUserImageEchoLabels = undefined;
624
+ this.broadcastSync();
625
+ return;
626
+ }
708
627
  if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta" || event.type === "retrying") {
709
628
  this.broadcastSync();
710
629
  return;
@@ -772,13 +691,10 @@ export class WebRepl {
772
691
  if (event.type === "tool.started") {
773
692
  this.finalizeLiveLine(this.assistantLineId);
774
693
  this.finalizeThinkingLine();
775
- this.broadcastSync();
776
694
  return;
777
695
  }
778
- if (event.type === "tool.finished") {
779
- this.broadcastSync();
696
+ if (event.type === "tool.finished")
780
697
  return;
781
- }
782
698
  if (event.type === "terminal") {
783
699
  this.finalizeLiveLine(this.assistantLineId);
784
700
  this.finalizeThinkingLine();
@@ -789,6 +705,15 @@ export class WebRepl {
789
705
  if (event.type === "error")
790
706
  this.append({ kind: "error", text: event.error.message });
791
707
  }
708
+ matchesPendingUserImageEcho(message) {
709
+ const pending = this.pendingUserImageEchoLabels;
710
+ if (!pending?.length || message.role !== "user")
711
+ return false;
712
+ const labels = message.blocks
713
+ .filter((block) => block.type === "image" && typeof block.label === "string")
714
+ .map((block) => block.label);
715
+ return labels.length === pending.length && labels.every((label, index) => label === pending[index]);
716
+ }
792
717
  async handleCommandOrPrompt(text, attachments = []) {
793
718
  const command = parseReplCommand(text);
794
719
  if (command.type === "exit") {
@@ -888,6 +813,10 @@ export class WebRepl {
888
813
  this.append({ kind: "user", text: promptPayload.displayText });
889
814
  for (const line of imageLinesForBlocks("user", promptPayload.blocks))
890
815
  this.append(line);
816
+ const optimisticImageLabels = (promptPayload.blocks ?? [])
817
+ .filter((block) => block.type === "image" && typeof block.label === "string")
818
+ .map((block) => block.label);
819
+ this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
891
820
  const runToken = ++this.foregroundRunToken;
892
821
  const abortController = new AbortController();
893
822
  this.activeAbortController = abortController;
@@ -927,6 +856,7 @@ export class WebRepl {
927
856
  this.activeAbortController = undefined;
928
857
  this.interruptArmed = false;
929
858
  this.finalizeForegroundView();
859
+ this.pendingUserImageEchoLabels = undefined;
930
860
  const queuedText = this.queuedInput;
931
861
  const queuedAttach = this.queuedAttachments;
932
862
  this.queuedInput = undefined;
@@ -979,14 +909,10 @@ export class WebRepl {
979
909
  res.write(`event: ${event}\n`);
980
910
  res.write(`data: ${JSON.stringify(data)}\n\n`);
981
911
  }
982
- sendSerialized(res, event, data) {
983
- res.write(`event: ${event}\n`);
984
- res.write(`data: ${data}\n\n`);
985
- }
986
912
  broadcastSync() {
987
- const payload = JSON.stringify(this.snapshot(false));
913
+ const payload = this.snapshot(false);
988
914
  for (const res of this.subscribers)
989
- this.sendSerialized(res, "sync", payload);
915
+ this.send(res, "sync", payload);
990
916
  }
991
917
  }
992
918
  function reqKeepAlive(res) {
@@ -1005,8 +931,6 @@ async function route(req, res, router) {
1005
931
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1006
932
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1007
933
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1008
- if (req.method === "GET" && url.pathname === "/api/images")
1009
- return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
1010
934
  const scope = webRuntimeScopeFromUrl(url);
1011
935
  const repl = await router.get(scope);
1012
936
  if (req.method === "GET" && url.pathname === "/events")
@@ -1019,6 +943,10 @@ async function route(req, res, router) {
1019
943
  const body = await readJsonBody(req);
1020
944
  return sendJson(res, repl.setAppPrompt(body));
1021
945
  }
946
+ if (req.method === "POST" && url.pathname === "/api/fast-mode") {
947
+ const body = await readJsonBody(req);
948
+ return sendJson(res, await repl.setFastMode(body.enabled === true));
949
+ }
1022
950
  if (req.method === "POST" && url.pathname === "/api/submit") {
1023
951
  const body = await readJsonBody(req);
1024
952
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
@@ -1070,23 +998,6 @@ async function sendFile(res, filepath, contentType) {
1070
998
  res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
1071
999
  res.end(body);
1072
1000
  }
1073
- async function sendImage(res, encodedPath, mimeType) {
1074
- const filepath = decodeImagePath(encodedPath);
1075
- if (!filepath)
1076
- return sendJson(res, { error: "missing image path" }, 400);
1077
- const contentType = safeImageContentType(mimeType);
1078
- const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
1079
- if (existsSync(binaryPath)) {
1080
- res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1081
- createReadStream(binaryPath).pipe(res);
1082
- return;
1083
- }
1084
- if (!existsSync(filepath))
1085
- return sendJson(res, { error: "image not found" }, 404);
1086
- const base64 = (await fs.readFile(filepath, "utf8")).trim();
1087
- res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1088
- res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
1089
- }
1090
1001
  function sendJson(res, value, status = 200) {
1091
1002
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1092
1003
  res.end(JSON.stringify(value));
@@ -1189,11 +1100,7 @@ function pushTextBlock(blocks, text) {
1189
1100
  }
1190
1101
  function reduceStatus(status, event) {
1191
1102
  if (event.type === "state")
1192
- return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "preparing" || event.phase === "calling_model" || event.phase === "ready" ? undefined : status.currentTool, 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 };
1193
- if (event.type === "tool.started")
1194
- return { ...status, phase: "running_tools", detail: event.toolUse.name, currentTool: { id: event.toolUse.id, name: event.toolUse.name, kind: toolKindForToolUse(event.toolUse.name, event.toolUse.input), startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1195
- if (event.type === "tool.finished")
1196
- return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1103
+ return { ...status, phase: event.phase, detail: event.detail, 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 };
1197
1104
  if (event.type === "context.metrics")
1198
1105
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1199
1106
  if (event.type === "usage")
@@ -1207,26 +1114,11 @@ function reduceStatus(status, event) {
1207
1114
  if (event.type === "retrying")
1208
1115
  return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
1209
1116
  if (event.type === "terminal")
1210
- return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1211
- if (event.type === "message" || event.type === "error")
1117
+ return { ...status, phase: "stopped", detail: event.reason, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1118
+ if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error")
1212
1119
  return { ...status, activityTick: status.activityTick + 1 };
1213
1120
  return status;
1214
1121
  }
1215
- function toolKindForToolUse(toolName, input) {
1216
- if (toolName === "image2")
1217
- return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
1218
- if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
1219
- return "编辑";
1220
- if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
1221
- return "执行";
1222
- if (toolName.includes("download"))
1223
- return "下载";
1224
- if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
1225
- return "查询";
1226
- if (toolName === "plan")
1227
- return "计划";
1228
- return "工具";
1229
- }
1230
1122
  async function handleExportCommand(outputPath, runtime) {
1231
1123
  const snapshot = runtime.engine.snapshot();
1232
1124
  if (!snapshot.session)
@@ -1627,43 +1519,20 @@ function imageLineForBlock(role, block) {
1627
1519
  kind,
1628
1520
  text: block.label ?? `[image ${block.mimeType}]`,
1629
1521
  image: {
1630
- src: imageBlockToSrc(block),
1522
+ src: imageBlockToDataUrl(block),
1631
1523
  label: block.label,
1632
1524
  mimeType: block.mimeType,
1633
1525
  },
1634
1526
  };
1635
1527
  }
1636
- function imageBlockToSrc(block) {
1637
- if (block.storage?.path) {
1638
- return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
1639
- }
1640
- const data = block.data.trim();
1528
+ function imageBlockToDataUrl(block) {
1529
+ const data = resolveImageBlockDataSync(block);
1641
1530
  if (!data)
1642
1531
  return "";
1643
1532
  if (data.startsWith("data:"))
1644
1533
  return data;
1645
1534
  return `data:${block.mimeType};base64,${data}`;
1646
1535
  }
1647
- function encodeImagePath(filepath) {
1648
- return Buffer.from(filepath, "utf8").toString("base64url");
1649
- }
1650
- function decodeImagePath(value) {
1651
- if (!value)
1652
- return undefined;
1653
- try {
1654
- return Buffer.from(value, "base64url").toString("utf8");
1655
- }
1656
- catch {
1657
- return undefined;
1658
- }
1659
- }
1660
- function safeImageContentType(value) {
1661
- return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
1662
- }
1663
- function stripDataUrlPrefix(value) {
1664
- const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
1665
- return match ? match[1] : value;
1666
- }
1667
1536
  function assistantText(message) {
1668
1537
  const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1669
1538
  return text.length > 0 ? text : undefined;
@@ -1716,30 +1585,59 @@ function thinkingLine(text, live = false) {
1716
1585
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1717
1586
  }
1718
1587
  function formatToolUse(toolUse) {
1719
- const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
1720
1588
  if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1721
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), toolKind, text: formatPlanToolPayload(toolUse.input), collapsible: true };
1722
- const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
1723
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, toolKind, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1589
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
1590
+ const summary = summarizeToolUse(toolUse.name, toolUse.input);
1591
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
1724
1592
  }
1725
1593
  function formatToolResultLine(toolName, output, ok) {
1726
1594
  const formatted = formatToolResult(toolName, output, ok);
1727
- return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", toolKind: toolKindForToolUse(toolName, undefined), text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true, artifact: xhsArtifactFromToolOutput(toolName, output) };
1728
- }
1729
- function xhsArtifactFromToolOutput(toolName, output) {
1730
- if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
1731
- return undefined;
1732
- const artifact = output.artifact;
1733
- return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
1595
+ return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true };
1734
1596
  }
1735
1597
  function toolTitle(toolName, _phase) {
1736
- return toolName;
1598
+ const labels = {
1599
+ agent: "子任务",
1600
+ edit: "编辑文件",
1601
+ exec: "执行命令",
1602
+ expose_downloads: "文件下载",
1603
+ grep: "搜索文本",
1604
+ image2: "图片生成",
1605
+ image_note: "记录图片说明",
1606
+ list: "列出文件",
1607
+ load_image: "读取图片",
1608
+ plan: "任务计划",
1609
+ read: "读取文件",
1610
+ search: "网络搜索",
1611
+ write: "写入文件",
1612
+ };
1613
+ return labels[toolName] ?? toolName;
1614
+ }
1615
+ function summarizeToolUse(toolName, input) {
1616
+ const purpose = toolUsePurpose(toolName, input);
1617
+ return {
1618
+ bodyTitle: purpose,
1619
+ text: purpose ? `目的:${purpose}` : "正在执行工具…",
1620
+ };
1737
1621
  }
1738
- function execDescriptionFromInput(input) {
1622
+ function toolUsePurpose(toolName, input) {
1739
1623
  if (!isRecord(input))
1740
1624
  return undefined;
1741
1625
  const description = typeof input.description === "string" ? input.description.trim() : "";
1742
- return description || undefined;
1626
+ if (description)
1627
+ return description;
1628
+ if (toolName === "expose_downloads")
1629
+ return "准备网页下载链接";
1630
+ if (toolName === "read" && typeof input.path === "string")
1631
+ return `读取 ${path.basename(input.path)}`;
1632
+ if (toolName === "list" && typeof input.path === "string")
1633
+ return `查看 ${input.path}`;
1634
+ if (toolName === "grep" && typeof input.query === "string")
1635
+ return `搜索 ${input.query}`;
1636
+ if (toolName === "write" && typeof input.path === "string")
1637
+ return `写入 ${path.basename(input.path)}`;
1638
+ if (toolName === "edit" && typeof input.path === "string")
1639
+ return `修改 ${path.basename(input.path)}`;
1640
+ return undefined;
1743
1641
  }
1744
1642
  function isPlanToolPayload(value) {
1745
1643
  if (!isRecord(value) || !Array.isArray(value.items))
@@ -1774,12 +1672,36 @@ function formatToolResult(toolName, output, ok) {
1774
1672
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1775
1673
  if (toolName === "image2" && isRecord(output))
1776
1674
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
1675
+ if (toolName === "expose_downloads")
1676
+ return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
1777
1677
  if (toolName === "plan" && isPlanToolPayload(output))
1778
1678
  return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
1779
1679
  if (typeof output === "string")
1780
1680
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1781
1681
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1782
1682
  }
1683
+ function formatExposeDownloadsToolResult(output, ok) {
1684
+ const downloads = extractDownloadEntries(output);
1685
+ if (!ok)
1686
+ return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
1687
+ if (!downloads.length)
1688
+ return "文件已准备好,请点击下载。";
1689
+ return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
1690
+ }
1691
+ function formatDownloadEntries(downloads) {
1692
+ return downloads.map((entry) => {
1693
+ const filename = entry.filename || entry.name || "下载文件";
1694
+ const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
1695
+ return url ? `- ${filename}: ${url}` : `- ${filename}`;
1696
+ }).join("\n");
1697
+ }
1698
+ function extractDownloadEntries(output) {
1699
+ if (!isRecord(output))
1700
+ return [];
1701
+ const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
1702
+ const downloads = candidates.find(Array.isArray);
1703
+ return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
1704
+ }
1783
1705
  function isEditToolOutput(value) {
1784
1706
  return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
1785
1707
  }
@@ -1857,6 +1779,17 @@ function formatImageGenerationToolResult(output, ok) {
1857
1779
  const quality = typeof output.quality === "string" ? output.quality : undefined;
1858
1780
  const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
1859
1781
  const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
1782
+ const prompt = typeof output.prompt === "string" ? output.prompt.trim() : "";
1783
+ const semanticName = typeof output.semanticName === "string" ? output.semanticName.trim() : "";
1784
+ const background = typeof output.background === "string" ? output.background : undefined;
1785
+ const startedAtIso = typeof output.startedAtIso === "string" ? output.startedAtIso : undefined;
1786
+ const finishedAtIso = typeof output.finishedAtIso === "string" ? output.finishedAtIso : undefined;
1787
+ const revisedPrompt = Array.isArray(output.images)
1788
+ ? output.images
1789
+ .filter(isRecord)
1790
+ .map((image) => typeof image.revisedPrompt === "string" ? image.revisedPrompt.trim() : "")
1791
+ .find((value) => Boolean(value))
1792
+ : undefined;
1860
1793
  const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
1861
1794
  const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
1862
1795
  if (details.length > 0)
@@ -1866,6 +1799,19 @@ function formatImageGenerationToolResult(output, ok) {
1866
1799
  const duration = imageGenerationDuration(output);
1867
1800
  if (duration !== undefined)
1868
1801
  lines.push(`duration: ${duration}ms`);
1802
+ if (prompt)
1803
+ lines.push(`prompt: ${prompt}`);
1804
+ if (revisedPrompt && revisedPrompt !== prompt)
1805
+ lines.push(`revisedPrompt: ${revisedPrompt}`);
1806
+ lines.push(`mode: ${mode}`);
1807
+ if (semanticName)
1808
+ lines.push(`semanticName: ${semanticName}`);
1809
+ if (background)
1810
+ lines.push(`background: ${background}`);
1811
+ if (startedAtIso)
1812
+ lines.push(`startedAt: ${startedAtIso}`);
1813
+ if (finishedAtIso)
1814
+ lines.push(`finishedAt: ${finishedAtIso}`);
1869
1815
  return lines.join("\n");
1870
1816
  }
1871
1817
  function imageGenerationDuration(output) {