openfox 2.0.0-beta.8 → 2.0.0

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 (35) hide show
  1. package/README.md +11 -0
  2. package/dist/{chat-handler-XIAY7O5X.js → chat-handler-LJMW4OK3.js} +9 -9
  3. package/dist/{chunk-QN5ST54E.js → chunk-32GQUDLN.js} +118 -97
  4. package/dist/{chunk-IGSZUXQP.js → chunk-7ZMMDZU7.js} +435 -33
  5. package/dist/{chunk-CDDMHSZF.js → chunk-CK6LGE7G.js} +10 -6
  6. package/dist/{chunk-7FDY3EOH.js → chunk-FFEAEPJB.js} +80 -226
  7. package/dist/{chunk-2ZS2B5HW.js → chunk-HPCGVAS4.js} +21 -45
  8. package/dist/{chunk-XJEOP6XU.js → chunk-ITWVFGFV.js} +1 -30
  9. package/dist/{chunk-DFITQ75T.js → chunk-LEDG5WAN.js} +29 -35
  10. package/dist/{chunk-GJ47CMZM.js → chunk-MFUPS6UE.js} +6 -6
  11. package/dist/{chunk-NPHYEUYE.js → chunk-XEK3KII6.js} +55 -4
  12. package/dist/{chunk-GJ34YIOL.js → chunk-ZDY4WMZJ.js} +185 -71
  13. package/dist/cli/dev.js +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/{config-QUT7YZQ7.js → config-BU66P4KX.js} +2 -7
  16. package/dist/{events-PQ3KTI5H.js → events-ZKWAKWI7.js} +3 -3
  17. package/dist/{folding-CXORTBAU.js → folding-YOCGTZYH.js} +2 -2
  18. package/dist/{orchestrator-GAOENX5Q.js → orchestrator-LX6FKB6L.js} +8 -8
  19. package/dist/package.json +2 -1
  20. package/dist/{processor-6VIYGURD.js → processor-NMYSEBC7.js} +11 -6
  21. package/dist/{protocol-CDOV1pyc.d.ts → protocol-D59sCy9L.d.ts} +15 -6
  22. package/dist/{provider-4T6BVPMD.js → provider-DTNQYCMV.js} +9 -20
  23. package/dist/provider-manager-LMHAHLIF.js +15 -0
  24. package/dist/{serve-2V7T6GDG.js → serve-SGNL43UL.js} +14 -15
  25. package/dist/server/index.d.ts +22 -16
  26. package/dist/server/index.js +9 -10
  27. package/dist/shared/index.d.ts +2 -2
  28. package/dist/{tools-SDXWIYSA.js → tools-BOE5T3KC.js} +7 -7
  29. package/dist/web/assets/index-Bego8SwT.js +299 -0
  30. package/dist/web/assets/{index-CSOB8dwI.css → index-Sdax8ayU.css} +1 -1
  31. package/dist/web/index.html +2 -2
  32. package/dist/web/sw.js +1 -1
  33. package/package.json +2 -1
  34. package/dist/chunk-EMJGF3A7.js +0 -466
  35. package/dist/web/assets/index-BJuF4wi7.js +0 -299
package/README.md CHANGED
@@ -22,6 +22,17 @@ openfox
22
22
 
23
23
  On first run, OpenFox automatically detects your local LLM backend (vLLM, sglang, ollama, llamacpp) and configures itself.
24
24
 
25
+ ## What's New in 2.0
26
+
27
+ - **Multi-Turn Agent Engine** — Completely rewritten agent loop with EventStore as single source of truth. All modes (builder, planner, verifier, sub-agents, compaction) run through the same unified loop.
28
+ - **Provider Dialog** — Comprehensive provider configuration UI with thinking mode (`reasoningEffort`), editable kwargs, profile defaults, and preset management.
29
+ - **Auto-Retry Patterns** — Replace the old XML protection toggle with configurable pattern matching. Define your own retry triggers in settings.
30
+ - **Unified Image Handling** — Automatic vision model fallback for non-vision models. Images are described before each turn so any model can "see" them.
31
+ - **Session Metadata** — Unified `session_metadata` tool replaces separate criterion/todo tools. Interactive criteria editor with CRUD and agent badges.
32
+ - **Workflow Sub-Groups** — Run individual workflow steps in isolation. New code review phase in the build-verify pipeline.
33
+ - **Parallel `edit_file` Safety** — Per-file mutex prevents race conditions when editing multiple files simultaneously.
34
+ - **Firefox Support** — Custom thin scrollbars with hover behavior, cross-browser compatible.
35
+
25
36
  ## CLI Commands
