neoctl 0.2.16 → 0.2.22

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 (74) hide show
  1. package/README.md +11 -9
  2. package/dist/app/app-prompt.d.ts +2 -0
  3. package/dist/app/app-prompt.js +2 -0
  4. package/dist/app/app-prompt.js.map +1 -1
  5. package/dist/context/compaction.d.ts +12 -0
  6. package/dist/context/compaction.js +22 -0
  7. package/dist/context/compaction.js.map +1 -1
  8. package/dist/context/prompts.js +1 -5
  9. package/dist/context/prompts.js.map +1 -1
  10. package/dist/context/smoke-context.js +30 -3
  11. package/dist/context/smoke-context.js.map +1 -1
  12. package/dist/core/query-engine.d.ts +4 -2
  13. package/dist/core/query-engine.js +22 -11
  14. package/dist/core/query-engine.js.map +1 -1
  15. package/dist/core/query.d.ts +1 -0
  16. package/dist/core/query.js +11 -124
  17. package/dist/core/query.js.map +1 -1
  18. package/dist/core/smoke-core-loop.js +9 -1
  19. package/dist/core/smoke-core-loop.js.map +1 -1
  20. package/dist/model/anthropic-mapper.js +1 -3
  21. package/dist/model/anthropic-mapper.js.map +1 -1
  22. package/dist/model/config.js +1 -1
  23. package/dist/model/env.js +1 -1
  24. package/dist/model/model-gateway.d.ts +1 -0
  25. package/dist/model/model-gateway.js.map +1 -1
  26. package/dist/model/model-metadata.json +70 -6
  27. package/dist/model/openai-chat-mapper.js +1 -0
  28. package/dist/model/openai-chat-mapper.js.map +1 -1
  29. package/dist/model/openai-mappers.js +1 -3
  30. package/dist/model/openai-mappers.js.map +1 -1
  31. package/dist/model/openai-responses-mapper.js +1 -0
  32. package/dist/model/openai-responses-mapper.js.map +1 -1
  33. package/dist/model/smoke-responses-mapper.js +5 -3
  34. package/dist/model/smoke-responses-mapper.js.map +1 -1
  35. package/dist/repl/commands.js +1 -1
  36. package/dist/repl/index.js +7 -40
  37. package/dist/repl/index.js.map +1 -1
  38. package/dist/session/session-store.d.ts +10 -0
  39. package/dist/session/session-store.js +22 -1
  40. package/dist/session/session-store.js.map +1 -1
  41. package/dist/session/smoke-session.js +3 -0
  42. package/dist/session/smoke-session.js.map +1 -1
  43. package/dist/tools/builtins/ripgrep-binary.js +1 -1
  44. package/dist/tools/builtins/ripgrep-binary.js.map +1 -1
  45. package/dist/web/index.d.ts +21 -16
  46. package/dist/web/index.js +141 -197
  47. package/dist/web/index.js.map +1 -1
  48. package/package.json +5 -3
  49. package/scripts/install-ripgrep.cjs +210 -196
  50. package/scripts/verify-ripgrep.cjs +83 -0
  51. package/vendor/ripgrep/darwin-arm64/COPYING +3 -3
  52. package/vendor/ripgrep/darwin-arm64/LICENSE-MIT +21 -21
  53. package/vendor/ripgrep/darwin-arm64/UNLICENSE +24 -24
  54. package/vendor/ripgrep/darwin-arm64/manifest.json +7 -7
  55. package/vendor/ripgrep/darwin-arm64/rg +0 -0
  56. package/vendor/ripgrep/darwin-x64/COPYING +3 -3
  57. package/vendor/ripgrep/darwin-x64/LICENSE-MIT +21 -21
  58. package/vendor/ripgrep/darwin-x64/UNLICENSE +24 -24
  59. package/vendor/ripgrep/darwin-x64/manifest.json +7 -7
  60. package/vendor/ripgrep/darwin-x64/rg +0 -0
  61. package/vendor/ripgrep/linux-arm64/COPYING +3 -3
  62. package/vendor/ripgrep/linux-arm64/LICENSE-MIT +21 -21
  63. package/vendor/ripgrep/linux-arm64/UNLICENSE +24 -24
  64. package/vendor/ripgrep/linux-arm64/manifest.json +7 -7
  65. package/vendor/ripgrep/linux-arm64/rg +0 -0
  66. package/vendor/ripgrep/linux-x64/COPYING +3 -3
  67. package/vendor/ripgrep/linux-x64/LICENSE-MIT +21 -21
  68. package/vendor/ripgrep/linux-x64/UNLICENSE +24 -24
  69. package/vendor/ripgrep/linux-x64/manifest.json +7 -7
  70. package/vendor/ripgrep/linux-x64/rg +0 -0
  71. package/vendor/ripgrep/win32-arm64/COPYING +3 -3
  72. package/vendor/ripgrep/win32-arm64/LICENSE-MIT +21 -21
  73. package/vendor/ripgrep/win32-arm64/UNLICENSE +24 -24
  74. 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: {
@@ -351,6 +253,7 @@ export class WebRepl {
351
253
  assistantLineId;
352
254
  thinkingLineId;
353
255
  finalizedThinkingLineId;
256
+ liveToolLineIds = new Map();
354
257
  activeAbortController;
355
258
  interruptArmed = false;
356
259
  lines;
@@ -398,6 +301,7 @@ export class WebRepl {
398
301
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
399
302
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
400
303
  session: this.runtime.engine.snapshot().session,
304
+ fastMode: this.runtime.engine.isFastMode(),
401
305
  appPrompt: this.runtime.engine.getAppPrompt(),
402
306
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
403
307
  interactive: includeCatalog ? webInteractiveCatalog(this.runtime) : undefined,
@@ -491,6 +395,7 @@ export class WebRepl {
491
395
  this.assistantLineId = undefined;
492
396
  this.thinkingLineId = undefined;
493
397
  this.finalizedThinkingLineId = undefined;
398
+ this.liveToolLineIds.clear();
494
399
  if (line)
495
400
  this.append(line);
496
401
  this.broadcastSync();
@@ -523,8 +428,18 @@ export class WebRepl {
523
428
  content: payload.content ?? "",
524
429
  id: payload.id,
525
430
  title: payload.title,
431
+ usage: payload.usage,
526
432
  source: payload.source,
527
433
  });
434
+ const usage = payload.usage?.trim();
435
+ if (!shouldClear && usage) {
436
+ this.append({
437
+ kind: "meta",
438
+ title: "提示词用法",
439
+ text: usage,
440
+ format: "markdown",
441
+ });
442
+ }
528
443
  this.broadcastSync();
529
444
  return { ok: true, appPrompt };
530
445
  }
@@ -533,6 +448,16 @@ export class WebRepl {
533
448
  return { ok: false, error: message };
534
449
  }
535
450
  }
451
+ async setFastMode(enabled) {
452
+ try {
453
+ const fastMode = await this.runtime.engine.setFastMode(enabled);
454
+ this.broadcastSync();
455
+ return { ok: true, fastMode };
456
+ }
457
+ catch (error) {
458
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
459
+ }
460
+ }
536
461
  loginForm(providerValue) {
537
462
  const provider = parseLoginProvider(providerValue);
538
463
  return createLoginFormPayload(this.runtime.envPath, provider);
@@ -603,7 +528,7 @@ export class WebRepl {
603
528
  this.broadcastSync();
604
529
  }
605
530
  setStatus(next) {
606
- this.status = next.phase === "running_tools" ? next : { ...next, currentTool: undefined };
531
+ this.status = next;
607
532
  this.broadcastSync();
608
533
  }
609
534
  finalizeForegroundView() {
@@ -625,8 +550,9 @@ export class WebRepl {
625
550
  this.queuedInput = undefined;
626
551
  this.queuedAttachments = undefined;
627
552
  this.finalizeForegroundView();
553
+ this.finalizeLiveToolLines();
628
554
  this.busy = false;
629
- this.status = { ...this.status, phase: "ready", detail: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
555
+ this.status = { ...this.status, phase: "ready", detail: undefined, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined };
630
556
  this.broadcastSync();
631
557
  return runWasActive;
632
558
  }
@@ -704,6 +630,11 @@ export class WebRepl {
704
630
  this.finalizedThinkingLineId = id;
705
631
  this.thinkingLineId = undefined;
706
632
  }
633
+ finalizeLiveToolLines() {
634
+ for (const id of this.liveToolLineIds.values())
635
+ this.finalizeLiveLine(id);
636
+ this.liveToolLineIds.clear();
637
+ }
707
638
  handleEvent(event) {
708
639
  this.reduce(event);
709
640
  if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
@@ -757,7 +688,14 @@ export class WebRepl {
757
688
  return;
758
689
  }
759
690
  if (event.message.role === "tool_result") {
760
- renderToolResultMessage(event.message, (line) => this.append(line));
691
+ renderToolResultMessage(event.message, (line, toolUseId) => {
692
+ const liveLineId = this.liveToolLineIds.get(toolUseId);
693
+ if (liveLineId === undefined)
694
+ return this.append(line);
695
+ this.replaceLine(liveLineId, line);
696
+ this.liveToolLineIds.delete(toolUseId);
697
+ return liveLineId;
698
+ });
761
699
  renderMessageImages(event.message, (line) => this.append(line));
762
700
  return;
763
701
  }
@@ -778,16 +716,22 @@ export class WebRepl {
778
716
  if (event.type === "tool.started") {
779
717
  this.finalizeLiveLine(this.assistantLineId);
780
718
  this.finalizeThinkingLine();
781
- this.broadcastSync();
719
+ const id = this.append({ ...formatToolUse(event.toolUse), live: true });
720
+ this.liveToolLineIds.set(event.toolUse.id, id);
782
721
  return;
783
722
  }
784
723
  if (event.type === "tool.finished") {
785
- this.broadcastSync();
724
+ const id = this.liveToolLineIds.get(event.toolUse.id);
725
+ if (id !== undefined) {
726
+ this.finalizeLiveLine(id);
727
+ this.liveToolLineIds.delete(event.toolUse.id);
728
+ }
786
729
  return;
787
730
  }
788
731
  if (event.type === "terminal") {
789
732
  this.finalizeLiveLine(this.assistantLineId);
790
733
  this.finalizeThinkingLine();
734
+ this.finalizeLiveToolLines();
791
735
  this.assistantLineId = undefined;
792
736
  this.broadcastSync();
793
737
  return;
@@ -999,14 +943,10 @@ export class WebRepl {
999
943
  res.write(`event: ${event}\n`);
1000
944
  res.write(`data: ${JSON.stringify(data)}\n\n`);
1001
945
  }
1002
- sendSerialized(res, event, data) {
1003
- res.write(`event: ${event}\n`);
1004
- res.write(`data: ${data}\n\n`);
1005
- }
1006
946
  broadcastSync() {
1007
- const payload = JSON.stringify(this.snapshot(false));
947
+ const payload = this.snapshot(false);
1008
948
  for (const res of this.subscribers)
1009
- this.sendSerialized(res, "sync", payload);
949
+ this.send(res, "sync", payload);
1010
950
  }
1011
951
  }
1012
952
  function reqKeepAlive(res) {
@@ -1025,8 +965,6 @@ async function route(req, res, router) {
1025
965
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
1026
966
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
1027
967
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1028
- if (req.method === "GET" && url.pathname === "/api/images")
1029
- return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
1030
968
  const scope = webRuntimeScopeFromUrl(url);
1031
969
  const repl = await router.get(scope);
1032
970
  if (req.method === "GET" && url.pathname === "/events")
@@ -1039,6 +977,10 @@ async function route(req, res, router) {
1039
977
  const body = await readJsonBody(req);
1040
978
  return sendJson(res, repl.setAppPrompt(body));
1041
979
  }
980
+ if (req.method === "POST" && url.pathname === "/api/fast-mode") {
981
+ const body = await readJsonBody(req);
982
+ return sendJson(res, await repl.setFastMode(body.enabled === true));
983
+ }
1042
984
  if (req.method === "POST" && url.pathname === "/api/submit") {
1043
985
  const body = await readJsonBody(req);
1044
986
  return sendJson(res, await repl.submit(String(body.text ?? ""), sanitizeWebAttachments(body.attachments)));
@@ -1090,23 +1032,6 @@ async function sendFile(res, filepath, contentType) {
1090
1032
  res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
1091
1033
  res.end(body);
1092
1034
  }
1093
- async function sendImage(res, encodedPath, mimeType) {
1094
- const filepath = decodeImagePath(encodedPath);
1095
- if (!filepath)
1096
- return sendJson(res, { error: "missing image path" }, 400);
1097
- const contentType = safeImageContentType(mimeType);
1098
- const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
1099
- if (existsSync(binaryPath)) {
1100
- res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1101
- createReadStream(binaryPath).pipe(res);
1102
- return;
1103
- }
1104
- if (!existsSync(filepath))
1105
- return sendJson(res, { error: "image not found" }, 404);
1106
- const base64 = (await fs.readFile(filepath, "utf8")).trim();
1107
- res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1108
- res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
1109
- }
1110
1035
  function sendJson(res, value, status = 200) {
1111
1036
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1112
1037
  res.end(JSON.stringify(value));
@@ -1209,11 +1134,7 @@ function pushTextBlock(blocks, text) {
1209
1134
  }
1210
1135
  function reduceStatus(status, event) {
1211
1136
  if (event.type === "state")
1212
- 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 };
1213
- if (event.type === "tool.started")
1214
- 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 };
1215
- if (event.type === "tool.finished")
1216
- return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1137
+ 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 };
1217
1138
  if (event.type === "context.metrics")
1218
1139
  return { ...status, metrics: event.metrics, inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt, activityTick: status.activityTick + 1 };
1219
1140
  if (event.type === "usage")
@@ -1226,27 +1147,16 @@ function reduceStatus(status, event) {
1226
1147
  return { ...status, phase: "calling_model", streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.argumentsDelta), outputTokenUpdatedAt: Date.now(), activityTick: status.activityTick + 1 };
1227
1148
  if (event.type === "retrying")
1228
1149
  return { ...status, phase: "calling_model", detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`, retryCooldownUntil: Date.now() + event.delayMs, activityTick: status.activityTick + 1 };
1150
+ if (event.type === "tool.started")
1151
+ return { ...status, phase: "running_tools", currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1152
+ if (event.type === "tool.finished")
1153
+ return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1229
1154
  if (event.type === "terminal")
1230
1155
  return { ...status, phase: "stopped", detail: event.reason, currentTool: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined, activityTick: status.activityTick + 1 };
1231
1156
  if (event.type === "message" || event.type === "error")
1232
1157
  return { ...status, activityTick: status.activityTick + 1 };
1233
1158
  return status;
1234
1159
  }
1235
- function toolKindForToolUse(toolName, input) {
1236
- if (toolName === "image2")
1237
- return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
1238
- if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
1239
- return "编辑";
1240
- if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
1241
- return "执行";
1242
- if (toolName.includes("download"))
1243
- return "下载";
1244
- if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
1245
- return "查询";
1246
- if (toolName === "plan")
1247
- return "计划";
1248
- return "工具";
1249
- }
1250
1160
  async function handleExportCommand(outputPath, runtime) {
1251
1161
  const snapshot = runtime.engine.snapshot();
1252
1162
  if (!snapshot.session)
@@ -1365,7 +1275,7 @@ const LOGIN_FIELD_DEFINITIONS = {
1365
1275
  openai: [
1366
1276
  { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
1367
1277
  { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
1368
- { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
1278
+ { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.6" },
1369
1279
  { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
1370
1280
  { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
1371
1281
  ...SHARED_LOGIN_FIELDS,
@@ -1420,7 +1330,7 @@ function defaultBaseUrlForLoginProvider(provider) {
1420
1330
  function defaultModelForLoginProvider(provider) {
1421
1331
  if (provider === "anthropic")
1422
1332
  return "claude-sonnet-4-6";
1423
- return "gpt-5.5";
1333
+ return "gpt-5.6";
1424
1334
  }
1425
1335
  function validateLoginFormPayload(payload) {
1426
1336
  for (const field of LOGIN_FIELD_DEFINITIONS[payload.provider]) {
@@ -1572,6 +1482,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1572
1482
  return false;
1573
1483
  if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text"))
1574
1484
  return true;
1485
+ const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
1575
1486
  let rendered = false;
1576
1487
  for (const block of message.blocks) {
1577
1488
  if (block.type === "text") {
@@ -1588,7 +1499,7 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
1588
1499
  const line = imageLineForBlock(message.role, block);
1589
1500
  if (!line)
1590
1501
  continue;
1591
- append(line);
1502
+ append({ ...line, parentToolName });
1592
1503
  rendered = true;
1593
1504
  }
1594
1505
  else if (block.type === "thinking") {
@@ -1613,20 +1524,21 @@ function renderToolResultMessage(message, append) {
1613
1524
  for (const block of message.blocks) {
1614
1525
  if (block.type !== "tool_result")
1615
1526
  continue;
1616
- append(formatToolResultLine(block.name, block.output, block.ok));
1527
+ append(formatToolResultLine(block.name, block.output, block.ok), block.toolUseId);
1617
1528
  rendered = true;
1618
1529
  }
1619
1530
  return rendered;
1620
1531
  }
1621
1532
  function renderMessageImages(message, append) {
1622
1533
  let rendered = false;
1534
+ const parentToolName = typeof message.metadata?.tool === "string" ? message.metadata.tool : undefined;
1623
1535
  for (const block of message.blocks) {
1624
1536
  if (block.type !== "image")
1625
1537
  continue;
1626
1538
  const line = imageLineForBlock(message.role, block);
1627
1539
  if (!line)
1628
1540
  continue;
1629
- append(line);
1541
+ append({ ...line, parentToolName });
1630
1542
  rendered = true;
1631
1543
  }
1632
1544
  return rendered;
@@ -1647,43 +1559,20 @@ function imageLineForBlock(role, block) {
1647
1559
  kind,
1648
1560
  text: block.label ?? `[image ${block.mimeType}]`,
1649
1561
  image: {
1650
- src: imageBlockToSrc(block),
1562
+ src: imageBlockToDataUrl(block),
1651
1563
  label: block.label,
1652
1564
  mimeType: block.mimeType,
1653
1565
  },
1654
1566
  };
1655
1567
  }
1656
- function imageBlockToSrc(block) {
1657
- if (block.storage?.path) {
1658
- return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
1659
- }
1660
- const data = block.data.trim();
1568
+ function imageBlockToDataUrl(block) {
1569
+ const data = resolveImageBlockDataSync(block);
1661
1570
  if (!data)
1662
1571
  return "";
1663
1572
  if (data.startsWith("data:"))
1664
1573
  return data;
1665
1574
  return `data:${block.mimeType};base64,${data}`;
1666
1575
  }
1667
- function encodeImagePath(filepath) {
1668
- return Buffer.from(filepath, "utf8").toString("base64url");
1669
- }
1670
- function decodeImagePath(value) {
1671
- if (!value)
1672
- return undefined;
1673
- try {
1674
- return Buffer.from(value, "base64url").toString("utf8");
1675
- }
1676
- catch {
1677
- return undefined;
1678
- }
1679
- }
1680
- function safeImageContentType(value) {
1681
- return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
1682
- }
1683
- function stripDataUrlPrefix(value) {
1684
- const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
1685
- return match ? match[1] : value;
1686
- }
1687
1576
  function assistantText(message) {
1688
1577
  const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1689
1578
  return text.length > 0 ? text : undefined;
@@ -1736,30 +1625,61 @@ function thinkingLine(text, live = false) {
1736
1625
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1737
1626
  }
1738
1627
  function formatToolUse(toolUse) {
1739
- const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
1740
1628
  if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1741
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), toolKind, text: formatPlanToolPayload(toolUse.input), collapsible: true };
1742
- const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
1743
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, toolKind, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1629
+ return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
1630
+ if (toolUse.name === "image2")
1631
+ return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: "图片模型处理中", text: JSON.stringify(toolUse.input ?? {}, null, 2), format: "plain", previewStyle: "summary", collapsible: true };
1632
+ const summary = summarizeToolUse(toolUse.name, toolUse.input);
1633
+ return { kind: "tool", toolName: toolUse.name, title: toolTitle(toolUse.name, "running"), bodyTitle: summary.bodyTitle, text: summary.text, previewStyle: "summary", collapsible: true };
1744
1634
  }
1745
1635
  function formatToolResultLine(toolName, output, ok) {
1746
1636
  const formatted = formatToolResult(toolName, output, ok);
1747
- 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) };
1748
- }
1749
- function xhsArtifactFromToolOutput(toolName, output) {
1750
- if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
1751
- return undefined;
1752
- const artifact = output.artifact;
1753
- return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
1637
+ return { kind: ok ? "tool" : "error", toolName, 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 };
1754
1638
  }
1755
1639
  function toolTitle(toolName, _phase) {
1756
- return toolName;
1640
+ const labels = {
1641
+ agent: "子任务",
1642
+ edit: "编辑文件",
1643
+ exec: "执行命令",
1644
+ expose_downloads: "文件下载",
1645
+ grep: "搜索文本",
1646
+ image2: "图片生成",
1647
+ image_note: "记录图片说明",
1648
+ list: "列出文件",
1649
+ load_image: "读取图片",
1650
+ plan: "任务计划",
1651
+ read: "读取文件",
1652
+ search: "网络搜索",
1653
+ write: "写入文件",
1654
+ };
1655
+ return labels[toolName] ?? toolName;
1656
+ }
1657
+ function summarizeToolUse(toolName, input) {
1658
+ const purpose = toolUsePurpose(toolName, input);
1659
+ return {
1660
+ bodyTitle: purpose,
1661
+ text: purpose ? `目的:${purpose}` : "正在执行工具…",
1662
+ };
1757
1663
  }
1758
- function execDescriptionFromInput(input) {
1664
+ function toolUsePurpose(toolName, input) {
1759
1665
  if (!isRecord(input))
1760
1666
  return undefined;
1761
1667
  const description = typeof input.description === "string" ? input.description.trim() : "";
1762
- return description || undefined;
1668
+ if (description)
1669
+ return description;
1670
+ if (toolName === "expose_downloads")
1671
+ return "准备网页下载链接";
1672
+ if (toolName === "read" && typeof input.path === "string")
1673
+ return `读取 ${path.basename(input.path)}`;
1674
+ if (toolName === "list" && typeof input.path === "string")
1675
+ return `查看 ${input.path}`;
1676
+ if (toolName === "grep" && typeof input.query === "string")
1677
+ return `搜索 ${input.query}`;
1678
+ if (toolName === "write" && typeof input.path === "string")
1679
+ return `写入 ${path.basename(input.path)}`;
1680
+ if (toolName === "edit" && typeof input.path === "string")
1681
+ return `修改 ${path.basename(input.path)}`;
1682
+ return undefined;
1763
1683
  }
1764
1684
  function isPlanToolPayload(value) {
1765
1685
  if (!isRecord(value) || !Array.isArray(value.items))
@@ -1794,12 +1714,36 @@ function formatToolResult(toolName, output, ok) {
1794
1714
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1795
1715
  if (toolName === "image2" && isRecord(output))
1796
1716
  return { text: formatImageGenerationToolResult(output, ok), format: "plain", summaryMaxLines: 4 };
1717
+ if (toolName === "expose_downloads")
1718
+ return { text: formatExposeDownloadsToolResult(output, ok), full: true, bodyTitle: ok ? "请点击下载" : "下载准备失败" };
1797
1719
  if (toolName === "plan" && isPlanToolPayload(output))
1798
1720
  return { text: formatPlanToolPayload(output), full: true, bodyTitle: planToolBodyTitle(output) };
1799
1721
  if (typeof output === "string")
1800
1722
  return { text: output, format: hasAnsi(output) ? "ansi" : undefined, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1801
1723
  return { text: `${ok ? "ok" : "failed"}\n${formatReplData(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
1802
1724
  }
1725
+ function formatExposeDownloadsToolResult(output, ok) {
1726
+ const downloads = extractDownloadEntries(output);
1727
+ if (!ok)
1728
+ return downloads.length ? `下载准备失败\n${formatDownloadEntries(downloads)}` : "下载准备失败";
1729
+ if (!downloads.length)
1730
+ return "文件已准备好,请点击下载。";
1731
+ return `文件已准备好,请点击下载。\n${formatDownloadEntries(downloads)}`;
1732
+ }
1733
+ function formatDownloadEntries(downloads) {
1734
+ return downloads.map((entry) => {
1735
+ const filename = entry.filename || entry.name || "下载文件";
1736
+ const url = entry.url || (entry.id ? `/api/downloads/${encodeURIComponent(entry.id)}` : "");
1737
+ return url ? `- ${filename}: ${url}` : `- ${filename}`;
1738
+ }).join("\n");
1739
+ }
1740
+ function extractDownloadEntries(output) {
1741
+ if (!isRecord(output))
1742
+ return [];
1743
+ const candidates = [output.downloads, isRecord(output.output) ? output.output.downloads : undefined, isRecord(output.result) ? output.result.downloads : undefined];
1744
+ const downloads = candidates.find(Array.isArray);
1745
+ return Array.isArray(downloads) ? downloads.filter(isRecord).map((entry) => ({ ...entry })) : [];
1746
+ }
1803
1747
  function isEditToolOutput(value) {
1804
1748
  return typeof value.path === "string" && typeof value.operation === "string" && typeof value.replacements === "number" && Array.isArray(value.patch) && value.patch.every(isEditPatchHunk);
1805
1749
  }
@@ -2122,4 +2066,4 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
2122
2066
  process.exitCode = 1;
2123
2067
  });
2124
2068
  }
2125
- //# sourceMappingURL=index.js.map
2069
+ //# sourceMappingURL=index.js.map