26
37
 
27
38
  ```bash
@@ -5,18 +5,19 @@ import {
5
5
  generateSessionName,
6
6
  getSessionMessageCount,
7
7
  needsNameGenerationCheck
8
- } from "./chunk-CDDMHSZF.js";
8
+ } from "./chunk-CK6LGE7G.js";
9
9
  import {
10
10
  runChatTurn
11
- } from "./chunk-DFITQ75T.js";
12
- import "./chunk-QN5ST54E.js";
11
+ } from "./chunk-LEDG5WAN.js";
12
+ import "./chunk-32GQUDLN.js";
13
13
  import "./chunk-DL6ZILAF.js";
14
14
  import "./chunk-PBGOZMVY.js";
15
15
  import "./chunk-VRGRAQDG.js";
16
+ import "./chunk-XAMAYRDA.js";
16
17
  import {
17
18
  getEventStore
18
- } from "./chunk-NPHYEUYE.js";
19
- import "./chunk-XJEOP6XU.js";
19
+ } from "./chunk-XEK3KII6.js";
20
+ import "./chunk-ITWVFGFV.js";
20
21
  import {
21
22
  createChatMessageMessage,
22
23
  createPhaseChangedMessage,
@@ -24,14 +25,13 @@ import {
24
25
  } from "./chunk-7TTEGAO6.js";
25
26
  import "./chunk-BJYPTN5S.js";
26
27
  import "./chunk-RFNEDBVO.js";
27
- import "./chunk-XAMAYRDA.js";
28
28
  import "./chunk-FBGWG4N6.js";
29
29
  import "./chunk-BVHFMAVN.js";
30
- import "./chunk-7FDY3EOH.js";
30
+ import "./chunk-CQGTEGKL.js";
31
+ import "./chunk-FFEAEPJB.js";
31
32
  import {
32
33
  logger
33
34
  } from "./chunk-K44MW7JJ.js";
34
- import "./chunk-CQGTEGKL.js";
35
35
 
36
36
  // src/server/session/chat-handler.ts
37
37
  var activeAgents = /* @__PURE__ */ new Map();
@@ -161,4 +161,4 @@ export {
161
161
  startChatSession,
162
162
  stopSessionExecution
163
163
  };
164
- //# sourceMappingURL=chat-handler-XIAY7O5X.js.map
164
+ //# sourceMappingURL=chat-handler-LJMW4OK3.js.map
@@ -14,12 +14,15 @@ import {
14
14
  getMaxPerSession,
15
15
  getSessionProcessCount
16
16
  } from "./chunk-VRGRAQDG.js";
17
+ import {
18
+ getProject
19
+ } from "./chunk-XAMAYRDA.js";
17
20
  import {
18
21
  getCurrentContextWindowId,
19
22
  getCurrentWindowMessageOptions,
20
23
  getEventStore,
21
24
  getRuntimeConfig
22
- } from "./chunk-NPHYEUYE.js";
25
+ } from "./chunk-XEK3KII6.js";
23
26
  import {
24
27
  buildContextMessagesFromEventHistory,
25
28
  foldContextState,
@@ -28,7 +31,7 @@ import {
28
31
  handleToolCall,
29
32
  handleToolResult,
30
33
  stripOrphanedToolCalls
31
- } from "./chunk-XJEOP6XU.js";
34
+ } from "./chunk-ITWVFGFV.js";
32
35
  import {
33
36
  createChatDoneMessage,
34
37
  createChatMessageMessage,
@@ -46,21 +49,18 @@ import {
46
49
  getSetting
47
50
  } from "./chunk-RFNEDBVO.js";
48
51
  import {
49
- getProject
50
- } from "./chunk-XAMAYRDA.js";
52
+ getGlobalConfigDir
53
+ } from "./chunk-CQGTEGKL.js";
51
54
  import {
52
55
  buildModelParams,
53
56
  getBackendCapabilities,
54
57
  getModelProfile,
55
58
  modelSupportsVision,
56
59
  streamWithSegments
57
- } from "./chunk-7FDY3EOH.js";
60
+ } from "./chunk-FFEAEPJB.js";
58
61
  import {
59
62
  logger
60
63
  } from "./chunk-K44MW7JJ.js";
61
- import {
62
- getGlobalConfigDir
63
- } from "./chunk-CQGTEGKL.js";
64
64
 
65
65
  // src/server/tools/read.ts
66
66
  import { readFile as readFile2, stat, readdir } from "fs/promises";
@@ -954,6 +954,21 @@ function buildRegion(edits, contextStart, contextEnd, lines) {
954
954
  }
955
955
 
956
956
  // src/server/tools/edit.ts
957
+ var fileLocks = /* @__PURE__ */ new Map();
958
+ async function withFileLock(filePath, fn) {
959
+ const prev = fileLocks.get(filePath) ?? Promise.resolve();
960
+ const next = prev.then(fn, fn);
961
+ fileLocks.set(
962
+ filePath,
963
+ next.then(
964
+ () => {
965
+ },
966
+ () => {
967
+ }
968
+ )
969
+ );
970
+ return next;
971
+ }
957
972
  function detectLineEnding(content) {
958
973
  if (content.includes("\r\n")) return "crlf";
959
974
  if (content.includes("\n")) return "lf";
@@ -995,95 +1010,97 @@ var editFileTool = createTool(
995
1010
  }
996
1011
  },
997
1012
  async (args, context, helpers) => {
998
- const replaceAll = args.replace_all ?? false;
999
1013
  const fullPath = helpers.resolvePath(args.path);
1000
1014
  await helpers.checkPathAccess([fullPath]);
1001
- const readFiles = context.sessionManager.getReadFiles(context.sessionId);
1002
- const validation = await validateFileForWrite(fullPath, readFiles);
1003
- if (!validation.valid) {
1004
- return helpers.error(validation.error?.message ?? "File validation failed");
1005
- }
1006
- let content;
1007
- try {
1008
- content = await readFile3(fullPath, "utf-8");
1009
- } catch {
1010
- return helpers.error(`File not found: ${args.path}`);
1011
- }
1012
- const fileLineEnding = detectLineEnding(content);
1013
- const normalizedContent = normalizeToLF(content);
1014
- const normalizedOldString = normalizeToLF(args.old_string);
1015
- const occurrences = normalizedContent.split(normalizedOldString).length - 1;
1016
- if (occurrences === 0) {
1017
- const preview = args.old_string.length > 100 ? args.old_string.slice(0, 100) + "..." : args.old_string;
1018
- return helpers.error(
1019
- `old_string not found in file.
1015
+ return withFileLock(fullPath, async () => {
1016
+ const replaceAll = args.replace_all ?? false;
1017
+ const readFiles = context.sessionManager.getReadFiles(context.sessionId);
1018
+ const validation = await validateFileForWrite(fullPath, readFiles);
1019
+ if (!validation.valid) {
1020
+ return helpers.error(validation.error?.message ?? "File validation failed");
1021
+ }
1022
+ let content;
1023
+ try {
1024
+ content = await readFile3(fullPath, "utf-8");
1025
+ } catch {
1026
+ return helpers.error(`File not found: ${args.path}`);
1027
+ }
1028
+ const fileLineEnding = detectLineEnding(content);
1029
+ const normalizedContent = normalizeToLF(content);
1030
+ const normalizedOldString = normalizeToLF(args.old_string);
1031
+ const occurrences = normalizedContent.split(normalizedOldString).length - 1;
1032
+ if (occurrences === 0) {
1033
+ const preview = args.old_string.length > 100 ? args.old_string.slice(0, 100) + "..." : args.old_string;
1034
+ return helpers.error(
1035
+ `old_string not found in file.
1020
1036
 
1021
1037
  Searched for:
1022
1038
  ${preview}
1023
1039
 
1024
1040
  Make sure whitespace and indentation match exactly.`
1041
+ );
1042
+ }
1043
+ if (occurrences > 1 && !replaceAll) {
1044
+ return helpers.error(
1045
+ `Found ${occurrences} matches for old_string. Use replace_all: true to replace all, or provide more context to make the match unique.`
1046
+ );
1047
+ }
1048
+ const contextResult = extractEditContext(
1049
+ normalizedContent,
1050
+ normalizedOldString,
1051
+ normalizeToLF(args.new_string),
1052
+ replaceAll
1025
1053
  );
1026
- }
1027
- if (occurrences > 1 && !replaceAll) {
1028
- return helpers.error(
1029
- `Found ${occurrences} matches for old_string. Use replace_all: true to replace all, or provide more context to make the match unique.`
1054
+ const editContextRegions = contextResult.regions.map((region) => ({
1055
+ beforeContext: region.beforeContext.map((line) => ({
1056
+ lineNumber: line.lineNumber,
1057
+ content: line.content
1058
+ })),
1059
+ afterContext: region.afterContext.map((line) => ({
1060
+ lineNumber: line.lineNumber,
1061
+ content: line.content
1062
+ })),
1063
+ startLine: region.startLine,
1064
+ endLine: region.endLine,
1065
+ oldContent: region.oldContent,
1066
+ newContent: region.newContent,
1067
+ edits: region.edits.map((edit) => ({
1068
+ startLine: edit.startLine,
1069
+ endLine: edit.endLine,
1070
+ oldContent: edit.oldContent,
1071
+ newContent: edit.newContent
1072
+ }))
1073
+ }));
1074
+ const normalizedNewString = normalizeToLF(args.new_string);
1075
+ let replacedContent;
1076
+ if (replaceAll) {
1077
+ replacedContent = normalizedContent.replaceAll(normalizedOldString, normalizedNewString);
1078
+ } else {
1079
+ const index = normalizedContent.indexOf(normalizedOldString);
1080
+ if (index === -1) {
1081
+ return helpers.error("old_string not found in file (unexpected)");
1082
+ }
1083
+ replacedContent = normalizedContent.slice(0, index) + normalizedNewString + normalizedContent.slice(index + normalizedOldString.length);
1084
+ }
1085
+ const newContent = replacedContent.replace(
1086
+ /\n/g,
1087
+ fileLineEnding === "crlf" ? "\r\n" : fileLineEnding === "cr" ? "\r" : "\n"
1030
1088
  );
1031
- }
1032
- const contextResult = extractEditContext(
1033
- normalizedContent,
1034
- normalizedOldString,
1035
- normalizeToLF(args.new_string),
1036
- replaceAll
1037
- );
1038
- const editContextRegions = contextResult.regions.map((region) => ({
1039
- beforeContext: region.beforeContext.map((line) => ({
1040
- lineNumber: line.lineNumber,
1041
- content: line.content
1042
- })),
1043
- afterContext: region.afterContext.map((line) => ({
1044
- lineNumber: line.lineNumber,
1045
- content: line.content
1046
- })),
1047
- startLine: region.startLine,
1048
- endLine: region.endLine,
1049
- oldContent: region.oldContent,
1050
- newContent: region.newContent,
1051
- edits: region.edits.map((edit) => ({
1052
- startLine: edit.startLine,
1053
- endLine: edit.endLine,
1054
- oldContent: edit.oldContent,
1055
- newContent: edit.newContent
1056
- }))
1057
- }));
1058
- const normalizedNewString = normalizeToLF(args.new_string);
1059
- let replacedContent;
1060
- if (replaceAll) {
1061
- replacedContent = normalizedContent.replaceAll(normalizedOldString, normalizedNewString);
1062
- } else {
1063
- const index = normalizedContent.indexOf(normalizedOldString);
1064
- if (index === -1) {
1065
- return helpers.error("old_string not found in file (unexpected)");
1089
+ await writeFile2(fullPath, newContent, "utf-8");
1090
+ let output = `Successfully replaced ${replaceAll ? occurrences : 1} occurrence(s) in ${args.path}`;
1091
+ let diagnostics = [];
1092
+ if (context.lspManager) {
1093
+ diagnostics = await context.lspManager.notifyFileChange(fullPath, newContent);
1094
+ output += formatDiagnosticsForLLM(diagnostics);
1066
1095
  }
1067
- replacedContent = normalizedContent.slice(0, index) + normalizedNewString + normalizedContent.slice(index + normalizedOldString.length);
1068
- }
1069
- const newContent = replacedContent.replace(
1070
- /\n/g,
1071
- fileLineEnding === "crlf" ? "\r\n" : fileLineEnding === "cr" ? "\r" : "\n"
1072
- );
1073
- await writeFile2(fullPath, newContent, "utf-8");
1074
- let output = `Successfully replaced ${replaceAll ? occurrences : 1} occurrence(s) in ${args.path}`;
1075
- let diagnostics = [];
1076
- if (context.lspManager) {
1077
- diagnostics = await context.lspManager.notifyFileChange(fullPath, newContent);
1078
- output += formatDiagnosticsForLLM(diagnostics);
1079
- }
1080
- const newHash = await computeFileHash(fullPath);
1081
- if (newHash) {
1082
- context.sessionManager.updateFileHash(context.sessionId, fullPath, newHash);
1083
- }
1084
- return helpers.success(output, false, {
1085
- ...diagnostics.length > 0 && { diagnostics },
1086
- ...editContextRegions.length > 0 && { editContext: { regions: editContextRegions } }
1096
+ const newHash = await computeFileHash(fullPath);
1097
+ if (newHash) {
1098
+ context.sessionManager.updateFileHash(context.sessionId, fullPath, newHash);
1099
+ }
1100
+ return helpers.success(output, false, {
1101
+ ...diagnostics.length > 0 && { diagnostics },
1102
+ ...editContextRegions.length > 0 && { editContext: { regions: editContextRegions } }
1103
+ });
1087
1104
  });
1088
1105
  }
1089
1106
  );
@@ -1938,6 +1955,11 @@ function buildAgentReminder(agentDef) {
1938
1955
  ${agentDef.prompt}${toolPermissions}
1939
1956
  </system-reminder>`;
1940
1957
  }
1958
+ function buildAgentSmallReminder(name) {
1959
+ return `<system-reminder>
1960
+ Reminder: you are in '${name}' mode.
1961
+ </system-reminder>`;
1962
+ }
1941
1963
  var WORKFLOW_KICKOFF_PROMPT = (criteriaCount) => `Implement the task and make sure you fulfil the ${criteriaCount} criteria.`;
1942
1964
  var COMPACTION_PROMPT = `You are a helpful AI assistant tasked with summarizing conversations for continuation.
1943
1965
 
@@ -1979,12 +2001,12 @@ function matchRetryPatterns(content, thinking, patterns) {
1979
2001
 
1980
2002
  // src/server/chat/stream-utils.ts
1981
2003
  function buildStreamRequestObject(params) {
1982
- const { messages, tools, toolChoice, disableThinking, signal, modelSettings } = params;
2004
+ const { messages, tools, toolChoice, reasoningEffort, signal, modelSettings } = params;
1983
2005
  return {
1984
2006
  messages,
1985
2007
  ...tools && { tools },
1986
2008
  ...toolChoice && { toolChoice },
1987
- disableThinking: disableThinking ?? false,
2009
+ ...reasoningEffort && { reasoningEffort },
1988
2010
  ...signal && { signal },
1989
2011
  ...modelSettings && { modelSettings }
1990
2012
  };
@@ -2038,7 +2060,7 @@ function createEmptyStreamResult(aborted, modelParams, patternMatch) {
2038
2060
  };
2039
2061
  }
2040
2062
  async function* streamLLMPure(options) {
2041
- const { messageId, systemPrompt, llmClient, messages, tools, toolChoice, signal, disableThinking, retryPatterns } = options;
2063
+ const { messageId, systemPrompt, llmClient, messages, tools, toolChoice, signal, reasoningEffort, retryPatterns } = options;
2042
2064
  const llmMessages = [{ role: "system", content: systemPrompt }, ...messages];
2043
2065
  const profile = getModelProfile(llmClient.getModel());
2044
2066
  const backend = getBackendCapabilities(llmClient.getBackend());
@@ -2071,7 +2093,7 @@ async function* streamLLMPure(options) {
2071
2093
  messages: llmMessages,
2072
2094
  tools,
2073
2095
  toolChoice,
2074
- disableThinking: disableThinking ?? false,
2096
+ reasoningEffort,
2075
2097
  signal: combinedSignal,
2076
2098
  modelSettings: options.modelSettings
2077
2099
  });
@@ -3083,7 +3105,7 @@ ${COMPACTION_PROMPT}`,
3083
3105
  });
3084
3106
  append(createMessageDoneEvent(assistantMsgId, { stats: turnMetrics.buildStats(statsIdentity, mode) }));
3085
3107
  append(createChatDoneEvent(assistantMsgId, "complete"));
3086
- config.injectModeReminder?.();
3108
+ config.injectAgentReminder?.();
3087
3109
  compacting = false;
3088
3110
  continue;
3089
3111
  }
@@ -3167,7 +3189,6 @@ function buildAssemblyInput(systemPrompt, baseInput) {
3167
3189
  injectedFiles: baseInput.injectedFiles,
3168
3190
  requestTools: baseInput.requestTools ?? baseInput.promptTools,
3169
3191
  toolChoice: baseInput.toolChoice ?? "auto",
3170
- disableThinking: baseInput.disableThinking ?? false,
3171
3192
  ...baseInput.customInstructions ? { customInstructions: baseInput.customInstructions } : {},
3172
3193
  ...baseInput.skills && baseInput.skills.length > 0 ? { skills: baseInput.skills } : {}
3173
3194
  });
@@ -3259,7 +3280,7 @@ async function describeImageFromDataUrl(dataUrl, visionModel, options) {
3259
3280
  import { createHash as createHash2 } from "crypto";
3260
3281
  async function loadVisionModelFromGlobalConfig() {
3261
3282
  try {
3262
- const { loadGlobalConfig, getVisionFallback } = await import("./config-QUT7YZQ7.js");
3283
+ const { loadGlobalConfig, getVisionFallback } = await import("./config-BU66P4KX.js");
3263
3284
  const runtimeConfig = getRuntimeConfig();
3264
3285
  const mode = runtimeConfig.mode ?? "production";
3265
3286
  const globalConfig = await loadGlobalConfig(mode);
@@ -3622,7 +3643,6 @@ async function executeSubAgent(options) {
3622
3643
  injectedFiles: input.injectedFiles,
3623
3644
  requestTools: input.promptTools,
3624
3645
  toolChoice: input.toolChoice,
3625
- disableThinking: true,
3626
3646
  ...instructionContent ? { customInstructions: instructionContent } : {},
3627
3647
  ...skills.length > 0 ? { skills } : {}
3628
3648
  }),
@@ -3713,7 +3733,7 @@ var callSubAgentTool = {
3713
3733
  };
3714
3734
  }
3715
3735
  try {
3716
- const { getToolRegistryForAgent: getToolRegistryForAgent2 } = await import("./tools-SDXWIYSA.js");
3736
+ const { getToolRegistryForAgent: getToolRegistryForAgent2 } = await import("./tools-BOE5T3KC.js");
3717
3737
  const toolRegistry = getToolRegistryForAgent2(agentDef);
3718
3738
  const turnMetrics = new TurnMetrics();
3719
3739
  const result = await executeSubAgent({
@@ -4721,6 +4741,7 @@ export {
4721
4741
  buildBasePrompt,
4722
4742
  buildTopLevelSystemPrompt,
4723
4743
  buildAgentReminder,
4744
+ buildAgentSmallReminder,
4724
4745
  WORKFLOW_KICKOFF_PROMPT,
4725
4746
  COMPACTION_PROMPT,
4726
4747
  streamLLMPure,
@@ -4764,4 +4785,4 @@ export {
4764
4785
  getToolRegistryForAgent,
4765
4786
  createToolRegistry
4766
4787
  };
4767
- //# sourceMappingURL=chunk-QN5ST54E.js.map
4788
+ //# sourceMappingURL=chunk-32GQUDLN.js.map