@nomad-e/bluma-cli 0.6.8 → 0.6.9

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 (3) hide show
  1. package/README.md +0 -1
  2. package/dist/main.js +1716 -1242
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -12575,7 +12575,7 @@ var getInstance = (stdout, createInstance) => {
12575
12575
 
12576
12576
  // src/main.ts
12577
12577
  import { EventEmitter as EventEmitter7 } from "events";
12578
- import fs45 from "fs";
12578
+ import fs46 from "fs";
12579
12579
  import path49 from "path";
12580
12580
  import { fileURLToPath as fileURLToPath7 } from "url";
12581
12581
  import { spawn as spawn6 } from "child_process";
@@ -12638,9 +12638,9 @@ var BLUMA_TERMINAL = {
12638
12638
  // red-500
12639
12639
  warn: "#eab308",
12640
12640
  // yellow-500
12641
- diffAdded: "#14532d",
12641
+ diffAdded: "#203a2b",
12642
12642
  // green-900
12643
- diffRemoved: "#7f1d1d",
12643
+ diffRemoved: "#4a221d",
12644
12644
  // red-900
12645
12645
  diffAddedWord: "#22c55e",
12646
12646
  diffRemovedWord: "#ef4444",
@@ -12686,14 +12686,18 @@ var HeaderComponent = ({ workdir, cliVersion = "?" }) => /* @__PURE__ */ jsxs2(
12686
12686
  Box_default,
12687
12687
  {
12688
12688
  flexDirection: "column",
12689
- padding: 1,
12689
+ marginBottom: 1,
12690
+ width: "100%",
12690
12691
  children: [
12691
12692
  /* @__PURE__ */ jsxs2(Box_default, { flexDirection: "row", flexWrap: "wrap", children: [
12692
- /* @__PURE__ */ jsx9(Text, { color: BLUMA_TERMINAL.orange, children: "\u273B " }),
12693
+ /* @__PURE__ */ jsxs2(Text, { color: BLUMA_TERMINAL.dim, children: [
12694
+ "$_",
12695
+ " "
12696
+ ] }),
12693
12697
  /* @__PURE__ */ jsx9(Text, { color: BLUMA_TERMINAL.m3OnSurface, bold: true, children: "Blu" }),
12694
12698
  /* @__PURE__ */ jsx9(Text, { color: BLUMA_TERMINAL.m3OnSurface, bold: true, children: "Ma" })
12695
12699
  ] }),
12696
- /* @__PURE__ */ jsx9(Box_default, { flexDirection: "row", flexWrap: "wrap", marginTop: 0, children: /* @__PURE__ */ jsx9(Text, { color: BLUMA_TERMINAL.dim, children: " Base Language Unit \xB7 Model Agent" }) })
12700
+ /* @__PURE__ */ jsx9(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", children: /* @__PURE__ */ jsx9(Text, { color: BLUMA_TERMINAL.dim, children: "Base Language Unit \xB7 Model Agent" }) })
12697
12701
  ]
12698
12702
  }
12699
12703
  );
@@ -12701,7 +12705,7 @@ var Header = memo(HeaderComponent);
12701
12705
  var SessionInfoComponent = ({
12702
12706
  toolsCount,
12703
12707
  mcpStatus
12704
- }) => /* @__PURE__ */ jsxs2(Box_default, { paddingX: 1, children: [
12708
+ }) => /* @__PURE__ */ jsxs2(Box_default, { children: [
12705
12709
  /* @__PURE__ */ jsx9(Text, { dimColor: true, children: "mcp " }),
12706
12710
  /* @__PURE__ */ jsx9(Text, { color: mcpStatus === "connected" ? BLUMA_TERMINAL.success : BLUMA_TERMINAL.warn, children: mcpStatus === "connected" ? "on" : "\u2026" }),
12707
12711
  toolsCount !== null ? /* @__PURE__ */ jsxs2(Text, { dimColor: true, children: [
@@ -12721,7 +12725,7 @@ var TaskStatusBarComponent = ({
12721
12725
  EXECUTION: BLUMA_TERMINAL.magenta,
12722
12726
  VERIFICATION: BLUMA_TERMINAL.orange
12723
12727
  };
12724
- return /* @__PURE__ */ jsxs2(Box_default, { paddingX: 1, children: [
12728
+ return /* @__PURE__ */ jsxs2(Box_default, { children: [
12725
12729
  /* @__PURE__ */ jsxs2(Text, { color: modeColors[mode], bold: true, children: [
12726
12730
  "[",
12727
12731
  mode.charAt(0),
@@ -17828,9 +17832,6 @@ async function exitPlanMode(args) {
17828
17832
  // src/app/agent/tools/TaskToolsTool/TaskToolsTool.ts
17829
17833
  var bySession = /* @__PURE__ */ new Map();
17830
17834
  var currentSessionId = "";
17831
- function setTaskToolSessionContext(sessionId) {
17832
- currentSessionId = String(sessionId || "").trim() || "default";
17833
- }
17834
17835
  function sessionMap() {
17835
17836
  const sid2 = currentSessionId || "default";
17836
17837
  let m = bySession.get(sid2);
@@ -18366,6 +18367,14 @@ async function fileWrite(args) {
18366
18367
  error: `File already exists and overwrite=false: ${resolvedPath}`
18367
18368
  };
18368
18369
  }
18370
+ let previousContent = "";
18371
+ if (existed) {
18372
+ try {
18373
+ previousContent = await fs17.readFile(resolvedPath, "utf-8");
18374
+ } catch {
18375
+ previousContent = "";
18376
+ }
18377
+ }
18369
18378
  if (create_directories) {
18370
18379
  await fs17.mkdir(dir, { recursive: true });
18371
18380
  }
@@ -18374,7 +18383,9 @@ async function fileWrite(args) {
18374
18383
  return {
18375
18384
  success: true,
18376
18385
  filepath: resolvedPath,
18386
+ content: String(content),
18377
18387
  bytes_written: bytes,
18388
+ previous_content: existed ? previousContent : "",
18378
18389
  created: !existed,
18379
18390
  overwritten: existed
18380
18391
  };
@@ -24540,8 +24551,7 @@ Rules:
24540
24551
  ],
24541
24552
  temperature: 0,
24542
24553
  max_tokens: 4096,
24543
- userContext,
24544
- response_format: { type: "json_object" }
24554
+ userContext
24545
24555
  };
24546
24556
  const resp = await llmService.chatCompletion(requestBase);
24547
24557
  const text = resp.choices[0]?.message?.content;
@@ -24836,6 +24846,39 @@ function sanitizeConversationForProvider(conversationHistory) {
24836
24846
  droppingCorruptTurn = true;
24837
24847
  continue;
24838
24848
  }
24849
+ const expectedToolIds = new Set(
24850
+ toolCalls.map((call) => String(call?.id ?? "").trim()).filter(Boolean)
24851
+ );
24852
+ const turnBuffer = [conversationHistory[index]];
24853
+ const seenToolIds = /* @__PURE__ */ new Set();
24854
+ let j = index + 1;
24855
+ let turnValid = true;
24856
+ while (j < conversationHistory.length) {
24857
+ const next = conversationHistory[j];
24858
+ if (next?.role !== "tool") {
24859
+ break;
24860
+ }
24861
+ const toolCallId = String(next?.tool_call_id ?? "").trim();
24862
+ if (!toolCallId || !expectedToolIds.has(toolCallId) || seenToolIds.has(toolCallId)) {
24863
+ issues.push({
24864
+ index,
24865
+ reason: "assistant tool_calls were not followed by a valid tool turn",
24866
+ toolNames: toolCalls.map((call) => String(call?.function?.name ?? "unknown"))
24867
+ });
24868
+ droppingCorruptTurn = true;
24869
+ turnValid = false;
24870
+ break;
24871
+ }
24872
+ seenToolIds.add(toolCallId);
24873
+ turnBuffer.push(conversationHistory[j]);
24874
+ j += 1;
24875
+ }
24876
+ if (turnValid) {
24877
+ cleaned.push(...turnBuffer);
24878
+ index = j - 1;
24879
+ continue;
24880
+ }
24881
+ continue;
24839
24882
  }
24840
24883
  cleaned.push(conversationHistory[index]);
24841
24884
  }
@@ -24858,50 +24901,9 @@ function partitionConversationIntoTurnSlices(conversationHistory) {
24858
24901
  }
24859
24902
  return turns;
24860
24903
  }
24861
- var MODEL_CONTEXT_WINDOW_DEFAULT = 2e5;
24862
- var MODEL_CONTEXT_WINDOWS = {
24863
- "gpt-4o": 2e5,
24864
- "gpt-4o-2024-08-06": 128e3,
24865
- "gpt-4-turbo": 128e3,
24866
- "gpt-4-0125-preview": 128e3,
24867
- "gpt-4-1106-preview": 128e3,
24868
- "claude-3-5-sonnet": 2e5,
24869
- "claude-3-opus": 2e5,
24870
- "claude-3-sonnet": 2e5,
24871
- "claude-3-haiku": 2e5,
24872
- "claude-sonnet-4-6-1m": 1e6,
24873
- "claude-opus-4-6-1m": 1e6
24874
- };
24875
- function getContextWindowForModel(modelName = "") {
24876
- if (!modelName) {
24877
- return MODEL_CONTEXT_WINDOW_DEFAULT;
24878
- }
24879
- const normalizedModel = modelName.toLowerCase();
24880
- const envOverride = process.env.BLUMA_CONTEXT_WINDOW_OVERRIDE;
24881
- if (envOverride) {
24882
- const override = parseInt(envOverride, 10);
24883
- if (!isNaN(override) && override > 0) {
24884
- return override;
24885
- }
24886
- }
24887
- for (const [modelPattern, contextWindow] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
24888
- if (normalizedModel.includes(modelPattern)) {
24889
- return contextWindow;
24890
- }
24891
- }
24892
- if (normalizedModel.includes("[1m]") || normalizedModel.includes("1m")) {
24893
- return 1e6;
24894
- }
24895
- return MODEL_CONTEXT_WINDOW_DEFAULT;
24896
- }
24897
- function getContextInputBudgetForModel(modelName = "") {
24898
- const contextWindow = getContextWindowForModel(modelName);
24899
- const outputReserve = Math.max(8e3, Math.floor(contextWindow * 0.1));
24900
- const effectiveContextPercent = 0.95;
24901
- const inputBudget = Math.floor(
24902
- (contextWindow - outputReserve) * effectiveContextPercent
24903
- );
24904
- return Math.max(inputBudget, 32e3);
24904
+ var STATIC_CONTEXT_INPUT_BUDGET = 96e3;
24905
+ function getContextInputBudgetForModel(_modelName = "") {
24906
+ return STATIC_CONTEXT_INPUT_BUDGET;
24905
24907
  }
24906
24908
 
24907
24909
  // src/app/agent/core/llm/llm.ts
@@ -24932,6 +24934,8 @@ function extractStreamingDelta(previous, next) {
24932
24934
  }
24933
24935
 
24934
24936
  // src/app/agent/core/llm/llm.ts
24937
+ init_logger();
24938
+ var llmLog = logger.child("llm");
24935
24939
  function defaultBlumaUserContextInput(sessionId, userMessage) {
24936
24940
  const msg = String(userMessage || "").slice(0, 300);
24937
24941
  return {
@@ -25009,6 +25013,7 @@ var THINKING_TOKEN_BUDGET_BY_EFFORT = {
25009
25013
  medium: 1024,
25010
25014
  high: 2048
25011
25015
  };
25016
+ var DEFAULT_REQUEST_MAX_TOKENS = 8192;
25012
25017
  function getThinkingTokenBudgetForEffort(effort) {
25013
25018
  if (!effort) return void 0;
25014
25019
  return THINKING_TOKEN_BUDGET_BY_EFFORT[effort];
@@ -25022,6 +25027,13 @@ function buildVllmReasoningPayload(effort) {
25022
25027
  }
25023
25028
  };
25024
25029
  }
25030
+ function resolveRequestMaxTokens(params) {
25031
+ const requested = params.max_tokens;
25032
+ if (typeof requested === "number" && Number.isFinite(requested) && requested > 0) {
25033
+ return Math.floor(requested);
25034
+ }
25035
+ return DEFAULT_REQUEST_MAX_TOKENS;
25036
+ }
25025
25037
  function resolveReasoningEffortForRequest(params, runtimeConfig = getRuntimeConfig()) {
25026
25038
  const policy = getSandboxPolicy();
25027
25039
  if (policy.isSandbox) {
@@ -25029,24 +25041,188 @@ function resolveReasoningEffortForRequest(params, runtimeConfig = getRuntimeConf
25029
25041
  }
25030
25042
  return params.reasoning?.effort ?? runtimeConfig.reasoningEffort;
25031
25043
  }
25032
- function buildChatCompletionRequestBody(params, runtimeConfig = getRuntimeConfig(), stream = false) {
25044
+ function buildChatCompletionRequestBody(params, runtimeConfig = getRuntimeConfig(), stream = false, options) {
25033
25045
  const tools = params.tools;
25034
25046
  const hasTools = Array.isArray(tools) && tools.length > 0;
25035
25047
  const effort = resolveReasoningEffortForRequest(params, runtimeConfig);
25036
- const reasoningPayload = buildVllmReasoningPayload(effort);
25048
+ const reasoningPayload = options?.disableReasoning ? void 0 : buildVllmReasoningPayload(effort);
25049
+ const messages = sanitizeMessagesForRequest(params.messages, options);
25037
25050
  return {
25038
25051
  model: params.model || runtimeConfig.model || "auto",
25039
- messages: params.messages,
25040
- tools: hasTools ? tools : void 0,
25041
- tool_choice: hasTools ? "auto" : void 0,
25042
- parallel_tool_calls: params.parallel_tool_calls ?? false,
25052
+ messages,
25053
+ tools: hasTools && !options?.disableTools ? tools : void 0,
25054
+ tool_choice: hasTools && !options?.disableTools ? "auto" : void 0,
25055
+ parallel_tool_calls: options?.disableParallelToolCalls ? void 0 : params.parallel_tool_calls ?? false,
25043
25056
  temperature: params.temperature ?? 0,
25044
25057
  ...reasoningPayload ?? {},
25045
- max_tokens: params.max_tokens,
25058
+ max_tokens: resolveRequestMaxTokens(params),
25046
25059
  response_format: params.response_format,
25047
25060
  ...stream ? { stream: true } : {}
25048
25061
  };
25049
25062
  }
25063
+ function isBadRequestLike(error) {
25064
+ const anyError = error;
25065
+ if (anyError && typeof anyError.status === "number" && anyError.status === 400) {
25066
+ return true;
25067
+ }
25068
+ const message2 = String(anyError?.message ?? error ?? "").toLowerCase();
25069
+ return message2.includes("400 status code") || message2.includes("bad request");
25070
+ }
25071
+ function describeFallback(options) {
25072
+ const parts = [];
25073
+ if (options.disableReasoning) parts.push("no_reasoning");
25074
+ if (options.disableParallelToolCalls) parts.push("no_parallel");
25075
+ if (options.disableTools) parts.push("no_tools");
25076
+ if (options.stripToolTurns) parts.push("no_tool_turns");
25077
+ if (options.stripImages) parts.push("no_images");
25078
+ return parts.length > 0 ? parts.join("+") : "base";
25079
+ }
25080
+ function summarizeError(error) {
25081
+ if (!error || typeof error !== "object") {
25082
+ return { message: String(error) };
25083
+ }
25084
+ const anyError = error;
25085
+ return {
25086
+ name: typeof anyError.name === "string" ? anyError.name : void 0,
25087
+ message: typeof anyError.message === "string" ? anyError.message : String(error),
25088
+ status: typeof anyError.status === "number" ? anyError.status : void 0,
25089
+ code: typeof anyError.code === "string" || typeof anyError.code === "number" ? anyError.code : void 0,
25090
+ type: typeof anyError.type === "string" ? anyError.type : void 0
25091
+ };
25092
+ }
25093
+ function summarizeRequestForLog(params, runtimeConfig = getRuntimeConfig()) {
25094
+ const messageCounts = {
25095
+ total: params.messages.length,
25096
+ system: 0,
25097
+ user: 0,
25098
+ assistant: 0,
25099
+ tool: 0,
25100
+ function: 0
25101
+ };
25102
+ let toolCallMessages = 0;
25103
+ let imageMessages = 0;
25104
+ for (const message2 of params.messages) {
25105
+ if (message2.role in messageCounts) {
25106
+ messageCounts[message2.role] += 1;
25107
+ }
25108
+ if (message2.role === "assistant" && Array.isArray(message2.tool_calls) && message2.tool_calls.length > 0) {
25109
+ toolCallMessages += 1;
25110
+ }
25111
+ if (Array.isArray(message2.content)) {
25112
+ imageMessages += 1;
25113
+ }
25114
+ }
25115
+ const messagePreview = params.messages.slice(0, 8).map((message2) => {
25116
+ const content = message2.content;
25117
+ const toolCalls = Array.isArray(message2.tool_calls) ? message2.tool_calls : [];
25118
+ const preview = typeof content === "string" ? content.slice(0, 120) : Array.isArray(content) ? content.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => part.text).join(" ").slice(0, 120) : null;
25119
+ return {
25120
+ role: message2.role,
25121
+ name: message2.name ?? null,
25122
+ content_type: Array.isArray(content) ? "array" : typeof content,
25123
+ preview: preview || null,
25124
+ tool_calls: toolCalls.length,
25125
+ tool_call_id: message2.tool_call_id ?? null
25126
+ };
25127
+ });
25128
+ return {
25129
+ model: params.model || runtimeConfig.model || "auto",
25130
+ temperature: params.temperature ?? 0,
25131
+ max_tokens: resolveRequestMaxTokens(params),
25132
+ parallel_tool_calls: params.parallel_tool_calls ?? false,
25133
+ has_tools: Boolean(params.tools?.length),
25134
+ tool_count: params.tools?.length ?? 0,
25135
+ message_counts: messageCounts,
25136
+ assistant_tool_call_messages: toolCallMessages,
25137
+ multimodal_messages: imageMessages,
25138
+ reasoning: params.reasoning?.effort ?? runtimeConfig.reasoningEffort,
25139
+ message_preview: messagePreview
25140
+ };
25141
+ }
25142
+ function logLLMRetry(stage, params, fallbackIndex, totalFallbacks, error, stream) {
25143
+ llmLog.warn("LLM request fallback failed", {
25144
+ stage,
25145
+ fallback: `${fallbackIndex + 1}/${totalFallbacks}`,
25146
+ stream,
25147
+ request: summarizeRequestForLog(params),
25148
+ error: summarizeError(error)
25149
+ });
25150
+ }
25151
+ function getRequestFallbacks() {
25152
+ return [
25153
+ {},
25154
+ { disableReasoning: true },
25155
+ { disableReasoning: true, disableParallelToolCalls: true },
25156
+ { disableReasoning: true, disableParallelToolCalls: true, disableTools: true },
25157
+ {
25158
+ disableReasoning: true,
25159
+ disableParallelToolCalls: true,
25160
+ disableTools: true,
25161
+ stripToolTurns: true,
25162
+ stripImages: true
25163
+ }
25164
+ ];
25165
+ }
25166
+ function sanitizeTextOnlyMessageContent(content) {
25167
+ if (typeof content === "string") {
25168
+ const trimmed = content.trim();
25169
+ return trimmed || void 0;
25170
+ }
25171
+ if (!Array.isArray(content)) {
25172
+ return void 0;
25173
+ }
25174
+ const parts = [];
25175
+ for (const part of content) {
25176
+ if (!part || typeof part !== "object") continue;
25177
+ if (part.type === "text" && typeof part.text === "string") {
25178
+ const text = part.text.trim();
25179
+ if (text) parts.push(text);
25180
+ }
25181
+ }
25182
+ const joined = parts.join("\n").trim();
25183
+ return joined || void 0;
25184
+ }
25185
+ function sanitizeMessagesForRequest(messages, options) {
25186
+ if (!options?.stripToolTurns && !options?.stripImages) {
25187
+ return messages;
25188
+ }
25189
+ const out = [];
25190
+ for (const msg of messages) {
25191
+ if (options.stripToolTurns && (msg.role === "tool" || msg.role === "function")) {
25192
+ continue;
25193
+ }
25194
+ if (options.stripToolTurns && msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
25195
+ const textOnly = sanitizeTextOnlyMessageContent(msg.content);
25196
+ if (!textOnly) {
25197
+ continue;
25198
+ }
25199
+ out.push({
25200
+ role: "assistant",
25201
+ content: textOnly
25202
+ });
25203
+ continue;
25204
+ }
25205
+ if (options.stripImages && msg.role === "user" && Array.isArray(msg.content)) {
25206
+ const textOnly = sanitizeTextOnlyMessageContent(msg.content);
25207
+ out.push({
25208
+ ...msg,
25209
+ content: textOnly || "(image omitted)"
25210
+ });
25211
+ continue;
25212
+ }
25213
+ if (options.stripImages && msg.role === "assistant" && Array.isArray(msg.content)) {
25214
+ const textOnly = sanitizeTextOnlyMessageContent(msg.content);
25215
+ if (!textOnly) continue;
25216
+ out.push({
25217
+ ...msg,
25218
+ content: textOnly
25219
+ });
25220
+ continue;
25221
+ }
25222
+ out.push(msg);
25223
+ }
25224
+ return out;
25225
+ }
25050
25226
  function applyDeltaToolCallsToAccumulator(toolCallsAccumulator, deltaToolCalls) {
25051
25227
  if (!deltaToolCalls?.length) {
25052
25228
  return false;
@@ -25128,11 +25304,43 @@ var LLMService = class {
25128
25304
  throw new Error("LLMService.chatCompletion: userContext \xE9 obrigat\xF3rio");
25129
25305
  }
25130
25306
  const runtimeConfig = getRuntimeConfig();
25131
- const resp = await this.client.chat.completions.create(
25132
- buildChatCompletionRequestBody(params, runtimeConfig, false),
25133
- { headers: this.requestHeaders(params.userContext) }
25307
+ const requestHeaders = this.requestHeaders(params.userContext);
25308
+ let lastError = null;
25309
+ const fallbacks = getRequestFallbacks();
25310
+ for (let i = 0; i < fallbacks.length; i += 1) {
25311
+ const fallback = fallbacks[i];
25312
+ const stage = describeFallback(fallback);
25313
+ try {
25314
+ llmLog.debug("LLM request attempt", {
25315
+ stream: false,
25316
+ stage,
25317
+ fallback: `${i + 1}/${fallbacks.length}`,
25318
+ request: summarizeRequestForLog(params, runtimeConfig)
25319
+ });
25320
+ const resp = await this.client.chat.completions.create(
25321
+ buildChatCompletionRequestBody(params, runtimeConfig, false, fallback),
25322
+ { headers: requestHeaders }
25323
+ );
25324
+ return resp;
25325
+ } catch (error) {
25326
+ lastError = error;
25327
+ logLLMRetry(stage, params, i, fallbacks.length, error, false);
25328
+ if (!isBadRequestLike(error)) {
25329
+ throw error;
25330
+ }
25331
+ }
25332
+ }
25333
+ const lastStage = describeFallback(fallbacks[fallbacks.length - 1] ?? {});
25334
+ llmLog.error("LLM request failed after retries", {
25335
+ stream: false,
25336
+ last_stage: lastStage,
25337
+ attempts: fallbacks.length,
25338
+ request: summarizeRequestForLog(params, runtimeConfig),
25339
+ error: summarizeError(lastError)
25340
+ });
25341
+ throw new Error(
25342
+ `LLM request failed after ${fallbacks.length} attempts [last_stage=${lastStage}]: ${lastError instanceof Error ? lastError.message : String(lastError ?? "unknown error")}`
25134
25343
  );
25135
- return resp;
25136
25344
  }
25137
25345
  /**
25138
25346
  * Streaming — mesmo turnId em todas as chamadas do loop de tools (via params.userContext).
@@ -25142,10 +25350,46 @@ var LLMService = class {
25142
25350
  throw new Error("LLMService.chatCompletionStream: userContext \xE9 obrigat\xF3rio");
25143
25351
  }
25144
25352
  const runtimeConfig = getRuntimeConfig();
25145
- const stream = await this.client.chat.completions.create(
25146
- buildChatCompletionRequestBody(params, runtimeConfig, true),
25147
- { headers: this.requestHeaders(params.userContext) }
25148
- );
25353
+ const requestHeaders = this.requestHeaders(params.userContext);
25354
+ let stream = null;
25355
+ let lastError = null;
25356
+ const fallbacks = getRequestFallbacks();
25357
+ for (let i = 0; i < fallbacks.length; i += 1) {
25358
+ const fallback = fallbacks[i];
25359
+ const stage = describeFallback(fallback);
25360
+ try {
25361
+ llmLog.debug("LLM stream request attempt", {
25362
+ stream: true,
25363
+ stage,
25364
+ fallback: `${i + 1}/${fallbacks.length}`,
25365
+ request: summarizeRequestForLog(params, runtimeConfig)
25366
+ });
25367
+ stream = await this.client.chat.completions.create(
25368
+ buildChatCompletionRequestBody(params, runtimeConfig, true, fallback),
25369
+ { headers: requestHeaders }
25370
+ );
25371
+ break;
25372
+ } catch (error) {
25373
+ lastError = error;
25374
+ logLLMRetry(stage, params, i, fallbacks.length, error, true);
25375
+ if (!isBadRequestLike(error)) {
25376
+ throw error;
25377
+ }
25378
+ }
25379
+ }
25380
+ if (!stream) {
25381
+ const lastStage = describeFallback(fallbacks[fallbacks.length - 1] ?? {});
25382
+ llmLog.error("LLM stream request failed after retries", {
25383
+ stream: true,
25384
+ last_stage: lastStage,
25385
+ attempts: fallbacks.length,
25386
+ request: summarizeRequestForLog(params, runtimeConfig),
25387
+ error: summarizeError(lastError)
25388
+ });
25389
+ throw new Error(
25390
+ `LLM request failed after ${fallbacks.length} attempts [last_stage=${lastStage}]: ${lastError instanceof Error ? lastError.message : String(lastError ?? "unknown error")}`
25391
+ );
25392
+ }
25149
25393
  const toolCallsAccumulator = /* @__PURE__ */ new Map();
25150
25394
  let reasoningSnapshot = "";
25151
25395
  for await (const chunk of stream) {
@@ -25184,231 +25428,37 @@ var LLMService = class {
25184
25428
  }
25185
25429
  };
25186
25430
 
25187
- // src/app/agent/core/llm/llm_errors.ts
25188
- function formatLlmUiError(error) {
25189
- const rawMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error during LLM request.";
25190
- const lower = rawMessage.toLowerCase();
25191
- let message2 = "Ocorreu um erro inesperado ao contactar o modelo.";
25192
- if (lower.includes("timeout") || lower.includes("etimedout") || lower.includes("upstream_timeout")) {
25193
- message2 = "O BluMa demorou demasiado a responder.";
25194
- } else if (lower.includes("connection") || lower.includes("econnrefused") || lower.includes("ehostunreach") || lower.includes("enotfound")) {
25195
- message2 = "N\xE3o foi poss\xEDvel conectar ao servi\xE7o do modelo.";
25196
- } else if (lower.includes("401") || lower.includes("403") || lower.includes("unauthorized") || lower.includes("forbidden")) {
25197
- message2 = "Falha de autentica\xE7\xE3o/autoriza\xE7\xE3o ao contactar o modelo.";
25198
- } else if (lower.includes("api")) {
25199
- message2 = "Erro de comunica\xE7\xE3o com a API do modelo.";
25200
- }
25201
- return {
25202
- message: message2,
25203
- rawMessage
25204
- };
25431
+ // src/app/agent/runtime/tool_execution_policy.ts
25432
+ init_sandbox_policy();
25433
+ init_runtime_config();
25434
+ init_permission_rules();
25435
+ import path38 from "path";
25436
+ var LOCAL_EDIT_TOOL_NAMES = /* @__PURE__ */ new Set(["edit_tool", "file_write", "notebook_edit"]);
25437
+ function getToolPermissionLayer(metadata) {
25438
+ if (metadata.riskLevel === "safe") return "read";
25439
+ if (metadata.riskLevel === "write") return "write";
25440
+ if (metadata.riskLevel === "network") return "network";
25441
+ return "execute";
25205
25442
  }
25206
-
25207
- // src/app/agent/core/llm/tool_call_normalizer.ts
25208
- import { randomUUID } from "crypto";
25209
- var ToolCallNormalizer = class {
25210
- /**
25211
- * Com tool_calls e sem texto visível: content deve ser null (API OpenAI-compatible), nunca "" nem undefined.
25212
- */
25213
- static assistantContentWithToolCalls(content) {
25214
- if (content === void 0 || content === null) return null;
25215
- if (typeof content === "string") return content.trim() === "" ? null : content;
25216
- return null;
25443
+ function isLocalEditTool(toolName) {
25444
+ return LOCAL_EDIT_TOOL_NAMES.has(toolName);
25445
+ }
25446
+ function checkFilePermissionRules(toolName, filePath, policy) {
25447
+ if (!filePath) {
25448
+ return { allowed: false, reason: "No file path provided for permission check." };
25217
25449
  }
25218
- /**
25219
- * Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
25220
- */
25221
- static normalizeAssistantMessage(message2) {
25222
- if (message2.tool_calls && this.isOpenAIFormat(message2.tool_calls)) {
25223
- return {
25224
- ...message2,
25225
- content: this.assistantContentWithToolCalls(message2.content)
25226
- };
25227
- }
25228
- const toolCalls = this.extractToolCalls(message2);
25229
- if (toolCalls.length > 0) {
25230
- return {
25231
- role: message2.role || "assistant",
25232
- content: this.assistantContentWithToolCalls(message2.content),
25233
- tool_calls: toolCalls
25234
- };
25235
- }
25236
- return message2;
25450
+ const resolvedPath = path38.resolve(filePath);
25451
+ if (!isPathInsideWorkspace(resolvedPath, policy)) {
25452
+ return { allowed: false, reason: `File path "${filePath}" is outside workspace root.` };
25237
25453
  }
25238
- /**
25239
- * Verifica se já está no formato OpenAI
25240
- */
25241
- static isOpenAIFormat(toolCalls) {
25242
- if (!Array.isArray(toolCalls) || toolCalls.length === 0) return false;
25243
- const firstCall = toolCalls[0];
25244
- return typeof firstCall.id === "string" && firstCall.type === "function" && typeof firstCall.function?.name === "string" && typeof firstCall.function?.arguments === "string";
25454
+ const relativePath = path38.relative(policy.workspaceRoot, resolvedPath);
25455
+ const toolPattern = `${toolName}(${relativePath})`;
25456
+ const ruleDecision = permissionRulesEngine.checkPermission(toolPattern, { filepath: filePath });
25457
+ if (ruleDecision === "deny") {
25458
+ return { allowed: false, reason: `File "${filePath}" denied by permission rules.` };
25245
25459
  }
25246
- /**
25247
- * Extrai tool calls de diversos formatos possíveis
25248
- */
25249
- static extractToolCalls(message2) {
25250
- const results = [];
25251
- if (message2.tool_calls && Array.isArray(message2.tool_calls)) {
25252
- for (const call of message2.tool_calls) {
25253
- const normalized = this.normalizeToolCall(call);
25254
- if (normalized) results.push(normalized);
25255
- }
25256
- }
25257
- if (typeof message2.content === "string" && message2.content.trim()) {
25258
- const extracted = this.extractFromContent(message2.content);
25259
- results.push(...extracted);
25260
- }
25261
- if (message2.function_call) {
25262
- const normalized = this.normalizeToolCall(message2.function_call);
25263
- if (normalized) results.push(normalized);
25264
- }
25265
- return results;
25266
- }
25267
- /**
25268
- * Normaliza um único tool call para o formato OpenAI
25269
- */
25270
- static normalizeToolCall(call) {
25271
- try {
25272
- if (call.id && call.function?.name) {
25273
- return {
25274
- id: call.id,
25275
- type: "function",
25276
- function: {
25277
- name: call.function.name,
25278
- arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments)
25279
- }
25280
- };
25281
- }
25282
- if (call.name) {
25283
- return {
25284
- id: call.id || randomUUID(),
25285
- type: "function",
25286
- function: {
25287
- name: call.name,
25288
- arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments || {})
25289
- }
25290
- };
25291
- }
25292
- if (call.function && typeof call.function === "object") {
25293
- return {
25294
- id: call.id || randomUUID(),
25295
- type: "function",
25296
- function: {
25297
- name: call.function.name,
25298
- arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments || {})
25299
- }
25300
- };
25301
- }
25302
- return null;
25303
- } catch (error) {
25304
- console.error("Error normalizing tool call:", error, call);
25305
- return null;
25306
- }
25307
- }
25308
- /**
25309
- * Extrai tool calls do content (pode estar em markdown, JSON, etc)
25310
- */
25311
- static extractFromContent(content) {
25312
- const results = [];
25313
- const cleanContent2 = content.replace(/```(?:json)?\s*([\s\S]*?)```/g, "$1");
25314
- const jsonMatches = this.extractJsonObjects(cleanContent2);
25315
- for (const jsonStr of jsonMatches) {
25316
- try {
25317
- const parsed = JSON.parse(jsonStr);
25318
- if (Array.isArray(parsed)) {
25319
- for (const call of parsed) {
25320
- const normalized = this.normalizeToolCall(call);
25321
- if (normalized) results.push(normalized);
25322
- }
25323
- } else if (parsed.name || parsed.function) {
25324
- const normalized = this.normalizeToolCall(parsed);
25325
- if (normalized) results.push(normalized);
25326
- } else if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) {
25327
- for (const call of parsed.tool_calls) {
25328
- const normalized = this.normalizeToolCall(call);
25329
- if (normalized) results.push(normalized);
25330
- }
25331
- }
25332
- } catch (e) {
25333
- }
25334
- }
25335
- return results;
25336
- }
25337
- /**
25338
- * Extrai objetos JSON de uma string (suporta múltiplos objetos)
25339
- */
25340
- static extractJsonObjects(text) {
25341
- const results = [];
25342
- let depth = 0;
25343
- let start = -1;
25344
- for (let i = 0; i < text.length; i++) {
25345
- if (text[i] === "{") {
25346
- if (depth === 0) start = i;
25347
- depth++;
25348
- } else if (text[i] === "}") {
25349
- depth--;
25350
- if (depth === 0 && start !== -1) {
25351
- results.push(text.substring(start, i + 1));
25352
- start = -1;
25353
- }
25354
- }
25355
- }
25356
- return results;
25357
- }
25358
- /**
25359
- * Valida se um tool call normalizado é válido
25360
- * Validação dupla:
25361
- * 1. JSON dos argumentos é válido
25362
- * 2. A ferramenta existe no catálogo de ferramentas nativas
25363
- */
25364
- static isValidToolCall(call) {
25365
- if (!(call.id && call.type === "function" && call.function?.name && typeof call.function.arguments === "string")) {
25366
- return false;
25367
- }
25368
- try {
25369
- JSON.parse(call.function.arguments);
25370
- } catch {
25371
- return false;
25372
- }
25373
- const toolMetadata = getNativeToolMetadata(call.function.name);
25374
- if (!toolMetadata) {
25375
- return false;
25376
- }
25377
- return true;
25378
- }
25379
- };
25380
-
25381
- // src/app/agent/runtime/tool_execution_policy.ts
25382
- init_sandbox_policy();
25383
- init_runtime_config();
25384
- init_permission_rules();
25385
- import path38 from "path";
25386
- var LOCAL_EDIT_TOOL_NAMES = /* @__PURE__ */ new Set(["edit_tool", "file_write", "notebook_edit"]);
25387
- function getToolPermissionLayer(metadata) {
25388
- if (metadata.riskLevel === "safe") return "read";
25389
- if (metadata.riskLevel === "write") return "write";
25390
- if (metadata.riskLevel === "network") return "network";
25391
- return "execute";
25392
- }
25393
- function isLocalEditTool(toolName) {
25394
- return LOCAL_EDIT_TOOL_NAMES.has(toolName);
25395
- }
25396
- function checkFilePermissionRules(toolName, filePath, policy) {
25397
- if (!filePath) {
25398
- return { allowed: false, reason: "No file path provided for permission check." };
25399
- }
25400
- const resolvedPath = path38.resolve(filePath);
25401
- if (!isPathInsideWorkspace(resolvedPath, policy)) {
25402
- return { allowed: false, reason: `File path "${filePath}" is outside workspace root.` };
25403
- }
25404
- const relativePath = path38.relative(policy.workspaceRoot, resolvedPath);
25405
- const toolPattern = `${toolName}(${relativePath})`;
25406
- const ruleDecision = permissionRulesEngine.checkPermission(toolPattern, { filepath: filePath });
25407
- if (ruleDecision === "deny") {
25408
- return { allowed: false, reason: `File "${filePath}" denied by permission rules.` };
25409
- }
25410
- if (ruleDecision === "allow") {
25411
- return { allowed: true, reason: `File "${filePath}" allowed by permission rules.` };
25460
+ if (ruleDecision === "allow") {
25461
+ return { allowed: true, reason: `File "${filePath}" allowed by permission rules.` };
25412
25462
  }
25413
25463
  const dirPath = path38.dirname(relativePath);
25414
25464
  const dirPattern = `${toolName}(${dirPath}/**)`;
@@ -25699,28 +25749,6 @@ function consolidateCodingMemoryFile() {
25699
25749
  };
25700
25750
  }
25701
25751
 
25702
- // src/app/agent/runtime/tool_orchestration.ts
25703
- var PARALLEL_SAFE_TOOL_NAMES = /* @__PURE__ */ new Set([
25704
- "ls_tool",
25705
- "read_file_lines",
25706
- "count_file_lines",
25707
- "find_by_name",
25708
- "grep_search",
25709
- "view_file_outline",
25710
- "read_artifact",
25711
- "list_agents",
25712
- "list_mcp_resources",
25713
- "read_mcp_resource",
25714
- "todo"
25715
- ]);
25716
- function toolEligibleForParallelRead(toolName, sessionId) {
25717
- const name = String(toolName || "").trim();
25718
- if (!name || !PARALLEL_SAFE_TOOL_NAMES.has(name)) return false;
25719
- if (!decideToolExecution(name, void 0, sessionId).autoApprove) return false;
25720
- const meta = getNativeToolMetadata(name);
25721
- return meta?.riskLevel === "safe";
25722
- }
25723
-
25724
25752
  // src/app/agent/bluma/turn_start_payload.ts
25725
25753
  function buildUserPromptPreview(inputText) {
25726
25754
  const t = String(inputText ?? "").trim();
@@ -25785,276 +25813,153 @@ function subscribeToolResultStatuses(listener) {
25785
25813
  return changed.subscribe(listener);
25786
25814
  }
25787
25815
 
25788
- // src/app/agent/bluma/core/bluma.ts
25789
- var BluMaAgent = class _BluMaAgent {
25790
- static MAX_INVALID_TOOL_CALL_RETRIES = 3;
25791
- llm;
25792
- sessionId;
25793
- sessionFile = "";
25794
- history = [];
25795
- eventBus;
25796
- mcpClient;
25797
- feedbackSystem;
25798
- skillLoader;
25799
- compressor;
25800
- isInterrupted = false;
25801
- /** Mesmo turnId durante processTurn + todo o loop de tool_calls (FactorRouter). */
25802
- activeTurnContext = null;
25803
- /** Evita POST /turns/.../end duplicado no mesmo turno (ex.: Esc após message_result). */
25804
- factorRouterTurnClosed = false;
25805
- /** Passos seguidos sem tool_calls nem texto visível (só raciocínio) — evita loop lento no mesmo turno. */
25806
- emptyAssistantReplySteps = 0;
25807
- /** Reintentos consecutivos por tool call inválido. */
25808
- invalidToolCallRetrySteps = 0;
25809
- constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
25810
- this.sessionId = sessionId;
25811
- this.eventBus = eventBus;
25812
- this.llm = llm;
25813
- this.mcpClient = mcpClient;
25814
- this.feedbackSystem = feedbackSystem;
25815
- this.skillLoader = new SkillLoader(process.cwd());
25816
- this.compressor = new HistoryCompressor();
25817
- this.eventBus.on("user_interrupt", async () => {
25818
- this.isInterrupted = true;
25819
- await this.notifyFactorTurnEndIfNeeded("user_interrupt");
25820
- this.eventBus.emit("backend_message", { type: "done", status: "interrupted" });
25821
- });
25822
- this.eventBus.on("user_overlay", async (data) => {
25823
- const clean = String(data.payload ?? "").trim();
25824
- this.history.push({ role: "user", content: clean });
25825
- this.eventBus.emit("backend_message", { type: "user_overlay", payload: clean, ts: data.ts || Date.now() });
25826
- try {
25827
- if (this.sessionFile) {
25828
- this.persistSession();
25829
- } else {
25830
- const [sessionFile, , mem] = await loadOrcreateSession(this.sessionId);
25831
- this.sessionFile = sessionFile;
25832
- this.compressor.restoreSnapshot(mem);
25833
- this.persistSession();
25834
- }
25835
- } catch (e) {
25836
- this.eventBus.emit("backend_message", { type: "error", message: `Falha ao salvar hist\xF3rico ap\xF3s user_overlay: ${e.message}` });
25837
- }
25838
- });
25839
- }
25840
- getMemorySnapshot() {
25841
- return this.compressor.getSnapshot();
25842
- }
25843
- /**
25844
- * Salvar histórico de forma SÍNCRONA - usado em situações críticas (erros, shutdown)
25845
- * para garantir que nada se perde. NÃO usa debounce.
25846
- */
25847
- persistSessionSync() {
25848
- if (!this.sessionFile) return;
25816
+ // src/utils/toolActionStatus.ts
25817
+ function parseArgsRecord(args) {
25818
+ if (args == null) return {};
25819
+ if (typeof args === "string") {
25849
25820
  try {
25850
- const sessionData = {
25851
- session_id: path40.basename(this.sessionFile, ".json"),
25852
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
25853
- conversation_history: this.history,
25854
- last_updated: (/* @__PURE__ */ new Date()).toISOString(),
25855
- ...this.compressor.getSnapshot()
25856
- };
25857
- fs39.writeFileSync(this.sessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
25858
- } catch (error) {
25859
- console.error("[Bluma] Failed to persist session synchronously:", error);
25821
+ return JSON.parse(args);
25822
+ } catch {
25823
+ return {};
25860
25824
  }
25861
25825
  }
25862
- /** Debounced: grava histórico + estado de compressão no mesmo ficheiro de sessão. */
25863
- persistSession() {
25864
- if (!this.sessionFile) return;
25865
- void saveSessionHistory(this.sessionFile, this.history, this.getMemorySnapshot());
25866
- }
25867
- recordUiSlashCommand(command, mode = "visible_only") {
25868
- const text = String(command ?? "").trim();
25869
- if (!text) return;
25870
- this.history.push({
25871
- role: "user",
25872
- name: mode === "with_internal_prompt" ? "ui_slash_command" : "ui_slash_command_local",
25873
- content: text
25874
- });
25875
- this.persistSession();
25826
+ if (typeof args === "object") {
25827
+ return args;
25876
25828
  }
25877
- async handleInvalidToolCallRetry(message2) {
25878
- this.invalidToolCallRetrySteps += 1;
25879
- if (this.history[this.history.length - 1] === message2) {
25880
- this.history.pop();
25881
- }
25882
- if (this.invalidToolCallRetrySteps >= _BluMaAgent.MAX_INVALID_TOOL_CALL_RETRIES) {
25883
- this.eventBus.emit("backend_message", {
25884
- type: "error",
25885
- message: "The model kept returning invalid tool calls. Closing the turn to avoid a retry loop."
25886
- });
25887
- this.eventBus.emit("backend_message", {
25888
- type: "log",
25889
- message: "Invalid tool call retry limit reached",
25890
- payload: String(this.invalidToolCallRetrySteps)
25891
- });
25892
- await this.notifyFactorTurnEndIfNeeded("invalid_tool_calls_exhausted");
25893
- this.eventBus.emit("backend_message", { type: "done", status: "failed" });
25894
- this.invalidToolCallRetrySteps = 0;
25895
- return;
25829
+ return {};
25830
+ }
25831
+ function truncate2(value, max = 44) {
25832
+ const text = value.trim().replace(/\s+/g, " ");
25833
+ if (!text) return "";
25834
+ return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
25835
+ }
25836
+ function shortFileName(pathLike) {
25837
+ const trimmed = pathLike.trim();
25838
+ if (!trimmed) return "";
25839
+ return trimmed.split("/").pop() || trimmed;
25840
+ }
25841
+ function formatCommand(command) {
25842
+ const compact = truncate2(command, 42);
25843
+ return compact ? `shell command: ${compact}` : "shell command";
25844
+ }
25845
+ function formatSearchQuery(prefix, query) {
25846
+ const compact = truncate2(query, 36);
25847
+ return compact ? `${prefix}: ${compact}` : prefix;
25848
+ }
25849
+ function formatResponseLabel(toolName, args) {
25850
+ switch (toolName) {
25851
+ case "shell_command":
25852
+ case "run_command": {
25853
+ const cmd = typeof args.command === "string" ? args.command : "";
25854
+ return cmd ? `Running ${formatCommand(cmd)}` : "Running shell command";
25896
25855
  }
25897
- this.history.push({
25898
- role: "system",
25899
- content: "Previous assistant tool_calls were invalid. Retry with valid JSON arguments only, or answer without tools."
25900
- });
25901
- this.persistSession();
25902
- await this._continueConversation();
25903
- }
25904
- async initialize() {
25905
- await this.mcpClient.nativeToolInvoker.initialize();
25906
- await this.mcpClient.initialize();
25907
- const [sessionFile, history, mem] = await loadOrcreateSession(this.sessionId);
25908
- this.sessionFile = sessionFile;
25909
- this.history = history;
25910
- this.compressor.restoreSnapshot(mem);
25911
- initializeSkillContext({
25912
- history: this.history,
25913
- skillLoader: this.skillLoader
25914
- });
25915
- registerSessionCronBridge({
25916
- eventBus: this.eventBus,
25917
- getSessionId: () => this.sessionId
25918
- });
25919
- const dirs = this.skillLoader.getSkillsDirs();
25920
- this.eventBus.emit("backend_message", {
25921
- type: "info",
25922
- message: `Skills dirs \u2014 bundled: ${dirs.bundled} | project: ${dirs.project} | global: ${dirs.global}`
25923
- });
25924
- const availableSkills = this.skillLoader.listAvailable();
25925
- this.eventBus.emit("backend_message", {
25926
- type: "info",
25927
- message: `Skills loaded: ${availableSkills.length} \u2014 ${availableSkills.map((s) => `${s.name} (${s.source})`).join(", ") || "none"}`
25928
- });
25929
- if (this.skillLoader.hasConflicts()) {
25930
- for (const warning of this.skillLoader.formatConflictWarnings()) {
25931
- this.eventBus.emit("backend_message", {
25932
- type: "warning",
25933
- message: warning
25934
- });
25935
- }
25936
- }
25937
- const toolsDetailed = this.mcpClient.getAvailableToolsDetailed();
25938
- const availableToolNames = toolsDetailed.map((t) => t.function?.name).filter(Boolean);
25939
- const systemPrompt = await getUnifiedSystemPrompt(availableSkills, {
25940
- availableToolNames,
25941
- toolsDetailed,
25942
- mcpClient: this.mcpClient,
25943
- sessionId: this.sessionId
25944
- });
25945
- if (this.history.length === 0) {
25946
- this.history.push({ role: "system", content: systemPrompt });
25947
- } else {
25948
- const sysIdx = this.history.findIndex(
25949
- (m) => m.role === "system" && typeof m.content === "string" && String(m.content).includes("<identity>")
25950
- );
25951
- if (sysIdx >= 0) {
25952
- this.history[sysIdx] = { ...this.history[sysIdx], content: systemPrompt };
25953
- } else {
25954
- this.history.unshift({ role: "system", content: systemPrompt });
25955
- }
25956
- }
25957
- this.persistSession();
25958
- const uiHistory = this.history.filter(
25959
- (m) => m.role !== "system"
25960
- );
25961
- if (uiHistory.length > 0) {
25962
- this.eventBus.emit("backend_message", {
25963
- type: "session_history",
25964
- messages: uiHistory
25965
- });
25966
- }
25967
- }
25968
- getAvailableTools() {
25969
- return this.mcpClient.getAvailableTools();
25970
- }
25971
- getUiToolsDetailed() {
25972
- return this.mcpClient.getAvailableToolsDetailed();
25973
- }
25974
- listAvailableSkills() {
25975
- return this.skillLoader.listAvailable();
25976
- }
25977
- getSkillsDirs() {
25978
- return this.skillLoader.getSkillsDirs();
25979
- }
25980
- getSkillConflictWarnings() {
25981
- return this.skillLoader.formatConflictWarnings();
25982
- }
25983
- getFeedbackScore() {
25984
- return this.feedbackSystem.getCumulativeScore();
25985
- }
25986
- /**
25987
- * Contrato de ciclo de turno (UI, hooks, Factor Router):
25988
- *
25989
- * - `backend_message` `{ type: 'turn_start', turnId, sessionId, userPromptPreview }` — início;
25990
- * payload estável em {@link buildTurnStartBackendMessage}.
25991
- * - Stream: `stream_start` / `stream_reasoning_chunk` / `stream_chunk` / `stream_end`.
25992
- * Se a resposta incluir tool `message`, `stream_end` leva `{ omitAssistantFlush: true }` para não
25993
- * duplicar texto no histórico (o utilizador vê o `tool_result` com gutter). Ver `applyStreamEndFlush`.
25994
- * - Fim: `backend_message` `{ type: 'done', status }` (`completed` | `failed` | `interrupted` | …).
25995
- * - Factor Router: um POST `/turns/{turnId}/end` por turno via `notifyFactorTurnEndIfNeeded` (razões abaixo).
25996
- *
25997
- * Smoke manual sugerido: mensagem normal com `message`+`result`; Ctrl+C; erro LLM; loop sem resposta útil.
25998
- */
25999
- async processTurn(userInput, userContextInput, options) {
26000
- this.isInterrupted = false;
26001
- this.factorRouterTurnClosed = false;
26002
- const inputText = String(userInput.content || "").trim();
26003
- const turnId = uuidv48();
26004
- this.activeTurnContext = {
26005
- ...userContextInput,
26006
- turnId,
26007
- sessionId: userContextInput.sessionId || this.sessionId
26008
- };
26009
- process.env.BLUMA_USER_CONTEXT_JSON = JSON.stringify({
26010
- conversationId: this.activeTurnContext.conversationId ?? null,
26011
- userId: this.activeTurnContext.userId ?? null,
26012
- userName: this.activeTurnContext.userName ?? null,
26013
- userEmail: this.activeTurnContext.userEmail ?? null,
26014
- companyId: this.activeTurnContext.companyId ?? null,
26015
- companyName: this.activeTurnContext.companyName ?? null
26016
- });
26017
- const userContent = buildUserMessageContent(inputText, process.cwd());
26018
- this.history.push({
26019
- role: "user",
26020
- content: userContent,
26021
- ...options?.historyName ? { name: options.historyName } : {}
26022
- });
26023
- this.emptyAssistantReplySteps = 0;
26024
- this.invalidToolCallRetrySteps = 0;
26025
- this.eventBus.emit(
26026
- "backend_message",
26027
- buildTurnStartBackendMessage({
26028
- turnId: this.activeTurnContext.turnId,
26029
- sessionId: this.activeTurnContext.sessionId,
26030
- inputText,
26031
- appContext: this.activeTurnContext.appContext ?? null
26032
- })
26033
- );
26034
- if (inputText === "/init") {
26035
- this.eventBus.emit("dispatch", inputText);
26036
- }
26037
- await this._continueConversation();
25856
+ case "command_status":
25857
+ return "Checking command status";
25858
+ case "send_command_input":
25859
+ return "Sending command input";
25860
+ case "kill_command":
25861
+ return "Stopping command";
25862
+ case "read_file_lines":
25863
+ return `Reading ${shortFileName(String(args.filepath ?? args.file_path ?? "")) || "file"}`;
25864
+ case "count_file_lines":
25865
+ return `Counting lines in ${shortFileName(String(args.filepath ?? args.file_path ?? "")) || "file"}`;
25866
+ case "ls_tool":
25867
+ return "Listing files";
25868
+ case "edit_tool": {
25869
+ const edits = Array.isArray(args.edits) ? args.edits : [];
25870
+ const count = edits.length || 1;
25871
+ const filePath = typeof args.file_path === "string" ? args.file_path : typeof edits[0]?.file_path === "string" ? String(edits[0].file_path) : "";
25872
+ const file = shortFileName(filePath) || "file";
25873
+ return count === 1 ? `Editing ${file}` : `Editing ${count} files`;
25874
+ }
25875
+ case "file_write":
25876
+ return `Writing ${shortFileName(String(args.filepath ?? args.file_path ?? "")) || "file"}`;
25877
+ case "grep_search":
25878
+ return formatSearchQuery("Searching code", String(args.query ?? ""));
25879
+ case "find_by_name":
25880
+ return formatSearchQuery("Finding files", String(args.pattern ?? ""));
25881
+ case "view_file_outline":
25882
+ return `Inspecting ${shortFileName(String(args.file_path ?? "")) || "file outline"}`;
25883
+ case "web_fetch":
25884
+ return formatSearchQuery("Fetching URL", String(args.url ?? ""));
25885
+ case "search_web":
25886
+ return formatSearchQuery("Searching the web", String(args.query ?? ""));
25887
+ case "spawn_agent":
25888
+ return "Spawning a worker";
25889
+ case "wait_agent":
25890
+ return "Waiting for worker";
25891
+ case "list_agents":
25892
+ return "Reviewing workers";
25893
+ case "todo":
25894
+ return "Updating task list";
25895
+ case "task_boundary":
25896
+ return "Switching task boundary";
25897
+ case "task_create":
25898
+ case "task_list":
25899
+ case "task_get":
25900
+ case "task_update":
25901
+ case "task_stop":
25902
+ return "Managing task";
25903
+ case "load_skill":
25904
+ return "Loading skill context";
25905
+ case "coding_memory":
25906
+ return "Updating working memory";
25907
+ case "read_artifact":
25908
+ return `Reading ${String(args.filename ?? "artifact")}`;
25909
+ case "message":
25910
+ return "Preparing assistant reply";
25911
+ case "ask_user_question":
25912
+ return "Waiting for your answer";
25913
+ case "enter_plan_mode":
25914
+ return "Entering plan mode";
25915
+ case "exit_plan_mode":
25916
+ return "Leaving plan mode";
25917
+ case "list_mcp_resources":
25918
+ return "Browsing MCP resources";
25919
+ case "read_mcp_resource":
25920
+ return "Reading MCP resource";
25921
+ case "cron_create":
25922
+ return "Scheduling reminder";
25923
+ case "cron_list":
25924
+ return "Listing reminders";
25925
+ case "cron_delete":
25926
+ return "Cancelling reminder";
25927
+ case "notebook_edit":
25928
+ return `Editing ${shortFileName(String(args.filepath ?? "")) || "notebook"}`;
25929
+ case "lsp_query":
25930
+ return "Querying language server";
25931
+ default:
25932
+ return toolName.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()) || "Working";
26038
25933
  }
26039
- /** Política mostrada na UI: combina metadata base com auto-approve efectivo (incl. permission_ml). */
26040
- buildToolPolicyForUi(toolName, toolArgs) {
26041
- const base = decideToolExecution(toolName, void 0, this.sessionId);
26042
- const argsStr = typeof toolArgs === "string" ? toolArgs : JSON.stringify(toolArgs ?? {});
26043
- const synthetic = { function: { name: toolName, arguments: argsStr } };
26044
- const auto = effectiveToolAutoApprove(synthetic, this.sessionId, { logClassifier: false });
26045
- return {
26046
- ...base,
26047
- autoApprove: auto,
26048
- requiresConfirmation: !auto
26049
- };
25934
+ }
25935
+ function getToolActionLabel(toolName, args) {
25936
+ return formatResponseLabel(toolName, parseArgsRecord(args));
25937
+ }
25938
+ function getToolActionStatus(toolName, args) {
25939
+ return {
25940
+ action: getToolActionLabel(toolName, args),
25941
+ phase: "tool"
25942
+ };
25943
+ }
25944
+ function getReasoningActionStatus() {
25945
+ return {
25946
+ action: "Reasoning",
25947
+ phase: "thinking"
25948
+ };
25949
+ }
25950
+ function getRespondingActionStatus() {
25951
+ return {
25952
+ action: "Writing",
25953
+ phase: "responding"
25954
+ };
25955
+ }
25956
+
25957
+ // src/app/agent/bluma/core/bluma_tool_runner.ts
25958
+ var BluMaToolRunner = class {
25959
+ constructor(deps) {
25960
+ this.deps = deps;
26050
25961
  }
26051
- /**
26052
- * Executa uma tool aprovada: emit tool_call → invoke → tool_result → histórico.
26053
- * `backend_message` tool_call / tool_result incluem `tool_call_id` (id do tool call no modelo)
26054
- * para a UI correlacionar a linha de invocação (•) com o resultado compacto.
26055
- * @returns shouldContinue — false se `message` com message_type result terminou o turno.
26056
- */
26057
- async executeApprovedToolInvocation(toolCall, decisionData) {
25962
+ async executeApprovedToolInvocation(toolCall, _decisionData) {
26058
25963
  const toolName = toolCall.function.name;
26059
25964
  let toolResultContent;
26060
25965
  let shouldContinueConversation = true;
@@ -26068,8 +25973,8 @@ var BluMaAgent = class _BluMaAgent {
26068
25973
  if (toolArgs === void 0 || toolArgs === null) {
26069
25974
  toolArgs = {};
26070
25975
  }
26071
- } catch (parseError) {
26072
- this.eventBus.emit("backend_message", {
25976
+ } catch {
25977
+ this.deps.eventBus.emit("backend_message", {
26073
25978
  type: "info",
26074
25979
  message: "O BluMa encontrou um erro ao processar. A tentar recuperar a sess\xE3o..."
26075
25980
  });
@@ -26078,12 +25983,12 @@ var BluMaAgent = class _BluMaAgent {
26078
25983
  error: "Tool arguments could not be parsed",
26079
25984
  recovery: "Session recovered automatically"
26080
25985
  });
26081
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26082
- this.history.push({
25986
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
25987
+ this.deps.history.push({
26083
25988
  role: "system",
26084
25989
  content: "The previous tool call had invalid JSON arguments. Please retry with properly formatted JSON arguments."
26085
25990
  });
26086
- this.persistSession();
25991
+ this.deps.persistSession();
26087
25992
  return true;
26088
25993
  }
26089
25994
  const toolMetadata = getNativeToolMetadata(toolName);
@@ -26093,18 +25998,18 @@ var BluMaAgent = class _BluMaAgent {
26093
25998
  error: `Tool "${toolName}" not found in agent catalog.`,
26094
25999
  suggestion: "This tool does not exist. Please use a valid tool name from the available tools list."
26095
26000
  });
26096
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26097
- this.history.push({
26001
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26002
+ this.deps.history.push({
26098
26003
  role: "system",
26099
26004
  content: `[SYSTEM] Tool "${toolName}" is not found in the agent's available tools. Please use only tools that are listed in your tool catalog.`
26100
26005
  });
26101
- this.persistSession();
26006
+ this.deps.persistSession();
26102
26007
  return true;
26103
26008
  }
26104
26009
  if (toolName === "edit_tool") {
26105
26010
  const batch = toolArgs.edits;
26106
26011
  if (Array.isArray(batch) && batch.length > 0) {
26107
- for (let i = 0; i < batch.length; i++) {
26012
+ for (let i = 0; i < batch.length; i += 1) {
26108
26013
  const entry = batch[i];
26109
26014
  if (!entry?.file_path || typeof entry.file_path !== "string") {
26110
26015
  toolResultContent = JSON.stringify({
@@ -26113,8 +26018,8 @@ var BluMaAgent = class _BluMaAgent {
26113
26018
  details: `edits[${i}].file_path is required`,
26114
26019
  received: toolArgs
26115
26020
  });
26116
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26117
- this.persistSession();
26021
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26022
+ this.deps.persistSession();
26118
26023
  return true;
26119
26024
  }
26120
26025
  if (entry.old_string === void 0 || entry.new_string === void 0) {
@@ -26124,8 +26029,8 @@ var BluMaAgent = class _BluMaAgent {
26124
26029
  details: `edits[${i}]: old_string and new_string are required`,
26125
26030
  received: toolArgs
26126
26031
  });
26127
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26128
- this.persistSession();
26032
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26033
+ this.deps.persistSession();
26129
26034
  return true;
26130
26035
  }
26131
26036
  }
@@ -26137,8 +26042,8 @@ var BluMaAgent = class _BluMaAgent {
26137
26042
  details: "file_path is required and must be a string (or provide non-empty edits[])",
26138
26043
  received: toolArgs
26139
26044
  });
26140
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26141
- this.persistSession();
26045
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26046
+ this.deps.persistSession();
26142
26047
  return true;
26143
26048
  }
26144
26049
  if (toolArgs.old_string === void 0 || toolArgs.new_string === void 0) {
@@ -26148,8 +26053,8 @@ var BluMaAgent = class _BluMaAgent {
26148
26053
  details: "old_string and new_string are required (or provide edits[])",
26149
26054
  received: toolArgs
26150
26055
  });
26151
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26152
- this.persistSession();
26056
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26057
+ this.deps.persistSession();
26153
26058
  return true;
26154
26059
  }
26155
26060
  }
@@ -26157,61 +26062,34 @@ var BluMaAgent = class _BluMaAgent {
26157
26062
  let previewContent;
26158
26063
  if (toolName === "edit_tool") {
26159
26064
  try {
26160
- previewContent = await this._generateEditPreview(toolArgs);
26065
+ previewContent = await this.deps.generateEditPreview(toolArgs);
26161
26066
  } catch (previewError) {
26162
26067
  previewContent = `Failed to generate preview: ${previewError.message}`;
26163
26068
  }
26164
26069
  } else if (toolName === "file_write") {
26165
26070
  try {
26166
- previewContent = this._generateFileWritePreview(toolArgs);
26071
+ previewContent = this.deps.generateFileWritePreview(toolArgs);
26167
26072
  } catch (previewError) {
26168
26073
  previewContent = `Failed to generate preview: ${previewError.message}`;
26169
26074
  }
26170
26075
  }
26171
- this.eventBus.emit("backend_message", {
26076
+ this.deps.eventBus.emit("backend_message", {
26172
26077
  type: "tool_call",
26173
26078
  tool_call_id: String(toolCall.id),
26174
26079
  tool_name: toolName,
26175
26080
  arguments: toolArgs,
26176
26081
  preview: previewContent,
26177
26082
  suppress_edit_diff_preview: toolName === "edit_tool",
26178
- tool_policy: this.buildToolPolicyForUi(toolName, toolArgs)
26083
+ tool_policy: this.deps.buildToolPolicyForUi(toolName, toolArgs)
26179
26084
  });
26180
26085
  setToolResultStatus(String(toolCall.id), "running");
26181
26086
  try {
26182
- if (this.isInterrupted) {
26183
- this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled before tool execution." });
26087
+ if (this.deps.isInterrupted()) {
26088
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled before tool execution." });
26184
26089
  return false;
26185
26090
  }
26186
- const actionMap = {
26187
- edit_tool: "Editing",
26188
- shell_command: "Executing",
26189
- command_status: "Waiting",
26190
- ls_tool: "Reading",
26191
- read_file_lines: "Reading",
26192
- message: "Responding",
26193
- load_skill: "Loading skill",
26194
- search_web: "Searching",
26195
- todo: "Planning",
26196
- ask_user_question: "Question",
26197
- enter_plan_mode: "Planning",
26198
- exit_plan_mode: "Planning",
26199
- task_create: "Planning",
26200
- task_list: "Planning",
26201
- task_get: "Planning",
26202
- task_update: "Planning",
26203
- task_stop: "Planning",
26204
- list_mcp_resources: "Reading",
26205
- read_mcp_resource: "Reading",
26206
- cron_create: "Scheduling",
26207
- cron_list: "Reading",
26208
- cron_delete: "Scheduling",
26209
- notebook_edit: "Editing",
26210
- lsp_query: "Reading"
26211
- };
26212
- const action = actionMap[toolName] || "Processing";
26213
- this.eventBus.emit("action_status", { action });
26214
- const result = await this.mcpClient.invoke(toolName, toolArgs);
26091
+ this.deps.eventBus.emit("action_status", getToolActionStatus(toolName, toolArgs));
26092
+ const result = await this.deps.mcpClient.invoke(toolName, toolArgs);
26215
26093
  const normalized = normalizeToolResult(result);
26216
26094
  setToolResultStatus(String(toolCall.id), normalized.status);
26217
26095
  toolResultContent = toolResultToString(normalized);
@@ -26225,12 +26103,12 @@ var BluMaAgent = class _BluMaAgent {
26225
26103
  null,
26226
26104
  2
26227
26105
  );
26228
- this.eventBus.emit("backend_message", {
26106
+ this.deps.eventBus.emit("backend_message", {
26229
26107
  type: "error",
26230
26108
  message: `Tool "${toolName}" failed: ${error.message}`
26231
26109
  });
26232
26110
  }
26233
- this.eventBus.emit("backend_message", {
26111
+ this.deps.eventBus.emit("backend_message", {
26234
26112
  type: "tool_result",
26235
26113
  tool_call_id: String(toolCall.id),
26236
26114
  tool_name: toolName,
@@ -26242,21 +26120,20 @@ var BluMaAgent = class _BluMaAgent {
26242
26120
  try {
26243
26121
  const resultObj = typeof toolResultContent === "string" ? JSON.parse(toolResultContent) : toolResultContent;
26244
26122
  if (resultObj.message_type === "result") {
26245
- await this.notifyFactorTurnEndIfNeeded("message_result");
26123
+ await this.deps.notifyFactorTurnEndIfNeeded("message_result");
26246
26124
  shouldContinueConversation = false;
26247
- this.emitTurnCompleted();
26125
+ this.deps.emitTurnCompleted();
26248
26126
  }
26249
26127
  } catch {
26250
26128
  }
26251
26129
  }
26252
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26253
- this.persistSession();
26130
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26131
+ this.deps.persistSession();
26254
26132
  return shouldContinueConversation;
26255
26133
  }
26256
- /** Várias leituras seguras em paralelo (grep, ls, read_file, …). */
26257
26134
  async executeParallelReadBatch(batch, _decisionData) {
26258
- if (this.isInterrupted) {
26259
- this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled before tool execution." });
26135
+ if (this.deps.isInterrupted()) {
26136
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled before tool execution." });
26260
26137
  return false;
26261
26138
  }
26262
26139
  const parsed = [];
@@ -26279,59 +26156,46 @@ var BluMaAgent = class _BluMaAgent {
26279
26156
  error: `Tool "${toolName}" not found in agent catalog.`,
26280
26157
  suggestion: "This tool does not exist. Please use a valid tool name from the available tools list."
26281
26158
  });
26282
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26283
- this.history.push({
26159
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26160
+ this.deps.history.push({
26284
26161
  role: "system",
26285
26162
  content: `[SYSTEM] Tool "${toolName}" is not found in the agent's available tools. Please use only tools that are listed in your tool catalog.`
26286
26163
  });
26287
- this.persistSession();
26164
+ this.deps.persistSession();
26288
26165
  return true;
26289
26166
  }
26290
26167
  parsed.push({ toolCall, toolName, toolArgs });
26291
- } catch (parseError) {
26168
+ } catch {
26292
26169
  const toolResultContent = JSON.stringify({
26293
26170
  error: "Tool arguments could not be parsed",
26294
26171
  recovery: "Session recovered automatically"
26295
26172
  });
26296
- this.eventBus.emit("backend_message", {
26173
+ this.deps.eventBus.emit("backend_message", {
26297
26174
  type: "info",
26298
26175
  message: "O BluMa encontrou um erro ao processar. A tentar recuperar a sess\xE3o..."
26299
26176
  });
26300
- this.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26301
- this.persistSession();
26177
+ this.deps.history.push({ role: "tool", tool_call_id: toolCall.id, content: toolResultContent });
26178
+ this.deps.persistSession();
26302
26179
  return true;
26303
26180
  }
26304
26181
  }
26305
- const actionMap = {
26306
- ls_tool: "Reading",
26307
- read_file_lines: "Reading",
26308
- count_file_lines: "Reading",
26309
- find_by_name: "Reading",
26310
- grep_search: "Reading",
26311
- view_file_outline: "Reading",
26312
- read_artifact: "Reading",
26313
- list_agents: "Reading",
26314
- list_mcp_resources: "Reading",
26315
- read_mcp_resource: "Reading"
26316
- };
26317
26182
  const settled = await Promise.allSettled(
26318
26183
  parsed.map(async (p) => {
26319
- this.eventBus.emit("backend_message", {
26184
+ this.deps.eventBus.emit("backend_message", {
26320
26185
  type: "tool_call",
26321
26186
  tool_call_id: String(p.toolCall.id),
26322
26187
  tool_name: p.toolName,
26323
26188
  arguments: p.toolArgs,
26324
- tool_policy: this.buildToolPolicyForUi(p.toolName, p.toolArgs)
26189
+ tool_policy: this.deps.buildToolPolicyForUi(p.toolName, p.toolArgs)
26325
26190
  });
26326
26191
  setToolResultStatus(String(p.toolCall.id), "pending");
26327
26192
  setToolResultStatus(String(p.toolCall.id), "running");
26328
- const action = actionMap[p.toolName] || "Processing";
26329
- this.eventBus.emit("action_status", { action });
26330
- return this.mcpClient.invoke(p.toolName, p.toolArgs);
26193
+ this.deps.eventBus.emit("action_status", getToolActionStatus(p.toolName, p.toolArgs));
26194
+ return this.deps.mcpClient.invoke(p.toolName, p.toolArgs);
26331
26195
  })
26332
26196
  );
26333
26197
  let shouldContinue = true;
26334
- for (let k = 0; k < parsed.length; k++) {
26198
+ for (let k = 0; k < parsed.length; k += 1) {
26335
26199
  const p = parsed[k];
26336
26200
  const outcome = settled[k];
26337
26201
  let toolResultContent;
@@ -26343,7 +26207,7 @@ var BluMaAgent = class _BluMaAgent {
26343
26207
  2
26344
26208
  );
26345
26209
  setToolResultStatus(String(p.toolCall.id), "error");
26346
- this.eventBus.emit("backend_message", {
26210
+ this.deps.eventBus.emit("backend_message", {
26347
26211
  type: "error",
26348
26212
  message: `Tool "${p.toolName}" failed: ${errMsg}`
26349
26213
  });
@@ -26352,7 +26216,7 @@ var BluMaAgent = class _BluMaAgent {
26352
26216
  setToolResultStatus(String(p.toolCall.id), normalized.status);
26353
26217
  toolResultContent = toolResultToString(normalized);
26354
26218
  }
26355
- this.eventBus.emit("backend_message", {
26219
+ this.deps.eventBus.emit("backend_message", {
26356
26220
  type: "tool_result",
26357
26221
  tool_call_id: String(p.toolCall.id),
26358
26222
  tool_name: p.toolName,
@@ -26363,35 +26227,263 @@ var BluMaAgent = class _BluMaAgent {
26363
26227
  try {
26364
26228
  const resultObj = typeof toolResultContent === "string" ? JSON.parse(toolResultContent) : toolResultContent;
26365
26229
  if (resultObj.message_type === "result") {
26366
- await this.notifyFactorTurnEndIfNeeded("message_result");
26230
+ await this.deps.notifyFactorTurnEndIfNeeded("message_result");
26367
26231
  shouldContinue = false;
26368
- this.emitTurnCompleted();
26232
+ this.deps.emitTurnCompleted();
26369
26233
  }
26370
26234
  } catch {
26371
26235
  }
26372
26236
  }
26373
- this.history.push({ role: "tool", tool_call_id: p.toolCall.id, content: toolResultContent });
26237
+ this.deps.history.push({ role: "tool", tool_call_id: p.toolCall.id, content: toolResultContent });
26374
26238
  }
26375
- this.persistSession();
26239
+ this.deps.persistSession();
26376
26240
  return shouldContinue;
26377
26241
  }
26378
- async handleToolResponse(decisionData) {
26379
- const calls = decisionData.tool_calls;
26380
- if (!calls?.length) return;
26381
- const shouldContinueConversation = decisionData.continueConversation !== false;
26382
- if (!this.sessionFile) {
26383
- const [sessionFile, history, mem] = await loadOrcreateSession(this.sessionId);
26384
- this.sessionFile = sessionFile;
26385
- if (this.history.length === 0 && history.length > 0) {
26386
- this.history = history;
26387
- }
26388
- this.compressor.restoreSnapshot(mem);
26242
+ };
26243
+
26244
+ // src/app/agent/core/llm/llm_errors.ts
26245
+ function isContextWindowValidationError(error) {
26246
+ const rawMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "";
26247
+ const lower = rawMessage.toLowerCase();
26248
+ return lower.includes("vllmvalidationerror") || lower.includes("maximum context length") || lower.includes("input prompt contains at least") || lower.includes("requested 0 output tokens") || lower.includes("context length");
26249
+ }
26250
+ function formatLlmUiError(error) {
26251
+ const rawMessage = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error during LLM request.";
26252
+ const lower = rawMessage.toLowerCase();
26253
+ let message2 = "Ocorreu um erro inesperado ao contactar o modelo.";
26254
+ if (lower.includes("timeout") || lower.includes("etimedout") || lower.includes("upstream_timeout")) {
26255
+ message2 = "O BluMa demorou demasiado a responder.";
26256
+ } else if (lower.includes("connection") || lower.includes("econnrefused") || lower.includes("ehostunreach") || lower.includes("enotfound")) {
26257
+ message2 = "N\xE3o foi poss\xEDvel conectar ao servi\xE7o do modelo.";
26258
+ } else if (lower.includes("401") || lower.includes("403") || lower.includes("unauthorized") || lower.includes("forbidden")) {
26259
+ message2 = "Falha de autentica\xE7\xE3o/autoriza\xE7\xE3o ao contactar o modelo.";
26260
+ } else if (lower.includes("api")) {
26261
+ message2 = "Erro de comunica\xE7\xE3o com a API do modelo.";
26262
+ }
26263
+ return {
26264
+ message: message2,
26265
+ rawMessage
26266
+ };
26267
+ }
26268
+
26269
+ // src/app/agent/core/llm/tool_call_normalizer.ts
26270
+ import { randomUUID } from "crypto";
26271
+ var ToolCallNormalizer = class {
26272
+ /**
26273
+ * Com tool_calls e sem texto visível: content deve ser null (API OpenAI-compatible), nunca "" nem undefined.
26274
+ */
26275
+ static assistantContentWithToolCalls(content) {
26276
+ if (content === void 0 || content === null) return null;
26277
+ if (typeof content === "string") return content.trim() === "" ? null : content;
26278
+ return null;
26279
+ }
26280
+ /**
26281
+ * Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
26282
+ */
26283
+ static normalizeAssistantMessage(message2) {
26284
+ if (message2.tool_calls && this.isOpenAIFormat(message2.tool_calls)) {
26285
+ return {
26286
+ ...message2,
26287
+ content: this.assistantContentWithToolCalls(message2.content)
26288
+ };
26389
26289
  }
26390
- if (decisionData.type === "user_decision_decline") {
26391
- const declineMsg = "The system rejected this action. Verify that the command you are executing contributes to the tasks intent and try again.";
26392
- for (const toolCall of calls) {
26393
- const toolName = toolCall.function?.name || "tool";
26394
- let args = {};
26290
+ const toolCalls = this.extractToolCalls(message2);
26291
+ if (toolCalls.length > 0) {
26292
+ return {
26293
+ role: message2.role || "assistant",
26294
+ content: this.assistantContentWithToolCalls(message2.content),
26295
+ tool_calls: toolCalls
26296
+ };
26297
+ }
26298
+ return message2;
26299
+ }
26300
+ /**
26301
+ * Verifica se já está no formato OpenAI
26302
+ */
26303
+ static isOpenAIFormat(toolCalls) {
26304
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0) return false;
26305
+ const firstCall = toolCalls[0];
26306
+ return typeof firstCall.id === "string" && firstCall.type === "function" && typeof firstCall.function?.name === "string" && typeof firstCall.function?.arguments === "string";
26307
+ }
26308
+ /**
26309
+ * Extrai tool calls de diversos formatos possíveis
26310
+ */
26311
+ static extractToolCalls(message2) {
26312
+ const results = [];
26313
+ if (message2.tool_calls && Array.isArray(message2.tool_calls)) {
26314
+ for (const call of message2.tool_calls) {
26315
+ const normalized = this.normalizeToolCall(call);
26316
+ if (normalized) results.push(normalized);
26317
+ }
26318
+ }
26319
+ if (typeof message2.content === "string" && message2.content.trim()) {
26320
+ const extracted = this.extractFromContent(message2.content);
26321
+ results.push(...extracted);
26322
+ }
26323
+ if (message2.function_call) {
26324
+ const normalized = this.normalizeToolCall(message2.function_call);
26325
+ if (normalized) results.push(normalized);
26326
+ }
26327
+ return results;
26328
+ }
26329
+ /**
26330
+ * Normaliza um único tool call para o formato OpenAI
26331
+ */
26332
+ static normalizeToolCall(call) {
26333
+ try {
26334
+ if (call.id && call.function?.name) {
26335
+ return {
26336
+ id: call.id,
26337
+ type: "function",
26338
+ function: {
26339
+ name: call.function.name,
26340
+ arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments)
26341
+ }
26342
+ };
26343
+ }
26344
+ if (call.name) {
26345
+ return {
26346
+ id: call.id || randomUUID(),
26347
+ type: "function",
26348
+ function: {
26349
+ name: call.name,
26350
+ arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments || {})
26351
+ }
26352
+ };
26353
+ }
26354
+ if (call.function && typeof call.function === "object") {
26355
+ return {
26356
+ id: call.id || randomUUID(),
26357
+ type: "function",
26358
+ function: {
26359
+ name: call.function.name,
26360
+ arguments: typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments || {})
26361
+ }
26362
+ };
26363
+ }
26364
+ return null;
26365
+ } catch (error) {
26366
+ console.error("Error normalizing tool call:", error, call);
26367
+ return null;
26368
+ }
26369
+ }
26370
+ /**
26371
+ * Extrai tool calls do content (pode estar em markdown, JSON, etc)
26372
+ */
26373
+ static extractFromContent(content) {
26374
+ const results = [];
26375
+ const cleanContent2 = content.replace(/```(?:json)?\s*([\s\S]*?)```/g, "$1");
26376
+ const jsonMatches = this.extractJsonObjects(cleanContent2);
26377
+ for (const jsonStr of jsonMatches) {
26378
+ try {
26379
+ const parsed = JSON.parse(jsonStr);
26380
+ if (Array.isArray(parsed)) {
26381
+ for (const call of parsed) {
26382
+ const normalized = this.normalizeToolCall(call);
26383
+ if (normalized) results.push(normalized);
26384
+ }
26385
+ } else if (parsed.name || parsed.function) {
26386
+ const normalized = this.normalizeToolCall(parsed);
26387
+ if (normalized) results.push(normalized);
26388
+ } else if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) {
26389
+ for (const call of parsed.tool_calls) {
26390
+ const normalized = this.normalizeToolCall(call);
26391
+ if (normalized) results.push(normalized);
26392
+ }
26393
+ }
26394
+ } catch (e) {
26395
+ }
26396
+ }
26397
+ return results;
26398
+ }
26399
+ /**
26400
+ * Extrai objetos JSON de uma string (suporta múltiplos objetos)
26401
+ */
26402
+ static extractJsonObjects(text) {
26403
+ const results = [];
26404
+ let depth = 0;
26405
+ let start = -1;
26406
+ for (let i = 0; i < text.length; i++) {
26407
+ if (text[i] === "{") {
26408
+ if (depth === 0) start = i;
26409
+ depth++;
26410
+ } else if (text[i] === "}") {
26411
+ depth--;
26412
+ if (depth === 0 && start !== -1) {
26413
+ results.push(text.substring(start, i + 1));
26414
+ start = -1;
26415
+ }
26416
+ }
26417
+ }
26418
+ return results;
26419
+ }
26420
+ /**
26421
+ * Valida se um tool call normalizado é válido
26422
+ * Validação dupla:
26423
+ * 1. JSON dos argumentos é válido
26424
+ * 2. A ferramenta existe no catálogo de ferramentas nativas
26425
+ */
26426
+ static isValidToolCall(call) {
26427
+ if (!(call.id && call.type === "function" && call.function?.name && typeof call.function.arguments === "string")) {
26428
+ return false;
26429
+ }
26430
+ try {
26431
+ JSON.parse(call.function.arguments);
26432
+ } catch {
26433
+ return false;
26434
+ }
26435
+ const toolMetadata = getNativeToolMetadata(call.function.name);
26436
+ if (!toolMetadata) {
26437
+ return false;
26438
+ }
26439
+ return true;
26440
+ }
26441
+ };
26442
+
26443
+ // src/app/agent/runtime/tool_orchestration.ts
26444
+ var PARALLEL_SAFE_TOOL_NAMES = /* @__PURE__ */ new Set([
26445
+ "ls_tool",
26446
+ "read_file_lines",
26447
+ "count_file_lines",
26448
+ "find_by_name",
26449
+ "grep_search",
26450
+ "view_file_outline",
26451
+ "read_artifact",
26452
+ "list_agents",
26453
+ "list_mcp_resources",
26454
+ "read_mcp_resource",
26455
+ "todo"
26456
+ ]);
26457
+ function toolEligibleForParallelRead(toolName, sessionId) {
26458
+ const name = String(toolName || "").trim();
26459
+ if (!name || !PARALLEL_SAFE_TOOL_NAMES.has(name)) return false;
26460
+ if (!decideToolExecution(name, void 0, sessionId).autoApprove) return false;
26461
+ const meta = getNativeToolMetadata(name);
26462
+ return meta?.riskLevel === "safe";
26463
+ }
26464
+
26465
+ // src/app/agent/bluma/core/bluma_turn_coordinator.ts
26466
+ init_logger();
26467
+ var BluMaTurnCoordinator = class {
26468
+ constructor(deps) {
26469
+ this.deps = deps;
26470
+ }
26471
+ emptyAssistantReplySteps = 0;
26472
+ invalidToolCallRetrySteps = 0;
26473
+ turnLog = logger.child("turn_coordinator");
26474
+ resetTurnState() {
26475
+ this.emptyAssistantReplySteps = 0;
26476
+ this.invalidToolCallRetrySteps = 0;
26477
+ }
26478
+ async handleToolResponse(decisionData) {
26479
+ const calls = decisionData.tool_calls;
26480
+ if (!calls?.length) return;
26481
+ const shouldContinueConversation = decisionData.continueConversation !== false;
26482
+ if (decisionData.type === "user_decision_decline") {
26483
+ const declineMsg = "The system rejected this action. Verify that the command you are executing contributes to the tasks intent and try again.";
26484
+ for (const toolCall of calls) {
26485
+ const toolName = toolCall.function?.name || "tool";
26486
+ let args = {};
26395
26487
  try {
26396
26488
  if (typeof toolCall.function?.arguments === "string") {
26397
26489
  args = JSON.parse(toolCall.function.arguments);
@@ -26401,276 +26493,254 @@ var BluMaAgent = class _BluMaAgent {
26401
26493
  } catch {
26402
26494
  args = {};
26403
26495
  }
26404
- if (toolCall.id) {
26405
- setToolResultStatus(String(toolCall.id), "error");
26406
- }
26407
- this.eventBus.emit("backend_message", {
26496
+ this.deps.eventBus.emit("backend_message", {
26408
26497
  type: "tool_result",
26409
26498
  tool_call_id: String(toolCall.id),
26410
26499
  tool_name: toolName,
26411
26500
  arguments: args,
26412
26501
  result: JSON.stringify({ status: "error", error: declineMsg }, null, 2)
26413
26502
  });
26414
- this.history.push({
26503
+ this.deps.history.push({
26415
26504
  role: "tool",
26416
26505
  tool_call_id: toolCall.id,
26417
26506
  content: JSON.stringify({ status: "error", error: declineMsg }, null, 2)
26418
26507
  });
26419
26508
  }
26420
- this.persistSession();
26421
- if (shouldContinueConversation && !this.isInterrupted) {
26422
- await this._continueConversation();
26509
+ this.deps.persistSession();
26510
+ if (shouldContinueConversation && !this.deps.isInterrupted()) {
26511
+ await this.continueConversation();
26423
26512
  }
26424
26513
  return;
26425
26514
  }
26426
26515
  if (decisionData.type !== "user_decision_execute") {
26427
26516
  return;
26428
26517
  }
26429
- setTaskToolSessionContext(this.sessionId);
26430
26518
  let shouldContinue = true;
26431
26519
  let i = 0;
26432
- while (i < calls.length && shouldContinue && !this.isInterrupted) {
26520
+ while (i < calls.length && shouldContinue && !this.deps.isInterrupted()) {
26433
26521
  const name = calls[i].function.name;
26434
- if (!toolEligibleForParallelRead(name, this.sessionId)) {
26435
- shouldContinue = await this.executeApprovedToolInvocation(calls[i], decisionData);
26522
+ if (!toolEligibleForParallelRead(name, this.deps.sessionId)) {
26523
+ shouldContinue = await this.deps.toolRunner.executeApprovedToolInvocation(calls[i], decisionData);
26436
26524
  i += 1;
26437
26525
  continue;
26438
26526
  }
26439
26527
  let j = i;
26440
- while (j < calls.length && toolEligibleForParallelRead(calls[j].function.name, this.sessionId)) {
26528
+ while (j < calls.length && toolEligibleForParallelRead(calls[j].function.name, this.deps.sessionId)) {
26441
26529
  j += 1;
26442
26530
  }
26443
26531
  const slice = calls.slice(i, j);
26444
26532
  if (slice.length === 1) {
26445
- shouldContinue = await this.executeApprovedToolInvocation(slice[0], decisionData);
26533
+ shouldContinue = await this.deps.toolRunner.executeApprovedToolInvocation(slice[0], decisionData);
26446
26534
  } else {
26447
- shouldContinue = await this.executeParallelReadBatch(slice, decisionData);
26535
+ shouldContinue = await this.deps.toolRunner.executeParallelReadBatch(slice, decisionData);
26448
26536
  }
26449
26537
  i = j;
26450
26538
  }
26451
- if (shouldContinueConversation && shouldContinue && !this.isInterrupted) {
26452
- await this._continueConversation();
26539
+ if (shouldContinueConversation && shouldContinue && !this.deps.isInterrupted()) {
26540
+ await this.continueConversation();
26453
26541
  }
26454
26542
  }
26455
- async _generateEditPreview(toolArgs) {
26543
+ async continueConversation() {
26456
26544
  try {
26457
- if (Array.isArray(toolArgs.edits) && toolArgs.edits.length > 0) {
26458
- return previewEditBatch(toolArgs.edits);
26545
+ if (this.deps.isInterrupted()) {
26546
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Task Canceled." });
26547
+ return;
26459
26548
  }
26460
- if (!toolArgs.file_path) {
26461
- return "Error: file_path is required";
26549
+ const mailboxUpdate = await this.pollWorkerMailbox();
26550
+ if (mailboxUpdate && mailboxUpdate.followUp) {
26551
+ this.deps.history.push({
26552
+ role: "user",
26553
+ content: mailboxUpdate.followUp.message
26554
+ });
26555
+ this.deps.persistSession();
26556
+ this.deps.eventBus.emit("backend_message", {
26557
+ type: "info",
26558
+ message: `Received follow-up from coordinator (priority: ${mailboxUpdate.followUp.priority})`
26559
+ });
26462
26560
  }
26463
- const editData = await calculateEdit(
26464
- toolArgs.file_path,
26465
- toolArgs.old_string || "",
26466
- toolArgs.new_string || "",
26467
- toolArgs.expected_replacements || 1
26468
- );
26469
- if (editData.error) {
26470
- return `Failed to generate diff:
26471
-
26472
- ${editData.error.display}`;
26561
+ const sanitized = sanitizeConversationForProvider(this.deps.history);
26562
+ if (sanitized.issues.length > 0) {
26563
+ this.deps.eventBus.emit("backend_message", {
26564
+ type: "info",
26565
+ message: `Sanitizing ${sanitized.issues.length} messages from history`
26566
+ });
26567
+ this.deps.history.splice(0, this.deps.history.length, ...sanitized.messages);
26568
+ this.deps.persistSession();
26569
+ }
26570
+ this.turnLog.debug("Conversation sanitized", {
26571
+ history_length: this.deps.history.length,
26572
+ sanitized_length: sanitized.messages.length,
26573
+ issue_count: sanitized.issues.length,
26574
+ issues: sanitized.issues.slice(0, 5),
26575
+ history_preview: summarizeHistoryForLog(this.deps.history, 6)
26576
+ });
26577
+ const modelName = this.deps.llm.getModelName ? this.deps.llm.getModelName() : "auto";
26578
+ const baseTokenBudget = getContextInputBudgetForModel(modelName);
26579
+ const retryTokenBudget = Math.max(48e3, Math.floor(baseTokenBudget * 0.75));
26580
+ const tokenBudgets = [baseTokenBudget, retryTokenBudget];
26581
+ const llmService = this.deps.llm;
26582
+ for (let attempt = 0; attempt < tokenBudgets.length; attempt += 1) {
26583
+ const tokenBudget = tokenBudgets[attempt];
26584
+ try {
26585
+ const { messages: contextWindow, prunedHistory } = await this.deps.compressor.buildContextWindow(
26586
+ this.deps.history,
26587
+ this.deps.llm,
26588
+ this.deps.getLlmUserContext(),
26589
+ {
26590
+ tokenBudget,
26591
+ toolDefinitions: this.deps.mcpClient.getAvailableTools()
26592
+ }
26593
+ );
26594
+ this.turnLog.debug("Context window prepared", {
26595
+ history_length: this.deps.history.length,
26596
+ context_window_length: contextWindow.length,
26597
+ pruned_history_length: prunedHistory?.length ?? null,
26598
+ token_budget: tokenBudget,
26599
+ context_preview: summarizeHistoryForLog(contextWindow, 8)
26600
+ });
26601
+ if (prunedHistory) {
26602
+ this.deps.history.splice(0, this.deps.history.length, ...prunedHistory);
26603
+ }
26604
+ this.deps.persistSession();
26605
+ if (typeof llmService.chatCompletionStream === "function") {
26606
+ await this.handleStreamingResponse(contextWindow);
26607
+ } else {
26608
+ await this.handleNonStreamingResponse(contextWindow);
26609
+ }
26610
+ return;
26611
+ } catch (error) {
26612
+ const retryable = isContextWindowValidationError(error);
26613
+ const hasRetryLeft = attempt < tokenBudgets.length - 1;
26614
+ if (retryable && hasRetryLeft) {
26615
+ this.deps.eventBus.emit("backend_message", {
26616
+ type: "info",
26617
+ message: "The model rejected the prompt size. Retrying with a smaller context window."
26618
+ });
26619
+ this.deps.eventBus.emit("backend_message", {
26620
+ type: "log",
26621
+ message: "Context validation retry",
26622
+ payload: `attempt=${attempt + 1} budget=${tokenBudget} nextBudget=${tokenBudgets[attempt + 1]}`
26623
+ });
26624
+ continue;
26625
+ }
26626
+ throw error;
26627
+ }
26473
26628
  }
26474
- const filename = path40.basename(toolArgs.file_path);
26475
- return createDiff(filename, editData.currentContent || "", editData.newContent);
26476
- } catch (e) {
26477
- return `An unexpected error occurred while generating the edit preview: ${e.message}`;
26629
+ } catch (error) {
26630
+ this.deps.persistSessionSync();
26631
+ const uiError = formatLlmUiError(error);
26632
+ this.deps.eventBus.emit("backend_message", {
26633
+ type: "error",
26634
+ message: "Communication error with the BluMa"
26635
+ });
26636
+ this.deps.eventBus.emit("backend_message", {
26637
+ type: "log",
26638
+ message: "LLM request failed",
26639
+ payload: uiError.rawMessage
26640
+ });
26641
+ await this.deps.notifyFactorTurnEndIfNeeded("llm_error");
26642
+ this.deps.eventBus.emit("backend_message", {
26643
+ type: "done",
26644
+ status: "error",
26645
+ canRecover: true,
26646
+ message: "Communication error with the BluMa"
26647
+ });
26648
+ } finally {
26649
+ this.deps.persistSession();
26478
26650
  }
26479
26651
  }
26480
- _generateFileWritePreview(toolArgs) {
26481
- try {
26482
- const content = typeof toolArgs?.content === "string" ? toolArgs.content : String(toolArgs?.content ?? "");
26483
- if (!content) {
26484
- return "(No content)";
26485
- }
26486
- const normalized = content.replace(/\r\n/g, "\n");
26487
- const lines = normalized.split("\n");
26488
- const maxPreviewLines = 20;
26489
- const visibleLines = lines.slice(0, maxPreviewLines);
26490
- const hiddenLines = lines.length - visibleLines.length;
26491
- return hiddenLines > 0 ? `${visibleLines.join("\n")}
26492
- \u2026 +${hiddenLines} more lines` : normalized;
26493
- } catch (e) {
26494
- return `An unexpected error occurred while generating the file write preview: ${e.message}`;
26495
- }
26496
- }
26497
- getLlmUserContext() {
26498
- if (!this.activeTurnContext) {
26499
- throw new Error("BluMaAgent: activeTurnContext ausente (processTurn n\xE3o iniciou o turno).");
26500
- }
26501
- return this.activeTurnContext;
26502
- }
26503
- /**
26504
- * Um único POST ao Factor Router por turno (`notifyFactorRouterTurnEnd`).
26505
- * `reason` típicos: `message_result`, `user_interrupt`, `empty_reply_exhausted`, `llm_error`.
26506
- */
26507
- async notifyFactorTurnEndIfNeeded(reason) {
26508
- if (this.factorRouterTurnClosed || !this.activeTurnContext) return;
26509
- this.factorRouterTurnClosed = true;
26510
- const ctx = this.activeTurnContext;
26511
- await notifyFactorRouterTurnEnd({
26512
- turnId: ctx.turnId,
26513
- userContext: ctx,
26514
- reason
26515
- });
26516
- }
26517
- /** Fecho explícito de turno (útil para workers antes de process.exit). */
26518
- async closeActiveTurn(reason = "worker_exit") {
26519
- await this.notifyFactorTurnEndIfNeeded(reason);
26520
- }
26521
- async _continueConversation() {
26522
- try {
26523
- if (this.isInterrupted) {
26524
- this.eventBus.emit("backend_message", { type: "info", message: "Task Canceled." });
26525
- return;
26526
- }
26527
- const mailboxUpdate = await this._pollWorkerMailbox();
26528
- if (mailboxUpdate && mailboxUpdate.followUp) {
26529
- this.history.push({
26530
- role: "user",
26531
- content: mailboxUpdate.followUp.message
26532
- });
26533
- this.persistSession();
26534
- this.eventBus.emit("backend_message", {
26535
- type: "info",
26536
- message: `Received follow-up from coordinator (priority: ${mailboxUpdate.followUp.priority})`
26537
- });
26538
- }
26539
- const sanitized = sanitizeConversationForProvider(this.history);
26540
- if (sanitized.issues.length > 0) {
26541
- this.eventBus.emit("backend_message", {
26542
- type: "info",
26543
- message: `Sanitizing ${sanitized.issues.length} messages from history`
26544
- });
26545
- this.history = sanitized.messages;
26546
- this.persistSession();
26547
- }
26548
- const modelName = this.llm.getModelName ? this.llm.getModelName() : "auto";
26549
- const tokenBudget = getContextInputBudgetForModel(modelName);
26550
- const { messages: contextWindow, prunedHistory } = await this.compressor.buildContextWindow(
26551
- this.history,
26552
- this.llm,
26553
- this.getLlmUserContext(),
26554
- {
26555
- tokenBudget,
26556
- toolDefinitions: this.mcpClient.getAvailableTools()
26557
- }
26558
- );
26559
- if (prunedHistory) {
26560
- this.history = prunedHistory;
26561
- }
26562
- this.persistSession();
26563
- const llmService = this.llm;
26564
- if (typeof llmService.chatCompletionStream === "function") {
26565
- await this._handleStreamingResponse(contextWindow);
26566
- } else {
26567
- await this._handleNonStreamingResponse(contextWindow);
26568
- }
26569
- } catch (error) {
26570
- this.persistSessionSync();
26571
- this.history.push({
26572
- role: "assistant",
26573
- content: "[<=================== The conversation will continue from here ==================>]"
26574
- });
26575
- this.persistSessionSync();
26576
- const uiError = formatLlmUiError(error);
26577
- this.eventBus.emit("backend_message", {
26578
- type: "error",
26579
- message: "Communication error with the BluMa"
26580
- //details: 'The connection was temporarily interrupted. Your history has been safely saved.',
26581
- //hint: 'You can continue from where you left off. No information was lost.',
26582
- });
26583
- this.eventBus.emit("backend_message", {
26584
- type: "log",
26585
- message: "LLM request failed",
26586
- payload: uiError.rawMessage
26587
- });
26588
- await this.notifyFactorTurnEndIfNeeded("llm_error");
26589
- this.eventBus.emit("backend_message", {
26590
- type: "done",
26591
- status: "error",
26592
- canRecover: true,
26593
- message: "Communication error with the BluMa"
26594
- });
26595
- } finally {
26596
- this.persistSession();
26597
- }
26598
- }
26599
- /**
26600
- * O modelo devolveu mensagem sem texto útil nem tool_calls (só raciocínio interno).
26601
- * Nudge único + teto de tentativas para não ficar preso em "Thinking..." como no olá.
26602
- */
26603
26652
  async continueAfterEmptyAssistantResponse() {
26604
26653
  this.emptyAssistantReplySteps += 1;
26605
26654
  if (this.emptyAssistantReplySteps === 1) {
26606
- this.history.push({
26655
+ this.deps.history.push({
26607
26656
  role: "system",
26608
26657
  content: `You did not call any tool and produced no user-visible reply. Respond using the message tool with message_type "result". Keep it concise and in the user's language.`
26609
26658
  });
26610
26659
  } else if (this.emptyAssistantReplySteps === 2) {
26611
- this.history.push({
26660
+ this.deps.history.push({
26612
26661
  role: "system",
26613
26662
  content: 'Retry: call message tool with message_type "result" or use a tool.'
26614
26663
  });
26615
26664
  } else if (this.emptyAssistantReplySteps >= 6) {
26616
- this.eventBus.emit("backend_message", {
26665
+ this.deps.eventBus.emit("backend_message", {
26617
26666
  type: "info",
26618
26667
  message: "The BluMa is having difficulty processing yor task."
26619
26668
  });
26620
- await this.notifyFactorTurnEndIfNeeded("empty_reply_exhausted");
26621
- this.eventBus.emit("backend_message", { type: "done", status: "failed" });
26669
+ await this.deps.notifyFactorTurnEndIfNeeded("empty_reply_exhausted");
26670
+ this.deps.eventBus.emit("backend_message", { type: "done", status: "failed" });
26622
26671
  this.emptyAssistantReplySteps = 0;
26623
26672
  return;
26624
26673
  }
26625
- await this._continueConversation();
26674
+ await this.continueConversation();
26626
26675
  }
26627
- /** Paridade com streaming: texto + tools visíveis na área `StreamingText` antes dos `tool_call` definitivos. */
26628
- emitSyntheticStreamForNonStreamingMessage(message2) {
26629
- const content = typeof message2.content === "string" ? message2.content : "";
26630
- const hasTools = Array.isArray(message2.tool_calls) && message2.tool_calls.length > 0;
26631
- const hasContent = content.trim().length > 0;
26632
- if (!hasContent && !hasTools) {
26633
- return;
26676
+ async handleInvalidToolCallRetry(message2) {
26677
+ this.invalidToolCallRetrySteps += 1;
26678
+ if (this.deps.history[this.deps.history.length - 1] === message2) {
26679
+ this.deps.history.pop();
26634
26680
  }
26635
- this.eventBus.emit("stream_start", {});
26636
- if (hasContent) {
26637
- this.eventBus.emit("stream_chunk", { delta: content });
26681
+ if (this.invalidToolCallRetrySteps >= 3) {
26682
+ this.deps.eventBus.emit("backend_message", {
26683
+ type: "error",
26684
+ message: "The model kept returning invalid tool calls. Closing the turn to avoid a retry loop."
26685
+ });
26686
+ this.deps.eventBus.emit("backend_message", {
26687
+ type: "log",
26688
+ message: "Invalid tool call retry limit reached",
26689
+ payload: String(this.invalidToolCallRetrySteps)
26690
+ });
26691
+ await this.deps.notifyFactorTurnEndIfNeeded("invalid_tool_calls_exhausted");
26692
+ this.deps.eventBus.emit("backend_message", { type: "done", status: "failed" });
26693
+ this.invalidToolCallRetrySteps = 0;
26694
+ return;
26638
26695
  }
26639
- const omitAssistantFlush = hasTools && message2.tool_calls.some((tc) => String(tc?.function?.name ?? "").includes("message"));
26640
- this.eventBus.emit("stream_end", { omitAssistantFlush });
26696
+ this.deps.history.push({
26697
+ role: "system",
26698
+ content: "Previous assistant tool_calls were invalid. Retry with valid JSON arguments only, or answer without tools."
26699
+ });
26700
+ this.deps.persistSession();
26701
+ await this.continueConversation();
26641
26702
  }
26642
- async _handleStreamingResponse(contextWindow) {
26643
- const llmService = this.llm;
26703
+ async handleStreamingResponse(contextWindow) {
26704
+ const llmService = this.deps.llm;
26644
26705
  let accumulatedContent = "";
26645
26706
  let toolCalls;
26646
26707
  let hasEmittedStart = false;
26708
+ let currentStreamPhase = "idle";
26647
26709
  const stream = llmService.chatCompletionStream({
26648
26710
  messages: contextWindow,
26649
26711
  temperature: 0,
26650
- tools: this.mcpClient.getAvailableTools(),
26712
+ tools: this.deps.mcpClient.getAvailableTools(),
26651
26713
  parallel_tool_calls: true,
26652
- userContext: this.getLlmUserContext()
26714
+ userContext: this.deps.getLlmUserContext()
26653
26715
  });
26654
26716
  for await (const chunk of stream) {
26655
- if (this.isInterrupted) {
26656
- this.eventBus.emit("stream_end", {});
26657
- this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26717
+ if (this.deps.isInterrupted()) {
26718
+ this.deps.eventBus.emit("stream_end", {});
26719
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26658
26720
  return;
26659
26721
  }
26660
26722
  if (chunk.reasoning) {
26661
26723
  if (!hasEmittedStart) {
26662
- this.eventBus.emit("stream_start", {});
26724
+ this.deps.eventBus.emit("stream_start", {});
26663
26725
  hasEmittedStart = true;
26664
26726
  }
26665
- this.eventBus.emit("stream_reasoning_chunk", { delta: chunk.reasoning });
26727
+ if (currentStreamPhase !== "reasoning") {
26728
+ this.deps.eventBus.emit("action_status", getReasoningActionStatus());
26729
+ currentStreamPhase = "reasoning";
26730
+ }
26731
+ this.deps.eventBus.emit("stream_reasoning_chunk", { delta: chunk.reasoning });
26666
26732
  }
26667
26733
  if (chunk.delta) {
26668
26734
  if (!hasEmittedStart) {
26669
- this.eventBus.emit("stream_start", {});
26735
+ this.deps.eventBus.emit("stream_start", {});
26670
26736
  hasEmittedStart = true;
26671
26737
  }
26738
+ if (currentStreamPhase !== "responding") {
26739
+ this.deps.eventBus.emit("action_status", getRespondingActionStatus());
26740
+ currentStreamPhase = "responding";
26741
+ }
26672
26742
  accumulatedContent += chunk.delta;
26673
- this.eventBus.emit("stream_chunk", { delta: chunk.delta });
26743
+ this.deps.eventBus.emit("stream_chunk", { delta: chunk.delta });
26674
26744
  }
26675
26745
  if (chunk.tool_calls && chunk.tool_calls.length > 0) {
26676
26746
  toolCalls = chunk.tool_calls;
@@ -26678,14 +26748,14 @@ ${editData.error.display}`;
26678
26748
  const toolName = tc?.function?.name ?? "";
26679
26749
  const argsRaw = tc?.function?.arguments ?? "{}";
26680
26750
  if (toolName) {
26681
- this.eventBus.emit("tool_stream_start", { toolName, argsRaw });
26751
+ this.deps.eventBus.emit("tool_stream_start", { toolName, argsRaw });
26682
26752
  }
26683
26753
  }
26684
26754
  }
26685
26755
  }
26686
26756
  const omitAssistantFlush = Array.isArray(toolCalls) && toolCalls.some((tc) => String(tc?.function?.name ?? "").includes("message"));
26687
26757
  if (hasEmittedStart) {
26688
- this.eventBus.emit("stream_end", { omitAssistantFlush });
26758
+ this.deps.eventBus.emit("stream_end", { omitAssistantFlush });
26689
26759
  }
26690
26760
  const trimmedText = accumulatedContent.trim();
26691
26761
  const hasToolCalls = Boolean(toolCalls && toolCalls.length > 0);
@@ -26698,7 +26768,7 @@ ${editData.error.display}`;
26698
26768
  message2.tool_calls = toolCalls;
26699
26769
  }
26700
26770
  const normalizedMessage = ToolCallNormalizer.normalizeAssistantMessage(message2);
26701
- this.history.push(normalizedMessage);
26771
+ this.deps.history.push(normalizedMessage);
26702
26772
  if (normalizedMessage.tool_calls && normalizedMessage.tool_calls.length > 0) {
26703
26773
  this.emptyAssistantReplySteps = 0;
26704
26774
  this.invalidToolCallRetrySteps = 0;
@@ -26710,10 +26780,10 @@ ${editData.error.display}`;
26710
26780
  return;
26711
26781
  }
26712
26782
  const autoApprovedToolCalls = validToolCalls.filter(
26713
- (tc) => effectiveToolAutoApprove(tc, this.sessionId)
26783
+ (tc) => effectiveToolAutoApprove(tc, this.deps.sessionId)
26714
26784
  );
26715
26785
  const confirmationToolCalls = validToolCalls.filter(
26716
- (tc) => !effectiveToolAutoApprove(tc, this.sessionId)
26786
+ (tc) => !effectiveToolAutoApprove(tc, this.deps.sessionId)
26717
26787
  );
26718
26788
  if (autoApprovedToolCalls.length > 0) {
26719
26789
  await this.handleToolResponse({
@@ -26724,17 +26794,17 @@ ${editData.error.display}`;
26724
26794
  }
26725
26795
  if (confirmationToolCalls.length > 0) {
26726
26796
  const toolToCall = confirmationToolCalls[0];
26727
- const decision = decideToolExecution(toolToCall.function.name, void 0, this.sessionId);
26797
+ const decision = decideToolExecution(toolToCall.function.name, void 0, this.deps.sessionId);
26728
26798
  const toolName = toolToCall.function.name;
26729
26799
  let previewContent;
26730
26800
  if (toolName === "edit_tool") {
26731
26801
  const args = JSON.parse(toolToCall.function.arguments);
26732
- previewContent = await this._generateEditPreview(args);
26802
+ previewContent = await this.deps.generateEditPreview(args);
26733
26803
  } else if (toolName === "file_write") {
26734
26804
  const args = JSON.parse(toolToCall.function.arguments);
26735
- previewContent = this._generateFileWritePreview(args);
26805
+ previewContent = this.deps.generateFileWritePreview(args);
26736
26806
  }
26737
- this.eventBus.emit("backend_message", {
26807
+ this.deps.eventBus.emit("backend_message", {
26738
26808
  type: "confirmation_request",
26739
26809
  tool_calls: confirmationToolCalls,
26740
26810
  preview: previewContent,
@@ -26743,37 +26813,37 @@ ${editData.error.display}`;
26743
26813
  }
26744
26814
  } else if (trimmedText) {
26745
26815
  this.emptyAssistantReplySteps = 0;
26746
- this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
26747
- await this.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
26748
- this.emitTurnCompleted();
26816
+ this.invalidToolCallRetrySteps = 0;
26817
+ this.deps.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
26818
+ await this.deps.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
26819
+ this.deps.emitTurnCompleted();
26749
26820
  return;
26750
26821
  } else {
26751
26822
  await this.continueAfterEmptyAssistantResponse();
26752
26823
  }
26753
26824
  }
26754
- async _handleNonStreamingResponse(contextWindow) {
26755
- const response = await this.llm.chatCompletion({
26825
+ async handleNonStreamingResponse(contextWindow) {
26826
+ const response = await this.deps.llm.chatCompletion({
26756
26827
  messages: contextWindow,
26757
26828
  temperature: 0,
26758
- tools: this.mcpClient.getAvailableTools(),
26829
+ tools: this.deps.mcpClient.getAvailableTools(),
26759
26830
  parallel_tool_calls: true,
26760
- userContext: this.getLlmUserContext()
26831
+ userContext: this.deps.getLlmUserContext()
26761
26832
  });
26762
- if (this.isInterrupted) {
26763
- this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26833
+ if (this.deps.isInterrupted()) {
26834
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26764
26835
  return;
26765
26836
  }
26766
26837
  let message2 = response.choices[0].message;
26767
26838
  message2 = ToolCallNormalizer.normalizeAssistantMessage(message2);
26768
26839
  if (message2.reasoning_content || message2.reasoning) {
26769
26840
  const reasoningText = message2.reasoning_content || message2.reasoning;
26770
- this.eventBus.emit("backend_message", {
26841
+ this.deps.eventBus.emit("backend_message", {
26771
26842
  type: "reasoning",
26772
26843
  content: typeof reasoningText === "string" ? reasoningText : JSON.stringify(reasoningText)
26773
26844
  });
26774
26845
  }
26775
- this.emitSyntheticStreamForNonStreamingMessage(message2);
26776
- this.history.push(message2);
26846
+ this.deps.history.push(message2);
26777
26847
  if (message2.tool_calls && message2.tool_calls.length > 0) {
26778
26848
  this.emptyAssistantReplySteps = 0;
26779
26849
  this.invalidToolCallRetrySteps = 0;
@@ -26785,10 +26855,10 @@ ${editData.error.display}`;
26785
26855
  return;
26786
26856
  }
26787
26857
  const autoApprovedToolCalls = validToolCalls.filter(
26788
- (tc) => effectiveToolAutoApprove(tc, this.sessionId)
26858
+ (tc) => effectiveToolAutoApprove(tc, this.deps.sessionId)
26789
26859
  );
26790
26860
  const confirmationToolCalls = validToolCalls.filter(
26791
- (tc) => !effectiveToolAutoApprove(tc, this.sessionId)
26861
+ (tc) => !effectiveToolAutoApprove(tc, this.deps.sessionId)
26792
26862
  );
26793
26863
  if (autoApprovedToolCalls.length > 0) {
26794
26864
  await this.handleToolResponse({
@@ -26799,33 +26869,420 @@ ${editData.error.display}`;
26799
26869
  }
26800
26870
  if (confirmationToolCalls.length > 0) {
26801
26871
  const toolToCall = confirmationToolCalls[0];
26802
- const decision = decideToolExecution(toolToCall.function.name, void 0, this.sessionId);
26872
+ const decision = decideToolExecution(toolToCall.function.name, void 0, this.deps.sessionId);
26803
26873
  const toolName = toolToCall.function.name;
26804
26874
  let previewContent;
26805
26875
  if (toolName === "edit_tool") {
26806
26876
  const args = JSON.parse(toolToCall.function.arguments);
26807
- previewContent = await this._generateEditPreview(args);
26877
+ previewContent = await this.deps.generateEditPreview(args);
26808
26878
  } else if (toolName === "file_write") {
26809
26879
  const args = JSON.parse(toolToCall.function.arguments);
26810
- previewContent = this._generateFileWritePreview(args);
26880
+ previewContent = this.deps.generateFileWritePreview(args);
26811
26881
  }
26812
- this.eventBus.emit("backend_message", {
26882
+ this.deps.eventBus.emit("backend_message", {
26813
26883
  type: "confirmation_request",
26814
26884
  tool_calls: confirmationToolCalls,
26815
26885
  preview: previewContent,
26816
26886
  tool_policy: decision
26817
26887
  });
26818
26888
  }
26819
- } else if (typeof message2.content === "string" && message2.content.trim()) {
26820
- this.emptyAssistantReplySteps = 0;
26821
- this.invalidToolCallRetrySteps = 0;
26822
- this.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
26823
- await this.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
26824
- this.emitTurnCompleted();
26825
- return;
26826
- } else {
26827
- await this.continueAfterEmptyAssistantResponse();
26889
+ } else if (typeof message2.content === "string" && message2.content.trim()) {
26890
+ this.emptyAssistantReplySteps = 0;
26891
+ this.invalidToolCallRetrySteps = 0;
26892
+ this.deps.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
26893
+ await this.deps.notifyFactorTurnEndIfNeeded("assistant_text_without_tool_call");
26894
+ this.deps.emitTurnCompleted();
26895
+ return;
26896
+ } else {
26897
+ await this.continueAfterEmptyAssistantResponse();
26898
+ }
26899
+ }
26900
+ async pollWorkerMailbox() {
26901
+ if (!process.env.BLUMA_PARENT_SESSION_ID) {
26902
+ return null;
26903
+ }
26904
+ try {
26905
+ const { pollMailbox: pollMailbox2 } = await import("../../tools/natives/poll_mailbox.js");
26906
+ const result = await pollMailbox2({
26907
+ timeout: 0,
26908
+ pollInterval: 500,
26909
+ types: ["follow_up", "cancel_request", "permission_response"],
26910
+ includeSignals: false
26911
+ });
26912
+ if (!result.success || !result.hasNewMessages) {
26913
+ return null;
26914
+ }
26915
+ return {
26916
+ followUp: result.followUp ? {
26917
+ message: result.followUp.message,
26918
+ priority: result.followUp.priority
26919
+ } : void 0,
26920
+ cancelRequested: result.cancelRequested
26921
+ };
26922
+ } catch {
26923
+ return null;
26924
+ }
26925
+ }
26926
+ };
26927
+ function summarizeHistoryForLog(messages, limit = 8) {
26928
+ return messages.slice(0, limit).map((message2) => {
26929
+ const content = message2?.content;
26930
+ const contentType = Array.isArray(content) ? "array" : typeof content;
26931
+ const preview = typeof content === "string" ? content.slice(0, 120) : Array.isArray(content) ? content.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => part.text).join(" ").slice(0, 120) : void 0;
26932
+ return {
26933
+ role: message2?.role,
26934
+ name: message2?.name ?? null,
26935
+ content_type: contentType,
26936
+ preview: preview || null,
26937
+ has_tool_calls: Array.isArray(message2?.tool_calls) && message2.tool_calls.length > 0,
26938
+ tool_call_id: message2?.tool_call_id ?? null
26939
+ };
26940
+ });
26941
+ }
26942
+
26943
+ // src/app/agent/bluma/core/bluma.ts
26944
+ var BluMaAgent = class {
26945
+ llm;
26946
+ sessionId;
26947
+ sessionFile = "";
26948
+ history = [];
26949
+ eventBus;
26950
+ mcpClient;
26951
+ feedbackSystem;
26952
+ skillLoader;
26953
+ compressor;
26954
+ toolRunner;
26955
+ turnCoordinator;
26956
+ isInterrupted = false;
26957
+ /** Mesmo turnId durante processTurn + todo o loop de tool_calls (FactorRouter). */
26958
+ activeTurnContext = null;
26959
+ /** Evita POST /turns/.../end duplicado no mesmo turno (ex.: Esc após message_result). */
26960
+ factorRouterTurnClosed = false;
26961
+ constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
26962
+ this.sessionId = sessionId;
26963
+ this.eventBus = eventBus;
26964
+ this.llm = llm;
26965
+ this.mcpClient = mcpClient;
26966
+ this.feedbackSystem = feedbackSystem;
26967
+ this.skillLoader = new SkillLoader(process.cwd());
26968
+ this.compressor = new HistoryCompressor();
26969
+ this.toolRunner = new BluMaToolRunner({
26970
+ eventBus: this.eventBus,
26971
+ history: this.history,
26972
+ sessionId: this.sessionId,
26973
+ mcpClient: this.mcpClient,
26974
+ isInterrupted: () => this.isInterrupted,
26975
+ persistSession: () => this.persistSession(),
26976
+ notifyFactorTurnEndIfNeeded: (reason) => this.notifyFactorTurnEndIfNeeded(reason),
26977
+ emitTurnCompleted: () => this.emitTurnCompleted(),
26978
+ buildToolPolicyForUi: (toolName, toolArgs) => this.buildToolPolicyForUi(toolName, toolArgs),
26979
+ generateEditPreview: (toolArgs) => this._generateEditPreview(toolArgs),
26980
+ generateFileWritePreview: (toolArgs) => this._generateFileWritePreview(toolArgs)
26981
+ });
26982
+ this.turnCoordinator = new BluMaTurnCoordinator({
26983
+ eventBus: this.eventBus,
26984
+ history: this.history,
26985
+ sessionId: this.sessionId,
26986
+ llm: this.llm,
26987
+ mcpClient: this.mcpClient,
26988
+ compressor: this.compressor,
26989
+ toolRunner: this.toolRunner,
26990
+ isInterrupted: () => this.isInterrupted,
26991
+ getLlmUserContext: () => this.getLlmUserContext(),
26992
+ persistSession: () => this.persistSession(),
26993
+ persistSessionSync: () => this.persistSessionSync(),
26994
+ notifyFactorTurnEndIfNeeded: (reason) => this.notifyFactorTurnEndIfNeeded(reason),
26995
+ emitTurnCompleted: () => this.emitTurnCompleted(),
26996
+ generateEditPreview: (toolArgs) => this._generateEditPreview(toolArgs),
26997
+ generateFileWritePreview: (toolArgs) => this._generateFileWritePreview(toolArgs)
26998
+ });
26999
+ this.eventBus.on("user_interrupt", async () => {
27000
+ this.isInterrupted = true;
27001
+ await this.notifyFactorTurnEndIfNeeded("user_interrupt");
27002
+ this.eventBus.emit("backend_message", { type: "done", status: "interrupted" });
27003
+ });
27004
+ this.eventBus.on("user_overlay", async (data) => {
27005
+ const clean = String(data.payload ?? "").trim();
27006
+ this.history.push({ role: "user", content: clean });
27007
+ this.eventBus.emit("backend_message", { type: "user_overlay", payload: clean, ts: data.ts || Date.now() });
27008
+ try {
27009
+ if (this.sessionFile) {
27010
+ this.persistSession();
27011
+ } else {
27012
+ const [sessionFile, , mem] = await loadOrcreateSession(this.sessionId);
27013
+ this.sessionFile = sessionFile;
27014
+ this.compressor.restoreSnapshot(mem);
27015
+ this.persistSession();
27016
+ }
27017
+ } catch (e) {
27018
+ this.eventBus.emit("backend_message", { type: "error", message: `Falha ao salvar hist\xF3rico ap\xF3s user_overlay: ${e.message}` });
27019
+ }
27020
+ });
27021
+ }
27022
+ getMemorySnapshot() {
27023
+ return this.compressor.getSnapshot();
27024
+ }
27025
+ /**
27026
+ * Salvar histórico de forma SÍNCRONA - usado em situações críticas (erros, shutdown)
27027
+ * para garantir que nada se perde. NÃO usa debounce.
27028
+ */
27029
+ persistSessionSync() {
27030
+ if (!this.sessionFile) return;
27031
+ try {
27032
+ const sessionData = {
27033
+ session_id: path40.basename(this.sessionFile, ".json"),
27034
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
27035
+ conversation_history: this.history,
27036
+ last_updated: (/* @__PURE__ */ new Date()).toISOString(),
27037
+ ...this.compressor.getSnapshot()
27038
+ };
27039
+ fs39.writeFileSync(this.sessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
27040
+ } catch (error) {
27041
+ console.error("[Bluma] Failed to persist session synchronously:", error);
27042
+ }
27043
+ }
27044
+ /** Debounced: grava histórico + estado de compressão no mesmo ficheiro de sessão. */
27045
+ persistSession() {
27046
+ if (!this.sessionFile) return;
27047
+ void saveSessionHistory(this.sessionFile, this.history, this.getMemorySnapshot());
27048
+ }
27049
+ recordUiSlashCommand(command, mode = "visible_only") {
27050
+ const text = String(command ?? "").trim();
27051
+ if (!text) return;
27052
+ this.history.push({
27053
+ role: "user",
27054
+ name: mode === "with_internal_prompt" ? "ui_slash_command" : "ui_slash_command_local",
27055
+ content: text
27056
+ });
27057
+ this.persistSession();
27058
+ }
27059
+ async initialize() {
27060
+ await this.mcpClient.nativeToolInvoker.initialize();
27061
+ await this.mcpClient.initialize();
27062
+ const [sessionFile, history, mem] = await loadOrcreateSession(this.sessionId);
27063
+ this.sessionFile = sessionFile;
27064
+ this.history.splice(0, this.history.length, ...history);
27065
+ this.compressor.restoreSnapshot(mem);
27066
+ initializeSkillContext({
27067
+ history: this.history,
27068
+ skillLoader: this.skillLoader
27069
+ });
27070
+ registerSessionCronBridge({
27071
+ eventBus: this.eventBus,
27072
+ getSessionId: () => this.sessionId
27073
+ });
27074
+ const dirs = this.skillLoader.getSkillsDirs();
27075
+ this.eventBus.emit("backend_message", {
27076
+ type: "info",
27077
+ message: `Skills dirs \u2014 bundled: ${dirs.bundled} | project: ${dirs.project} | global: ${dirs.global}`
27078
+ });
27079
+ const availableSkills = this.skillLoader.listAvailable();
27080
+ this.eventBus.emit("backend_message", {
27081
+ type: "info",
27082
+ message: `Skills loaded: ${availableSkills.length} \u2014 ${availableSkills.map((s) => `${s.name} (${s.source})`).join(", ") || "none"}`
27083
+ });
27084
+ if (this.skillLoader.hasConflicts()) {
27085
+ for (const warning of this.skillLoader.formatConflictWarnings()) {
27086
+ this.eventBus.emit("backend_message", {
27087
+ type: "warning",
27088
+ message: warning
27089
+ });
27090
+ }
27091
+ }
27092
+ const toolsDetailed = this.mcpClient.getAvailableToolsDetailed();
27093
+ const availableToolNames = toolsDetailed.map((t) => t.function?.name).filter(Boolean);
27094
+ const systemPrompt = await getUnifiedSystemPrompt(availableSkills, {
27095
+ availableToolNames,
27096
+ toolsDetailed,
27097
+ mcpClient: this.mcpClient,
27098
+ sessionId: this.sessionId
27099
+ });
27100
+ if (this.history.length === 0) {
27101
+ this.history.push({ role: "system", content: systemPrompt });
27102
+ } else {
27103
+ const sysIdx = this.history.findIndex(
27104
+ (m) => m.role === "system" && typeof m.content === "string" && String(m.content).includes("<identity>")
27105
+ );
27106
+ if (sysIdx >= 0) {
27107
+ this.history[sysIdx] = { ...this.history[sysIdx], content: systemPrompt };
27108
+ } else {
27109
+ this.history.unshift({ role: "system", content: systemPrompt });
27110
+ }
27111
+ }
27112
+ this.persistSession();
27113
+ const uiHistory = this.history.filter(
27114
+ (m) => m.role !== "system"
27115
+ );
27116
+ if (uiHistory.length > 0) {
27117
+ this.eventBus.emit("backend_message", {
27118
+ type: "session_history",
27119
+ messages: uiHistory
27120
+ });
27121
+ }
27122
+ }
27123
+ getAvailableTools() {
27124
+ return this.mcpClient.getAvailableTools();
27125
+ }
27126
+ getUiToolsDetailed() {
27127
+ return this.mcpClient.getAvailableToolsDetailed();
27128
+ }
27129
+ listAvailableSkills() {
27130
+ return this.skillLoader.listAvailable();
27131
+ }
27132
+ getSkillsDirs() {
27133
+ return this.skillLoader.getSkillsDirs();
27134
+ }
27135
+ getSkillConflictWarnings() {
27136
+ return this.skillLoader.formatConflictWarnings();
27137
+ }
27138
+ getFeedbackScore() {
27139
+ return this.feedbackSystem.getCumulativeScore();
27140
+ }
27141
+ /**
27142
+ * Contrato de ciclo de turno (UI, hooks, Factor Router):
27143
+ *
27144
+ * - `backend_message` `{ type: 'turn_start', turnId, sessionId, userPromptPreview }` — início;
27145
+ * payload estável em {@link buildTurnStartBackendMessage}.
27146
+ * - Stream: `stream_start` / `stream_reasoning_chunk` / `stream_chunk` / `stream_end`.
27147
+ * Se a resposta incluir tool `message`, `stream_end` leva `{ omitAssistantFlush: true }` para não
27148
+ * duplicar texto no histórico (o utilizador vê o `tool_result` com gutter). Ver `applyStreamEndFlush`.
27149
+ * - Fim: `backend_message` `{ type: 'done', status }` (`completed` | `failed` | `interrupted` | …).
27150
+ * - Factor Router: um POST `/turns/{turnId}/end` por turno via `notifyFactorTurnEndIfNeeded` (razões abaixo).
27151
+ *
27152
+ * Smoke manual sugerido: mensagem normal com `message`+`result`; Ctrl+C; erro LLM; loop sem resposta útil.
27153
+ */
27154
+ async processTurn(userInput, userContextInput, options) {
27155
+ this.isInterrupted = false;
27156
+ this.factorRouterTurnClosed = false;
27157
+ this.turnCoordinator.resetTurnState();
27158
+ const inputText = String(userInput.content || "").trim();
27159
+ const turnId = uuidv48();
27160
+ this.activeTurnContext = {
27161
+ ...userContextInput,
27162
+ turnId,
27163
+ sessionId: userContextInput.sessionId || this.sessionId
27164
+ };
27165
+ process.env.BLUMA_USER_CONTEXT_JSON = JSON.stringify({
27166
+ conversationId: this.activeTurnContext.conversationId ?? null,
27167
+ userId: this.activeTurnContext.userId ?? null,
27168
+ userName: this.activeTurnContext.userName ?? null,
27169
+ userEmail: this.activeTurnContext.userEmail ?? null,
27170
+ companyId: this.activeTurnContext.companyId ?? null,
27171
+ companyName: this.activeTurnContext.companyName ?? null
27172
+ });
27173
+ const userContent = buildUserMessageContent(inputText, process.cwd());
27174
+ this.history.push({
27175
+ role: "user",
27176
+ content: userContent,
27177
+ ...options?.historyName ? { name: options.historyName } : {}
27178
+ });
27179
+ this.eventBus.emit(
27180
+ "backend_message",
27181
+ buildTurnStartBackendMessage({
27182
+ turnId: this.activeTurnContext.turnId,
27183
+ sessionId: this.activeTurnContext.sessionId,
27184
+ inputText,
27185
+ appContext: this.activeTurnContext.appContext ?? null
27186
+ })
27187
+ );
27188
+ if (inputText === "/init") {
27189
+ this.eventBus.emit("dispatch", inputText);
27190
+ }
27191
+ await this.turnCoordinator.continueConversation();
27192
+ }
27193
+ /** Política mostrada na UI: combina metadata base com auto-approve efectivo (incl. permission_ml). */
27194
+ buildToolPolicyForUi(toolName, toolArgs) {
27195
+ const base = decideToolExecution(toolName, void 0, this.sessionId);
27196
+ const argsStr = typeof toolArgs === "string" ? toolArgs : JSON.stringify(toolArgs ?? {});
27197
+ const synthetic = { function: { name: toolName, arguments: argsStr } };
27198
+ const auto = effectiveToolAutoApprove(synthetic, this.sessionId, { logClassifier: false });
27199
+ return {
27200
+ ...base,
27201
+ autoApprove: auto,
27202
+ requiresConfirmation: !auto
27203
+ };
27204
+ }
27205
+ /**
27206
+ * Executa uma tool aprovada: emit tool_call → invoke → tool_result → histórico.
27207
+ * `backend_message` tool_call / tool_result incluem `tool_call_id` (id do tool call no modelo)
27208
+ * para a UI correlacionar a linha de invocação (•) com o resultado compacto.
27209
+ * @returns shouldContinue — false se `message` com message_type result terminou o turno.
27210
+ */
27211
+ async executeApprovedToolInvocation(toolCall, decisionData) {
27212
+ return this.toolRunner.executeApprovedToolInvocation(toolCall, decisionData);
27213
+ }
27214
+ /** Várias leituras seguras em paralelo (grep, ls, read_file, …). */
27215
+ async executeParallelReadBatch(batch, _decisionData) {
27216
+ return this.toolRunner.executeParallelReadBatch(batch, _decisionData);
27217
+ }
27218
+ async handleToolResponse(decisionData) {
27219
+ return this.turnCoordinator.handleToolResponse(decisionData);
27220
+ }
27221
+ async _generateEditPreview(toolArgs) {
27222
+ try {
27223
+ if (Array.isArray(toolArgs.edits) && toolArgs.edits.length > 0) {
27224
+ return previewEditBatch(toolArgs.edits);
27225
+ }
27226
+ if (!toolArgs.file_path) {
27227
+ return "Error: file_path is required";
27228
+ }
27229
+ const editData = await calculateEdit(
27230
+ toolArgs.file_path,
27231
+ toolArgs.old_string || "",
27232
+ toolArgs.new_string || "",
27233
+ toolArgs.expected_replacements || 1
27234
+ );
27235
+ if (editData.error) {
27236
+ return `Failed to generate diff:
27237
+
27238
+ ${editData.error.display}`;
27239
+ }
27240
+ const filename = path40.basename(toolArgs.file_path);
27241
+ return createDiff(filename, editData.currentContent || "", editData.newContent);
27242
+ } catch (e) {
27243
+ return `An unexpected error occurred while generating the edit preview: ${e.message}`;
27244
+ }
27245
+ }
27246
+ _generateFileWritePreview(toolArgs) {
27247
+ try {
27248
+ const content = typeof toolArgs?.content === "string" ? toolArgs.content : String(toolArgs?.content ?? "");
27249
+ if (!content) {
27250
+ return "(No content)";
27251
+ }
27252
+ const normalized = content.replace(/\r\n/g, "\n");
27253
+ const lines = normalized.split("\n");
27254
+ const maxPreviewLines = 20;
27255
+ const visibleLines = lines.slice(0, maxPreviewLines);
27256
+ const hiddenLines = lines.length - visibleLines.length;
27257
+ return hiddenLines > 0 ? `${visibleLines.join("\n")}
27258
+ \u2026 +${hiddenLines} more lines` : normalized;
27259
+ } catch (e) {
27260
+ return `An unexpected error occurred while generating the file write preview: ${e.message}`;
27261
+ }
27262
+ }
27263
+ getLlmUserContext() {
27264
+ if (!this.activeTurnContext) {
27265
+ throw new Error("BluMaAgent: activeTurnContext ausente (processTurn n\xE3o iniciou o turno).");
26828
27266
  }
27267
+ return this.activeTurnContext;
27268
+ }
27269
+ /**
27270
+ * Um único POST ao Factor Router por turno (`notifyFactorRouterTurnEnd`).
27271
+ * `reason` típicos: `message_result`, `user_interrupt`, `empty_reply_exhausted`, `llm_error`.
27272
+ */
27273
+ async notifyFactorTurnEndIfNeeded(reason) {
27274
+ if (this.factorRouterTurnClosed || !this.activeTurnContext) return;
27275
+ this.factorRouterTurnClosed = true;
27276
+ const ctx = this.activeTurnContext;
27277
+ await notifyFactorRouterTurnEnd({
27278
+ turnId: ctx.turnId,
27279
+ userContext: ctx,
27280
+ reason
27281
+ });
27282
+ }
27283
+ /** Fecho explícito de turno (útil para workers antes de process.exit). */
27284
+ async closeActiveTurn(reason = "worker_exit") {
27285
+ await this.notifyFactorTurnEndIfNeeded(reason);
26829
27286
  }
26830
27287
  emitTurnCompleted() {
26831
27288
  this.eventBus.emit("backend_message", { type: "done", status: "completed" });
@@ -26838,37 +27295,6 @@ ${editData.error.display}`;
26838
27295
  } catch {
26839
27296
  }
26840
27297
  }
26841
- /**
26842
- * WORKER: Poll mailbox para checkar follow-ups do coordinator
26843
- * Retorna null se não há novas mensagens ou se não é um worker
26844
- */
26845
- async _pollWorkerMailbox() {
26846
- if (!process.env.BLUMA_PARENT_SESSION_ID) {
26847
- return null;
26848
- }
26849
- try {
26850
- const { pollMailbox: pollMailbox2 } = await import("../../tools/natives/poll_mailbox.js");
26851
- const result = await pollMailbox2({
26852
- timeout: 0,
26853
- // Non-blocking
26854
- pollInterval: 500,
26855
- types: ["follow_up", "cancel_request", "permission_response"],
26856
- includeSignals: false
26857
- });
26858
- if (!result.success || !result.hasNewMessages) {
26859
- return null;
26860
- }
26861
- return {
26862
- followUp: result.followUp ? {
26863
- message: result.followUp.message,
26864
- priority: result.followUp.priority
26865
- } : void 0,
26866
- cancelRequested: result.cancelRequested
26867
- };
26868
- } catch (error) {
26869
- return null;
26870
- }
26871
- }
26872
27298
  };
26873
27299
 
26874
27300
  // src/app/agent/subagents/registry.ts
@@ -28482,7 +28908,7 @@ function ChatUserImageBlock({
28482
28908
  if (imageCount < 1) return null;
28483
28909
  const cap = caption?.trim() ?? "";
28484
28910
  const capLines = cap.length > 0 ? cap.split("\n") : [];
28485
- return /* @__PURE__ */ jsxs4(Box_default, { flexDirection: "column", marginBottom: 0, children: [
28911
+ return /* @__PURE__ */ jsxs4(Box_default, { flexDirection: "column", children: [
28486
28912
  Array.from({ length: imageCount }, (_, i) => /* @__PURE__ */ jsx11(Text, { color: BLUMA_TERMINAL.blue, children: `[IMAGE #${i + 1}]` }, `img-${i}`)),
28487
28913
  capLines.map((line, i) => /* @__PURE__ */ jsxs4(Box_default, { flexDirection: "row", flexWrap: "wrap", children: [
28488
28914
  /* @__PURE__ */ jsx11(Text, { color: BLUMA_TERMINAL.subtle, children: i === 0 ? " L " : " " }),
@@ -28656,15 +29082,14 @@ var HighlightedCodeComponent = ({ code, filePath, maxLines }) => {
28656
29082
  const visible = maxLines ? lines.slice(0, maxLines) : lines;
28657
29083
  const hidden = lines.length - visible.length;
28658
29084
  const gutterWidth = String(lines.length).length;
28659
- return /* @__PURE__ */ jsxs5(Box_default, { flexDirection: "column", children: [
29085
+ return /* @__PURE__ */ jsxs5(Box_default, { flexDirection: "column", width: "100%", alignItems: "stretch", children: [
28660
29086
  visible.map((line, idx) => {
28661
29087
  const tokens = tokenizeLine(line, language);
28662
29088
  const lineNum = String(idx + 1).padStart(gutterWidth, " ");
28663
- return /* @__PURE__ */ jsx12(Box_default, { children: /* @__PURE__ */ jsxs5(Text, { children: [
28664
- /* @__PURE__ */ jsx12(Text, { color: THEME.lineNumber, children: lineNum }),
28665
- /* @__PURE__ */ jsx12(Text, { color: THEME.gutter, children: " \u2502 " }),
28666
- tokens.map((t, ti) => /* @__PURE__ */ jsx12(Text, { color: t.color, children: t.text }, ti))
28667
- ] }) }, idx);
29089
+ return /* @__PURE__ */ jsxs5(Box_default, { width: "100%", flexDirection: "row", children: [
29090
+ /* @__PURE__ */ jsx12(Box_default, { marginRight: 2, children: /* @__PURE__ */ jsx12(Text, { color: THEME.lineNumber, children: lineNum }) }),
29091
+ /* @__PURE__ */ jsx12(Text, { children: tokens.map((t, ti) => /* @__PURE__ */ jsx12(Text, { color: t.color, children: t.text }, ti)) })
29092
+ ] }, idx);
28668
29093
  }),
28669
29094
  hidden > 0 && /* @__PURE__ */ jsxs5(Text, { color: THEME.lineNumber, dimColor: true, children: [
28670
29095
  "\u2026 +",
@@ -28735,6 +29160,11 @@ function headingAccent(depth) {
28735
29160
  if (depth === 3) return BLUMA_TERMINAL.magenta;
28736
29161
  return BLUMA_TERMINAL.inactive;
28737
29162
  }
29163
+ function normalizeMarkdownText(content) {
29164
+ const trimmed = content.trim();
29165
+ if (!trimmed) return "";
29166
+ return trimmed.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n");
29167
+ }
28738
29168
  function listMarker(ordered, index, start, task, checked) {
28739
29169
  if (task) return checked ? "[x]" : "[ ]";
28740
29170
  if (ordered) {
@@ -28928,6 +29358,46 @@ function renderParagraph(p, key, compact) {
28928
29358
  const nodes = walkInline(p.tokens, key);
28929
29359
  return /* @__PURE__ */ jsx13(Box_default, { marginBottom: compact ? 0 : 1, flexDirection: "row", flexWrap: "wrap", children: nodes.length > 0 ? nodes : /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.muted, wrap: "wrap", children: p.text }) }, key);
28930
29360
  }
29361
+ function renderCodeBlock(code, key) {
29362
+ const raw = code.text.replace(/\n$/, "");
29363
+ return /* @__PURE__ */ jsxs6(
29364
+ Box_default,
29365
+ {
29366
+ flexDirection: "column",
29367
+ marginBottom: 1,
29368
+ paddingX: 1,
29369
+ paddingY: 0,
29370
+ backgroundColor: BLUMA_TERMINAL.surfaceContainer,
29371
+ borderStyle: "round",
29372
+ borderColor: BLUMA_TERMINAL.outlineVariant,
29373
+ children: [
29374
+ code.lang ? /* @__PURE__ */ jsx13(Box_default, { marginBottom: 0, children: /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, bold: true, children: code.lang }) }) : null,
29375
+ /* @__PURE__ */ jsx13(HighlightedCode, { code: raw, filePath: code.lang || "snippet" })
29376
+ ]
29377
+ },
29378
+ key
29379
+ );
29380
+ }
29381
+ function renderBlockquote(q, key) {
29382
+ const inner = q.tokens && q.tokens.length > 0 ? renderBlockTokens(q.tokens, `${key}-inner`) : /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, italic: true, wrap: "wrap", children: q.text });
29383
+ return /* @__PURE__ */ jsxs6(
29384
+ Box_default,
29385
+ {
29386
+ flexDirection: "row",
29387
+ marginBottom: 1,
29388
+ alignItems: "flex-start",
29389
+ paddingX: 1,
29390
+ borderStyle: "round",
29391
+ borderColor: BLUMA_TERMINAL.outlineVariant,
29392
+ backgroundColor: BLUMA_TERMINAL.surfaceContainer,
29393
+ children: [
29394
+ /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, children: "\u2502 " }),
29395
+ /* @__PURE__ */ jsx13(Box_default, { flexDirection: "column", flexGrow: 1, children: inner })
29396
+ ]
29397
+ },
29398
+ key
29399
+ );
29400
+ }
28931
29401
  function renderListItemBlocks(item, depth, keyBase) {
28932
29402
  if (!item.tokens?.length) {
28933
29403
  const nodes = walkInline(
@@ -28963,13 +29433,6 @@ function renderListBlock(list, depth, keyBase) {
28963
29433
  ] }, `${keyBase}-row-${idx}`);
28964
29434
  }) }, keyBase);
28965
29435
  }
28966
- function renderBlockquote(q, key) {
28967
- const inner = q.tokens && q.tokens.length > 0 ? renderBlockTokens(q.tokens, `${key}-inner`) : /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, italic: true, wrap: "wrap", children: q.text });
28968
- return /* @__PURE__ */ jsxs6(Box_default, { flexDirection: "row", marginBottom: 1, alignItems: "flex-start", children: [
28969
- /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, children: "\u2502 " }),
28970
- /* @__PURE__ */ jsx13(Box_default, { flexDirection: "column", flexGrow: 1, children: inner })
28971
- ] }, key);
28972
- }
28973
29436
  function renderBlockTokens(tokens, keyRoot) {
28974
29437
  const elements = [];
28975
29438
  for (let i = 0; i < tokens.length; i++) {
@@ -28992,17 +29455,9 @@ function renderBlockTokens(tokens, keyRoot) {
28992
29455
  case "list":
28993
29456
  elements.push(renderListBlock(token, 0, key));
28994
29457
  break;
28995
- case "code": {
28996
- const code = token;
28997
- const raw = code.text.replace(/\n$/, "");
28998
- elements.push(
28999
- /* @__PURE__ */ jsxs6(Box_default, { flexDirection: "column", marginBottom: 1, children: [
29000
- code.lang ? /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, wrap: "wrap", children: code.lang }) : null,
29001
- /* @__PURE__ */ jsx13(HighlightedCode, { code: raw, filePath: code.lang || "snippet" })
29002
- ] }, key)
29003
- );
29458
+ case "code":
29459
+ elements.push(renderCodeBlock(token, key));
29004
29460
  break;
29005
- }
29006
29461
  case "blockquote":
29007
29462
  elements.push(renderBlockquote(token, key));
29008
29463
  break;
@@ -29059,7 +29514,8 @@ var MarkdownRendererComponent = ({ markdown }) => {
29059
29514
  if (!markdown || markdown.trim() === "") {
29060
29515
  return null;
29061
29516
  }
29062
- const tokens = cachedLexer(markdown);
29517
+ const normalized = normalizeMarkdownText(markdown);
29518
+ const tokens = cachedLexer(normalized);
29063
29519
  return /* @__PURE__ */ jsx13(Box_default, { flexDirection: "column", paddingRight: 1, children: renderBlockTokens(tokens, "md") });
29064
29520
  };
29065
29521
  var MarkdownRenderer = memo4(MarkdownRendererComponent, (prevProps, nextProps) => {
@@ -30618,9 +31074,12 @@ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
30618
31074
  function renderToolHeader6({ args }) {
30619
31075
  const cmd = args?.command ?? "";
30620
31076
  const displayCmd = cmd.length > 50 ? cmd.substring(0, 50) + "\u2026" : cmd;
30621
- return /* @__PURE__ */ jsxs20(Box_default, { flexDirection: "row", gap: 1, alignItems: "flex-end", children: [
31077
+ return /* @__PURE__ */ jsxs20(Box_default, { flexDirection: "row", alignItems: "flex-end", children: [
30622
31078
  /* @__PURE__ */ jsx33(Text, { bold: true, color: BLUMA_TERMINAL.m3OnSurface, children: "Ran" }),
30623
- /* @__PURE__ */ jsx33(Text, { color: BLUMA_TERMINAL.dim, children: displayCmd })
31079
+ /* @__PURE__ */ jsxs20(Text, { color: BLUMA_TERMINAL.dim, children: [
31080
+ " ",
31081
+ displayCmd
31082
+ ] })
30624
31083
  ] });
30625
31084
  }
30626
31085
  function renderToolUseMessage6({ args }) {
@@ -31965,6 +32424,46 @@ function getPatchForEdit({
31965
32424
  }));
31966
32425
  return { patch, updatedFile: unescapeFromDiff(updatedFile) };
31967
32426
  }
32427
+ function getPatchFromContents({
32428
+ filePath,
32429
+ oldContent,
32430
+ newContent
32431
+ }) {
32432
+ const result = structuredPatch(
32433
+ filePath,
32434
+ filePath,
32435
+ escapeForDiff(oldContent),
32436
+ escapeForDiff(newContent),
32437
+ void 0,
32438
+ void 0,
32439
+ {
32440
+ context: CONTEXT_LINES,
32441
+ timeout: DIFF_TIMEOUT_MS
32442
+ }
32443
+ );
32444
+ if (!result) {
32445
+ return [];
32446
+ }
32447
+ return result.hunks.map((h) => ({
32448
+ ...h,
32449
+ lines: h.lines.map(unescapeFromDiff)
32450
+ }));
32451
+ }
32452
+ function patchToUnifiedDiffText(filePath, patch) {
32453
+ if (patch.length === 0) return "";
32454
+ let text = `--- a/${filePath}
32455
+ +++ b/${filePath}
32456
+ `;
32457
+ for (const hunk of patch) {
32458
+ text += `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@
32459
+ `;
32460
+ for (const line of hunk.lines) {
32461
+ text += `${line}
32462
+ `;
32463
+ }
32464
+ }
32465
+ return text;
32466
+ }
31968
32467
 
31969
32468
  // src/app/ui/utils/readEditContext.ts
31970
32469
  import { promises as fs41 } from "fs";
@@ -32138,21 +32637,21 @@ function rowLineCounts(rows) {
32138
32637
  }
32139
32638
  return { additions, removals };
32140
32639
  }
32141
- function gutterCtx(wOld, wNew, oldN, newN) {
32142
- return `${String(oldN).padStart(wOld)} ${String(newN).padStart(wNew)} \u2502 `;
32640
+ function gutterCtx(wOld, wNew, oldN, newN, showSeparator) {
32641
+ return `${String(oldN).padStart(wOld)} ${String(newN).padStart(wNew)}${showSeparator ? " \u2502 " : " "}`;
32143
32642
  }
32144
- function gutterRem(wOld, wNew, oldN) {
32145
- return `${String(oldN).padStart(wOld)} ${" ".repeat(wNew)} \u2502\u2212`;
32643
+ function gutterRem(wOld, wNew, oldN, showSeparator) {
32644
+ return `${String(oldN).padStart(wOld)} ${" ".repeat(wNew)}${showSeparator ? " \u2502 " : " "}`;
32146
32645
  }
32147
- function gutterAdd(wOld, wNew, newN) {
32148
- return `${" ".repeat(wOld)} ${String(newN).padStart(wNew)} \u2502+`;
32646
+ function gutterAdd(wOld, wNew, newN, showSeparator) {
32647
+ return `${" ".repeat(wOld)} ${String(newN).padStart(wNew)}${showSeparator ? " \u2502 " : " "}`;
32149
32648
  }
32150
- function gutterOmit(wOld, wNew) {
32649
+ function gutterOmit(wOld, wNew, showSeparator) {
32151
32650
  const cols = wOld + 1 + wNew;
32152
32651
  const dot = "\u22EE";
32153
32652
  const padL = Math.max(0, Math.floor((cols - dot.length) / 2));
32154
32653
  const padR = Math.max(0, cols - dot.length - padL);
32155
- return `${" ".repeat(padL)}${dot}${" ".repeat(padR)} \u2502 `;
32654
+ return `${" ".repeat(padL)}${dot}${" ".repeat(padR)}${showSeparator ? " \u2502 " : " "}`;
32156
32655
  }
32157
32656
  function WordDiffChunks({
32158
32657
  parts,
@@ -32170,9 +32669,9 @@ function WordDiffChunks({
32170
32669
  /* @__PURE__ */ jsx41(Text, { backgroundColor: bg, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: p.value }, k++)
32171
32670
  );
32172
32671
  }
32173
- return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", children: chunks });
32672
+ return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", flexGrow: 1, children: chunks });
32174
32673
  }
32175
- function renderStructuredRows(rows, maxHeight) {
32674
+ function renderStructuredRows(rows, maxHeight, showGutterSeparator) {
32176
32675
  const paired = collapseContextRows(pairWordDiffs(rows));
32177
32676
  const { wOld, wNew } = maxLineNums(paired);
32178
32677
  const { additions, removals } = rowLineCounts(paired);
@@ -32191,8 +32690,8 @@ function renderStructuredRows(rows, maxHeight) {
32191
32690
  for (const r of paired) {
32192
32691
  if (r.kind === "omit") {
32193
32692
  push(
32194
- /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", children: [
32195
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterOmit(wOld, wNew) }),
32693
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, children: [
32694
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterOmit(wOld, wNew, showGutterSeparator) }),
32196
32695
  r.count > 1 ? /* @__PURE__ */ jsxs26(Text, { dimColor: true, wrap: "wrap", children: [
32197
32696
  " ",
32198
32697
  "\xB7 ",
@@ -32204,14 +32703,14 @@ function renderStructuredRows(rows, maxHeight) {
32204
32703
  }
32205
32704
  if (r.kind === "meta") {
32206
32705
  push(
32207
- /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", width: "100%", children: /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: r.text }) }, `m-${idx++}`)
32706
+ /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, children: /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: r.text }) }, `m-${idx++}`)
32208
32707
  );
32209
32708
  continue;
32210
32709
  }
32211
32710
  if (r.kind === "ctx") {
32212
32711
  push(
32213
- /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", children: [
32214
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterCtx(wOld, wNew, r.old, r.new) }),
32712
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", flexGrow: 1, children: [
32713
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterCtx(wOld, wNew, r.old, r.new, showGutterSeparator) }),
32215
32714
  /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.muted, wrap: "wrap", children: r.text || " " })
32216
32715
  ] }, `c-${idx++}`)
32217
32716
  );
@@ -32219,18 +32718,20 @@ function renderStructuredRows(rows, maxHeight) {
32219
32718
  }
32220
32719
  if (r.kind === "rem") {
32221
32720
  push(
32222
- /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexWrap: "wrap", children: [
32223
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterRem(wOld, wNew, r.old) }),
32224
- r.wordParts && r.wordParts.length > 0 ? /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, children: /* @__PURE__ */ jsx41(WordDiffChunks, { parts: r.wordParts, mode: "rem" }) }) : /* @__PURE__ */ jsx41(Text, { backgroundColor: BLUMA_TERMINAL.diffRemoved, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: r.text || " " })
32721
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffRemoved, children: [
32722
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterRem(wOld, wNew, r.old, showGutterSeparator) }),
32723
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: "-" }),
32724
+ r.wordParts && r.wordParts.length > 0 ? /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, marginLeft: 1, children: /* @__PURE__ */ jsx41(WordDiffChunks, { parts: r.wordParts, mode: "rem" }) }) : /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, marginLeft: 1, children: /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: r.text || " " }) })
32225
32725
  ] }, `r-${idx++}`)
32226
32726
  );
32227
32727
  continue;
32228
32728
  }
32229
32729
  if (r.kind === "add") {
32230
32730
  push(
32231
- /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexWrap: "wrap", children: [
32232
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterAdd(wOld, wNew, r.new) }),
32233
- r.wordParts && r.wordParts.length > 0 ? /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, children: /* @__PURE__ */ jsx41(WordDiffChunks, { parts: r.wordParts, mode: "add" }) }) : /* @__PURE__ */ jsx41(Text, { backgroundColor: BLUMA_TERMINAL.diffAdded, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: r.text || " " })
32731
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffAdded, children: [
32732
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterAdd(wOld, wNew, r.new, showGutterSeparator) }),
32733
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: "+" }),
32734
+ r.wordParts && r.wordParts.length > 0 ? /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, marginLeft: 1, children: /* @__PURE__ */ jsx41(WordDiffChunks, { parts: r.wordParts, mode: "add" }) }) : /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", flexGrow: 1, marginLeft: 1, children: /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: r.text || " " }) })
32234
32735
  ] }, `a-${idx++}`)
32235
32736
  );
32236
32737
  }
@@ -32348,7 +32849,8 @@ function collapseContextRows(rows) {
32348
32849
  }
32349
32850
  function LegacyDiffBody({
32350
32851
  lines,
32351
- maxHeight
32852
+ maxHeight,
32853
+ showGutterSeparator
32352
32854
  }) {
32353
32855
  const segs = legacyDiffToSegs(lines);
32354
32856
  const { wOld, wNew } = legacySegWidths(segs);
@@ -32357,14 +32859,14 @@ function LegacyDiffBody({
32357
32859
  const hiddenBelow = truncated ? segs.length - toRender.length : 0;
32358
32860
  const { additions, removals } = legacySegLineCounts(lines);
32359
32861
  const summary = diffSummary(additions, removals, hiddenBelow);
32360
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32862
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", width: "100%", alignItems: "stretch", children: [
32361
32863
  toRender.map((seg, index) => {
32362
32864
  if (seg.kind === "header") {
32363
32865
  return /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: seg.line }, index);
32364
32866
  }
32365
32867
  if (seg.kind === "omit") {
32366
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", children: [
32367
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterOmit(wOld, wNew) }),
32868
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, children: [
32869
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterOmit(wOld, wNew, showGutterSeparator) }),
32368
32870
  seg.count > 1 ? /* @__PURE__ */ jsxs26(Text, { dimColor: true, wrap: "wrap", children: [
32369
32871
  " ",
32370
32872
  "\xB7 ",
@@ -32376,15 +32878,23 @@ function LegacyDiffBody({
32376
32878
  return /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: seg.line }, index);
32377
32879
  }
32378
32880
  if (seg.kind === "rem") {
32379
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexWrap: "wrap", children: [
32380
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterRem(wOld, wNew, seg.oldLine) }),
32381
- /* @__PURE__ */ jsx41(Text, { backgroundColor: BLUMA_TERMINAL.diffRemoved, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: seg.body || " " })
32881
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffRemoved, children: [
32882
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterRem(wOld, wNew, seg.oldLine, showGutterSeparator) }),
32883
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: "-" }),
32884
+ /* @__PURE__ */ jsxs26(Text, { color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: [
32885
+ " ",
32886
+ seg.body || " "
32887
+ ] })
32382
32888
  ] }, index);
32383
32889
  }
32384
32890
  if (seg.kind === "add") {
32385
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexWrap: "wrap", children: [
32386
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterAdd(wOld, wNew, seg.newLine) }),
32387
- /* @__PURE__ */ jsx41(Text, { backgroundColor: BLUMA_TERMINAL.diffAdded, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: seg.body || " " })
32891
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffAdded, children: [
32892
+ /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterAdd(wOld, wNew, seg.newLine, showGutterSeparator) }),
32893
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: "+" }),
32894
+ /* @__PURE__ */ jsxs26(Text, { color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: [
32895
+ " ",
32896
+ seg.body || " "
32897
+ ] })
32388
32898
  ] }, index);
32389
32899
  }
32390
32900
  return null;
@@ -32392,7 +32902,7 @@ function LegacyDiffBody({
32392
32902
  summary ? /* @__PURE__ */ jsx41(Text, { dimColor: true, children: summary }) : null
32393
32903
  ] });
32394
32904
  }
32395
- var SimpleDiff = ({ text, maxHeight, frame = false }) => {
32905
+ var SimpleDiff = ({ text, maxHeight, frame = false, showGutterSeparator = true }) => {
32396
32906
  const raw = stripOptionalMarkdownFence(String(text ?? ""));
32397
32907
  let patches = [];
32398
32908
  try {
@@ -32404,7 +32914,7 @@ var SimpleDiff = ({ text, maxHeight, frame = false }) => {
32404
32914
  const rule = "\u2500".repeat(diffRuleWidth());
32405
32915
  if (hunks.length === 0) {
32406
32916
  const allLines = raw.split("\n");
32407
- const body2 = /* @__PURE__ */ jsx41(LegacyDiffBody, { lines: allLines, maxHeight });
32917
+ const body2 = /* @__PURE__ */ jsx41(LegacyDiffBody, { lines: allLines, maxHeight, showGutterSeparator });
32408
32918
  if (!frame) return body2;
32409
32919
  return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32410
32920
  /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule }),
@@ -32418,16 +32928,17 @@ var SimpleDiff = ({ text, maxHeight, frame = false }) => {
32418
32928
  }
32419
32929
  const { nodes, hidden, additions, removals } = renderStructuredRows(
32420
32930
  rows,
32421
- maxHeight
32931
+ maxHeight,
32932
+ showGutterSeparator
32422
32933
  );
32423
- const body = /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32934
+ const body = /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", width: "100%", alignItems: "stretch", children: [
32424
32935
  (hidden > 0 || additions > 0 || removals > 0) && /* @__PURE__ */ jsx41(Text, { dimColor: true, children: diffSummary(additions, removals, hidden) }),
32425
32936
  nodes
32426
32937
  ] });
32427
32938
  if (!frame) {
32428
32939
  return body;
32429
32940
  }
32430
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32941
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", width: "100%", alignItems: "stretch", children: [
32431
32942
  /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule }),
32432
32943
  /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", paddingX: 1, children: body }),
32433
32944
  /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule })
@@ -32529,6 +33040,7 @@ function EditToolDiffPanel({
32529
33040
  description,
32530
33041
  diffText,
32531
33042
  maxHeight = EDIT_DIFF_PREVIEW_MAX_LINES,
33043
+ showGutterSeparator = true,
32532
33044
  oldString,
32533
33045
  newString,
32534
33046
  replaceAll = false
@@ -32545,11 +33057,27 @@ function EditToolDiffPanel({
32545
33057
  replaceAll,
32546
33058
  maxHeight
32547
33059
  }
32548
- ) }) : hasDiffText ? /* @__PURE__ */ jsx43(Box_default, { marginTop: 0, children: /* @__PURE__ */ jsx43(SimpleDiff, { text: diffText, maxHeight }) }) : /* @__PURE__ */ jsx43(Box_default, { marginTop: 0, children: /* @__PURE__ */ jsx43(Text, { dimColor: true, wrap: "wrap", children: "Diff preview unavailable" }) }) });
33060
+ ) }) : hasDiffText ? /* @__PURE__ */ jsx43(Box_default, { marginTop: 0, children: /* @__PURE__ */ jsx43(SimpleDiff, { text: diffText, maxHeight, showGutterSeparator }) }) : /* @__PURE__ */ jsx43(Box_default, { marginTop: 0, children: /* @__PURE__ */ jsx43(Text, { dimColor: true, wrap: "wrap", children: "Diff preview unavailable" }) }) });
32549
33061
  }
32550
33062
 
32551
33063
  // src/app/agent/tools/EditTool/UI.tsx
33064
+ import { diffLines as diffLines2 } from "diff";
32552
33065
  import { Fragment as Fragment5, jsx as jsx44, jsxs as jsxs27 } from "react/jsx-runtime";
33066
+ function countLineDiff(oldText, newText) {
33067
+ const parts = diffLines2(oldText.replace(/\r\n/g, "\n"), newText.replace(/\r\n/g, "\n"));
33068
+ let added = 0;
33069
+ let removed = 0;
33070
+ for (const part of parts) {
33071
+ const lines = part.value.replace(/\n$/, "").split("\n");
33072
+ const lineCount = part.value === "" ? 0 : lines.length;
33073
+ if (part.added) added += lineCount;
33074
+ if (part.removed) removed += lineCount;
33075
+ }
33076
+ return { added, removed };
33077
+ }
33078
+ function stripUnifiedDiffFileHeaders(diffText) {
33079
+ return diffText.replace(/^--- .*\n/m, "").replace(/^\+\+\+ .*\n/m, "");
33080
+ }
32553
33081
  function userFacingName11(args) {
32554
33082
  if (!args) return "Updated";
32555
33083
  if (args.edits && Array.isArray(args.edits) && args.edits.length > 0) {
@@ -32564,11 +33092,31 @@ function renderToolUseMessage12({ args }) {
32564
33092
  return /* @__PURE__ */ jsx44(Text, { color: BLUMA_TERMINAL.blue, children: p });
32565
33093
  }
32566
33094
  function renderToolHeader12({ args }) {
32567
- const label = userFacingName11(args);
32568
33095
  const path50 = args?.file_path ?? ".";
33096
+ const oldText = typeof args?.old_string === "string" ? args.old_string : "";
33097
+ const newText = typeof args?.new_string === "string" ? args.new_string : "";
33098
+ const counts = countLineDiff(oldText, newText);
33099
+ const fileName = String(path50).split("/").pop() || String(path50);
33100
+ const action = oldText === "" ? "Created" : "Update";
32569
33101
  return /* @__PURE__ */ jsxs27(Box_default, { flexDirection: "row", gap: 1, alignItems: "flex-end", children: [
32570
- /* @__PURE__ */ jsx44(Text, { bold: true, color: BLUMA_TERMINAL.m3OnSurface, children: label }),
32571
- /* @__PURE__ */ jsx44(FilePathLink, { filePath: path50, color: BLUMA_TERMINAL.dim })
33102
+ /* @__PURE__ */ jsxs27(Text, { bold: true, color: BLUMA_TERMINAL.m3OnSurface, children: [
33103
+ action,
33104
+ " ",
33105
+ /* @__PURE__ */ jsx44(FilePathLink, { filePath: path50, color: BLUMA_TERMINAL.dim })
33106
+ ] }),
33107
+ /* @__PURE__ */ jsxs27(Text, { dimColor: true, children: [
33108
+ "(",
33109
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
33110
+ "+",
33111
+ counts.added
33112
+ ] }),
33113
+ " ",
33114
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
33115
+ "-",
33116
+ counts.removed
33117
+ ] }),
33118
+ ")"
33119
+ ] })
32572
33120
  ] });
32573
33121
  }
32574
33122
  function renderSingleEditDiff(edit, index, total, state2) {
@@ -32584,7 +33132,20 @@ function renderSingleEditDiff(edit, index, total, state2) {
32584
33132
  total
32585
33133
  ] }),
32586
33134
  /* @__PURE__ */ jsx44(Text, { bold: true, color: isNewFile ? BLUMA_TERMINAL.success : BLUMA_TERMINAL.blue, children: isNewFile ? "Create" : "Update" }),
32587
- /* @__PURE__ */ jsx44(FilePathLink, { filePath, color: BLUMA_TERMINAL.dim })
33135
+ /* @__PURE__ */ jsx44(FilePathLink, { filePath, color: BLUMA_TERMINAL.dim }),
33136
+ /* @__PURE__ */ jsxs27(Text, { dimColor: true, children: [
33137
+ "(",
33138
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
33139
+ "+",
33140
+ countLineDiff(oldString, newString).added
33141
+ ] }),
33142
+ " ",
33143
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
33144
+ "-",
33145
+ countLineDiff(oldString, newString).removed
33146
+ ] }),
33147
+ ")"
33148
+ ] })
32588
33149
  ] }),
32589
33150
  /* @__PURE__ */ jsx44(Box_default, { marginLeft: 2, marginTop: 0, children: /* @__PURE__ */ jsx44(
32590
33151
  EditToolDiffPanel,
@@ -32653,7 +33214,8 @@ function renderToolResultMessage12(result) {
32653
33214
  {
32654
33215
  filePath: edit.file_path,
32655
33216
  isNewFile: edit.is_new_file ?? false,
32656
- diffText: edit.diff
33217
+ diffText: typeof edit.diff === "string" ? stripUnifiedDiffFileHeaders(edit.diff) : edit.diff,
33218
+ showGutterSeparator: false
32657
33219
  }
32658
33220
  ) }, `result-edit-${idx}`)) });
32659
33221
  }
@@ -32662,7 +33224,8 @@ function renderToolResultMessage12(result) {
32662
33224
  {
32663
33225
  filePath: result.file_path,
32664
33226
  isNewFile: result.is_new_file ?? false,
32665
- diffText: result.diff
33227
+ diffText: stripUnifiedDiffFileHeaders(result.diff),
33228
+ showGutterSeparator: false
32666
33229
  }
32667
33230
  ) }) });
32668
33231
  }
@@ -32691,7 +33254,7 @@ function prettifyToolToken(value) {
32691
33254
  if (!s) return "";
32692
33255
  return s.replace(/[_-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
32693
33256
  }
32694
- function parseArgsRecord(args) {
33257
+ function parseArgsRecord2(args) {
32695
33258
  if (args == null) return {};
32696
33259
  if (typeof args === "string") {
32697
33260
  try {
@@ -32748,7 +33311,7 @@ var TOOL_INVOCATION_TITLES = {
32748
33311
  function getToolInvocationTitle(internalName, args) {
32749
33312
  const key = internalName.trim();
32750
33313
  if (key === "edit_tool") {
32751
- const p = parseArgsRecord(args);
33314
+ const p = parseArgsRecord2(args);
32752
33315
  const edits = p.edits;
32753
33316
  if (Array.isArray(edits) && edits.length > 0) {
32754
33317
  const allCreate = edits.every(
@@ -33458,6 +34021,8 @@ var loadSkillTool = createTool({
33458
34021
  });
33459
34022
 
33460
34023
  // src/app/agent/tools/FileWriteTool/UI.tsx
34024
+ import fs42 from "fs";
34025
+ import { diffLines as diffLines3 } from "diff";
33461
34026
  import { jsx as jsx54, jsxs as jsxs37 } from "react/jsx-runtime";
33462
34027
  function getFilePath(args) {
33463
34028
  if (typeof args?.filepath === "string" && args.filepath.trim()) {
@@ -33468,6 +34033,49 @@ function getFilePath(args) {
33468
34033
  }
33469
34034
  return void 0;
33470
34035
  }
34036
+ function readExistingFileText(filePath) {
34037
+ if (!filePath) return "";
34038
+ try {
34039
+ return fs42.readFileSync(filePath, "utf-8");
34040
+ } catch {
34041
+ return "";
34042
+ }
34043
+ }
34044
+ function countLineDiff2(oldText, newText) {
34045
+ const parts = diffLines3(oldText.replace(/\r\n/g, "\n"), newText.replace(/\r\n/g, "\n"));
34046
+ let added = 0;
34047
+ let removed = 0;
34048
+ for (const part of parts) {
34049
+ const lines = part.value.replace(/\n$/, "").split("\n");
34050
+ const lineCount = part.value === "" ? 0 : lines.length;
34051
+ if (part.added) added += lineCount;
34052
+ if (part.removed) removed += lineCount;
34053
+ }
34054
+ return { added, removed };
34055
+ }
34056
+ function renderChangeCount(added, removed) {
34057
+ return /* @__PURE__ */ jsxs37(Text, { dimColor: true, children: [
34058
+ "(",
34059
+ /* @__PURE__ */ jsxs37(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
34060
+ "+",
34061
+ added
34062
+ ] }),
34063
+ " ",
34064
+ /* @__PURE__ */ jsxs37(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
34065
+ "-",
34066
+ removed
34067
+ ] }),
34068
+ ")"
34069
+ ] });
34070
+ }
34071
+ function buildUnifiedDiffText(filePath, oldContent, newContent) {
34072
+ const patch = getPatchFromContents({
34073
+ filePath,
34074
+ oldContent,
34075
+ newContent
34076
+ });
34077
+ return patchToUnifiedDiffText(filePath, patch);
34078
+ }
33471
34079
  function userFacingName20(args) {
33472
34080
  if (!args) return "Wrote";
33473
34081
  const p = getFilePath(args)?.split("/").pop() ?? "...";
@@ -33480,9 +34088,12 @@ function renderToolUseMessage21({ args }) {
33480
34088
  function renderToolHeader21({ args }) {
33481
34089
  const filePath = getFilePath(args) ?? "";
33482
34090
  const isNewFile = !args?.hasExistingContent;
34091
+ const content = typeof args?.content === "string" ? args.content : "";
34092
+ const counts = content ? countLineDiff2(isNewFile ? "" : readExistingFileText(filePath), content) : { added: 0, removed: 0 };
33483
34093
  return /* @__PURE__ */ jsxs37(Box_default, { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-end", gap: 1, children: [
33484
- /* @__PURE__ */ jsx54(Text, { bold: true, color: isNewFile ? BLUMA_TERMINAL.success : BLUMA_TERMINAL.m3OnSurface, children: isNewFile ? "Created" : "Wrote" }),
33485
- /* @__PURE__ */ jsx54(FilePathLink, { filePath, color: BLUMA_TERMINAL.dim })
34094
+ /* @__PURE__ */ jsx54(Text, { bold: true, color: isNewFile ? BLUMA_TERMINAL.m3OnSurface : BLUMA_TERMINAL.err, children: isNewFile ? "Created" : "Updated" }),
34095
+ /* @__PURE__ */ jsx54(FilePathLink, { filePath, color: isNewFile ? BLUMA_TERMINAL.dim : BLUMA_TERMINAL.err }),
34096
+ content ? renderChangeCount(counts.added, counts.removed) : null
33486
34097
  ] });
33487
34098
  }
33488
34099
  function renderToolMessage20({
@@ -33494,12 +34105,15 @@ function renderToolMessage20({
33494
34105
  const filePath = getFilePath(p);
33495
34106
  const payload = resolveToolPayload(result);
33496
34107
  const content = typeof p?.content === "string" ? p.content : "";
34108
+ const isNewFile = !p?.hasExistingContent;
34109
+ const previousContent = isNewFile ? "" : readExistingFileText(filePath);
34110
+ const diffText = content && filePath ? buildUnifiedDiffText(filePath, previousContent, content) : "";
33497
34111
  return /* @__PURE__ */ jsx54(ChatBlock, { marginBottom: 1, children: /* @__PURE__ */ jsx54(MessageResponse, { children: /* @__PURE__ */ jsxs37(Box_default, { flexDirection: "column", marginTop: 1, children: [
33498
34112
  /* @__PURE__ */ jsxs37(Box_default, { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-end", children: [
33499
34113
  /* @__PURE__ */ jsx54(ToolUseLoader, { state: state2 ?? "pending" }),
33500
34114
  renderToolHeader21({ args: p })
33501
34115
  ] }),
33502
- /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1, children: payload != null ? renderToolResultMessage21(payload, { filePath, content }) : /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", children: /* @__PURE__ */ jsx54(Box_default, { marginTop: 0, children: content ? /* @__PURE__ */ jsx54(HighlightedCode, { code: content, filePath: filePath ?? "file.txt", maxLines: 40 }) : /* @__PURE__ */ jsxs37(Text, { dimColor: true, children: [
34116
+ /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", paddingLeft: 1, children: payload != null ? renderToolResultMessage21(payload, { filePath, content }) : /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", children: /* @__PURE__ */ jsx54(Box_default, { marginTop: 0, width: "100%", children: content ? /* @__PURE__ */ jsx54(Box_default, { width: "100%", flexGrow: 1, flexDirection: "column", alignItems: "stretch", children: /* @__PURE__ */ jsx54(SimpleDiff, { text: diffText, maxHeight: 4 }) }) : /* @__PURE__ */ jsxs37(Text, { dimColor: true, children: [
33503
34117
  "Writing to ",
33504
34118
  filePath ?? "...",
33505
34119
  " (",
@@ -33520,15 +34134,32 @@ function renderToolResultMessage21(result, context) {
33520
34134
  }
33521
34135
  const filePath = payload.filepath ?? payload.file_path ?? "";
33522
34136
  const bytes = payload.bytes_written ?? 0;
33523
- const statusText = payload.created ? "created" : payload.overwritten ? "overwritten" : "written";
33524
- return /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", children: typeof context?.content === "string" && context.content.trim() ? /* @__PURE__ */ jsx54(Box_default, { marginTop: 0, children: /* @__PURE__ */ jsx54(
33525
- HighlightedCode,
34137
+ const resultFilePath = filePath || context?.filePath || "file.txt";
34138
+ const previousContent = typeof payload.previous_content === "string" ? payload.previous_content : void 0;
34139
+ const currentContent = typeof payload.content === "string" ? payload.content : typeof context?.args?.content === "string" ? (context?.args).content : typeof context?.content === "string" ? context.content : void 0;
34140
+ const canRenderDiff = typeof currentContent === "string" && currentContent.trim() && typeof previousContent === "string";
34141
+ return /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", children: canRenderDiff ? /* @__PURE__ */ jsx54(Box_default, { marginTop: 0, width: "100%", flexDirection: "column", alignItems: "stretch", flexGrow: 1, children: /* @__PURE__ */ jsx54(
34142
+ SimpleDiff,
34143
+ {
34144
+ text: buildUnifiedDiffText(resultFilePath, previousContent ?? "", currentContent ?? ""),
34145
+ maxHeight: 200
34146
+ }
34147
+ ) }) : typeof currentContent === "string" && currentContent.trim() ? /* @__PURE__ */ jsx54(
34148
+ Box_default,
33526
34149
  {
33527
- code: context.content,
33528
- filePath: filePath || context.filePath || "file.txt",
33529
- maxLines: 200
34150
+ width: "100%",
34151
+ backgroundColor: BLUMA_TERMINAL.diffAdded,
34152
+ marginTop: 1,
34153
+ children: /* @__PURE__ */ jsx54(
34154
+ HighlightedCode,
34155
+ {
34156
+ code: currentContent,
34157
+ filePath: resultFilePath,
34158
+ maxLines: 2e3
34159
+ }
34160
+ )
33530
34161
  }
33531
- ) }) : null });
34162
+ ) : null });
33532
34163
  }
33533
34164
 
33534
34165
  // src/app/agent/tools/FileWriteTool/index.ts
@@ -35961,162 +36592,6 @@ var ConfirmationPrompt = memo13(ConfirmationPromptComponent);
35961
36592
  // src/app/ui/WorkingTimer.tsx
35962
36593
  import { useState as useState15, useEffect as useEffect13, memo as memo14, useRef as useRef6 } from "react";
35963
36594
  import { EventEmitter as EventEmitter6 } from "events";
35964
-
35965
- // src/app/ui/utils/toolActionLabels.ts
35966
- function parseArgsRecord2(args) {
35967
- if (args == null) return {};
35968
- if (typeof args === "string") {
35969
- try {
35970
- return JSON.parse(args);
35971
- } catch {
35972
- return {};
35973
- }
35974
- }
35975
- if (typeof args === "object") {
35976
- return args;
35977
- }
35978
- return {};
35979
- }
35980
- function getToolActionLabel(toolName, args) {
35981
- const p = parseArgsRecord2(args);
35982
- switch (toolName) {
35983
- case "shell_command":
35984
- case "run_command": {
35985
- const cmd = typeof p.command === "string" ? p.command : "";
35986
- const truncated = cmd.length > 40 ? `${cmd.slice(0, 40)}\u2026` : cmd;
35987
- return truncated ? `Bash ${truncated}` : "Bash";
35988
- }
35989
- case "command_status":
35990
- return "Status";
35991
- case "send_command_input":
35992
- return "Input";
35993
- case "kill_command":
35994
- return "Kill";
35995
- case "read_file_lines": {
35996
- const filepath = typeof p.filepath === "string" ? p.filepath : "";
35997
- const file = filepath.split("/").pop() || filepath;
35998
- return file ? `Read ${file}` : "Read";
35999
- }
36000
- case "ls_tool":
36001
- return "List";
36002
- case "count_file_lines": {
36003
- const filepath = typeof p.filepath === "string" ? p.filepath : "";
36004
- const file = filepath.split("/").pop() || filepath;
36005
- return file ? `Count ${file}` : "Count";
36006
- }
36007
- case "edit_tool": {
36008
- const edits = p.edits;
36009
- const count = Array.isArray(edits) ? edits.length : 1;
36010
- const filepath = typeof p.file_path === "string" ? p.file_path : Array.isArray(edits) && edits[0]?.file_path ? edits[0].file_path : "";
36011
- const file = filepath ? filepath.split("/").pop() : "file";
36012
- return count === 1 ? `Edit ${file}` : `Edit ${count} files`;
36013
- }
36014
- case "file_write": {
36015
- const filepath = typeof p.filepath === "string" ? p.filepath : "";
36016
- const file = filepath.split("/").pop() || filepath;
36017
- return file ? `Write ${file}` : "Write";
36018
- }
36019
- case "grep_search": {
36020
- const query = typeof p.query === "string" ? p.query : "";
36021
- const truncated = query.length > 30 ? `${query.slice(0, 30)}\u2026` : query;
36022
- return truncated ? `Grep ${truncated}` : "Grep";
36023
- }
36024
- case "find_by_name": {
36025
- const pattern = typeof p.pattern === "string" ? p.pattern : "";
36026
- return pattern ? `Find ${pattern}` : "Find files";
36027
- }
36028
- case "view_file_outline": {
36029
- const filepath = typeof p.file_path === "string" ? p.file_path : "";
36030
- const file = filepath.split("/").pop() || filepath;
36031
- return file ? `Outline ${file}` : "Outline";
36032
- }
36033
- case "web_fetch": {
36034
- const url = typeof p.url === "string" ? p.url : "";
36035
- const truncated = url.length > 40 ? `${url.slice(0, 40)}\u2026` : url;
36036
- return truncated ? `Fetch ${truncated}` : "Fetch URL";
36037
- }
36038
- case "search_web": {
36039
- const query = typeof p.query === "string" ? p.query : "";
36040
- const truncated = query.length > 30 ? `${query.slice(0, 30)}\u2026` : query;
36041
- return truncated ? `Web ${truncated}` : "Web search";
36042
- }
36043
- case "spawn_agent": {
36044
- const title = typeof p.title === "string" ? p.title : "";
36045
- const task = typeof p.task === "string" ? p.task : "";
36046
- const label = title ? prettifyToolToken(title) : task ? task.slice(0, 40) : "task";
36047
- return `Spawn ${label}`;
36048
- }
36049
- case "wait_agent":
36050
- return "Wait";
36051
- case "list_agents":
36052
- return "Agents";
36053
- case "todo": {
36054
- const action = typeof p.action === "string" ? p.action : "update";
36055
- return `Todo ${action}`;
36056
- }
36057
- case "task_boundary": {
36058
- const mode = typeof p.mode === "string" ? p.mode : "";
36059
- const taskName = typeof p.task_name === "string" ? p.task_name : "";
36060
- return taskName ? `${mode} ${taskName}` : `Task ${mode}`;
36061
- }
36062
- case "task_create":
36063
- case "task_list":
36064
- case "task_get":
36065
- case "task_update":
36066
- case "task_stop": {
36067
- const title = typeof p.title === "string" ? p.title : "";
36068
- return title ? `Task ${prettifyToolToken(title)}` : "Task";
36069
- }
36070
- case "load_skill": {
36071
- const skill = typeof p.skill_name === "string" ? p.skill_name : "";
36072
- return skill ? `Skill ${skill.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}` : "Skill";
36073
- }
36074
- case "coding_memory": {
36075
- const action = typeof p.action === "string" ? p.action : "update";
36076
- return `Memory ${action}`;
36077
- }
36078
- case "read_artifact": {
36079
- const filename = typeof p.filename === "string" ? p.filename : "";
36080
- return filename ? `Read ${filename}` : "Read artifact";
36081
- }
36082
- case "message":
36083
- return "Message";
36084
- case "ask_user_question":
36085
- return "Question";
36086
- case "enter_plan_mode":
36087
- return "Plan mode";
36088
- case "exit_plan_mode":
36089
- return "Exit plan";
36090
- case "list_mcp_resources":
36091
- return "MCP resources";
36092
- case "read_mcp_resource": {
36093
- const server = typeof p.server === "string" ? p.server : "";
36094
- const uri = typeof p.uri === "string" ? p.uri : "";
36095
- return `MCP ${server || uri || "resource"}`;
36096
- }
36097
- case "cron_create":
36098
- return "Schedule";
36099
- case "cron_list":
36100
- return "Reminders";
36101
- case "cron_delete":
36102
- return "Cancel reminder";
36103
- case "notebook_edit": {
36104
- const filepath = typeof p.filepath === "string" ? p.filepath : "";
36105
- const file = filepath.split("/").pop() || filepath;
36106
- return file ? `Notebook ${file}` : "Notebook";
36107
- }
36108
- case "lsp_query": {
36109
- const operation = typeof p.operation === "string" ? p.operation : "";
36110
- return `LSP ${operation || "query"}`;
36111
- }
36112
- default: {
36113
- const pretty = toolName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
36114
- return pretty || "Working";
36115
- }
36116
- }
36117
- }
36118
-
36119
- // src/app/ui/WorkingTimer.tsx
36120
36595
  import { Fragment as Fragment8, jsx as jsx77, jsxs as jsxs60 } from "react/jsx-runtime";
36121
36596
  var SHIMMER_CYCLE_MS = 2e3;
36122
36597
  var SHIMMER_TICK_MS = 80;
@@ -36163,9 +36638,6 @@ var WorkingTimerComponent = ({
36163
36638
  finalElapsedMs: externalFinalElapsedMs
36164
36639
  // External final elapsed time (from parent)
36165
36640
  }) => {
36166
- if (startedAtMs == null && externalFinalElapsedMs == null) {
36167
- return null;
36168
- }
36169
36641
  const [currentAction, setCurrentAction] = useState15("working");
36170
36642
  const [nowTick, setNowTick] = useState15(() => Date.now());
36171
36643
  const [internalFinalElapsedMs, setInternalFinalElapsedMs] = useState15(null);
@@ -36206,11 +36678,10 @@ var WorkingTimerComponent = ({
36206
36678
  }, [startedAtMs]);
36207
36679
  useEffect13(() => {
36208
36680
  const localBus = localEventBusRef.current;
36209
- if (startedAtMs == null) return;
36210
36681
  let isMounted = true;
36211
36682
  completionHandledRef.current = false;
36212
36683
  const handleTurnComplete = (data) => {
36213
- if (!isMounted || completionHandledRef.current) return;
36684
+ if (!isMounted || completionHandledRef.current || startedAtMs == null) return;
36214
36685
  const elapsedMs2 = typeof data?.elapsedMs === "number" ? data.elapsedMs : null;
36215
36686
  if (elapsedMs2 == null) return;
36216
36687
  completionHandledRef.current = true;
@@ -36235,7 +36706,10 @@ var WorkingTimerComponent = ({
36235
36706
  localBus.removeAllListeners();
36236
36707
  };
36237
36708
  }, [eventBus, startedAtMs]);
36238
- const displayAction = dynamicActionLabel || (taskStatus || (isReasoning ? "thinking" : currentAction)).trim() || "working";
36709
+ if (startedAtMs == null && externalFinalElapsedMs == null) {
36710
+ return null;
36711
+ }
36712
+ const displayAction = dynamicActionLabel || (taskStatus || (currentAction !== "working" ? currentAction : isReasoning ? getReasoningActionStatus().action : currentAction)).trim() || "working";
36239
36713
  const actionLine = `${displayAction}`;
36240
36714
  const elapsedMs = finalElapsedMs != null ? finalElapsedMs : startedAtMs != null ? Math.max(0, nowTick - startedAtMs) : 0;
36241
36715
  const elapsedLabel = formatTurnDurationMs(elapsedMs);
@@ -36379,13 +36853,13 @@ var StatusBar = memo16(() => {
36379
36853
  } = snapshot;
36380
36854
  void renderKey;
36381
36855
  const colors = {
36382
- primary: BLUMA_TERMINAL.primary,
36383
- secondary: BLUMA_TERMINAL.accent,
36384
- accent: BLUMA_TERMINAL.accent,
36385
- success: BLUMA_TERMINAL.success,
36386
- warning: BLUMA_TERMINAL.warning,
36387
- error: BLUMA_TERMINAL.err,
36388
- muted: BLUMA_TERMINAL.muted
36856
+ primary: BLUMA_TERMINAL.subtle,
36857
+ secondary: BLUMA_TERMINAL.subtle,
36858
+ accent: BLUMA_TERMINAL.subtle,
36859
+ success: BLUMA_TERMINAL.subtle,
36860
+ warning: BLUMA_TERMINAL.subtle,
36861
+ error: BLUMA_TERMINAL.subtle,
36862
+ muted: BLUMA_TERMINAL.subtle
36389
36863
  };
36390
36864
  const remainingPercent = tokenBudget > 0 ? Math.max(0, Math.round((tokenBudget - tokenCount) / tokenBudget * 100)) : 100;
36391
36865
  let remainingColor = colors.muted;
@@ -36399,27 +36873,27 @@ var StatusBar = memo16(() => {
36399
36873
  const shortWorkdir = workdir ? workdir.split("/").filter(Boolean).pop() || workdir : "";
36400
36874
  const compressionText = isCompressing ? ` \xB7 compressing ${compressionProgress}%` : "";
36401
36875
  const planModeText = isPlanMode ? /* @__PURE__ */ jsxs62(Fragment9, { children: [
36402
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" }),
36403
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "Plan Mode" })
36876
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36877
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Plan Mode" })
36404
36878
  ] }) : null;
36405
36879
  const agentModeIndicator = (() => {
36406
36880
  if (!agentMode || agentMode === "default") return null;
36407
36881
  const mode = agentMode.toLowerCase();
36408
36882
  if (mode === "coordinator") {
36409
36883
  return /* @__PURE__ */ jsxs62(Fragment9, { children: [
36410
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" }),
36411
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "Coordinator" })
36884
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36885
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Coordinator" })
36412
36886
  ] });
36413
36887
  }
36414
36888
  if (mode === "plan") {
36415
36889
  return /* @__PURE__ */ jsxs62(Fragment9, { children: [
36416
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" }),
36417
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "Plan" })
36890
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36891
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Plan" })
36418
36892
  ] });
36419
36893
  }
36420
36894
  return /* @__PURE__ */ jsxs62(Fragment9, { children: [
36421
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" }),
36422
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: agentMode })
36895
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36896
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: agentMode })
36423
36897
  ] });
36424
36898
  })();
36425
36899
  const modelText = modelName ? ` \xB7 model: ${modelName}` : "";
@@ -36435,25 +36909,25 @@ var StatusBar = memo16(() => {
36435
36909
  flexShrink: 0,
36436
36910
  children: [
36437
36911
  shortWorkdir && /* @__PURE__ */ jsxs62(Fragment9, { children: [
36438
- /* @__PURE__ */ jsxs62(Text, { color: BLUMA_TERMINAL.dim, children: [
36912
+ /* @__PURE__ */ jsxs62(Text, { color: BLUMA_TERMINAL.subtle, children: [
36439
36913
  "~/",
36440
36914
  shortWorkdir
36441
36915
  ] }),
36442
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" })
36916
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" })
36443
36917
  ] }),
36444
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "Context" }),
36918
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Context" }),
36445
36919
  /* @__PURE__ */ jsxs62(Text, { color: remainingColor, children: [
36446
36920
  remainingPercent,
36447
36921
  "% left"
36448
36922
  ] }),
36449
36923
  modelText && /* @__PURE__ */ jsxs62(Fragment9, { children: [
36450
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: modelText }),
36451
- /* @__PURE__ */ jsx79(Text, { dimColor: true, children: "\xB7" })
36924
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: modelText }),
36925
+ /* @__PURE__ */ jsx79(Text, { subtleColor: true, children: "\xB7" })
36452
36926
  ] }),
36453
- compressionText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: compressionText }),
36927
+ compressionText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: compressionText }),
36454
36928
  agentModeIndicator,
36455
36929
  planModeText,
36456
- projectsText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: projectsText })
36930
+ projectsText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: projectsText })
36457
36931
  ]
36458
36932
  }
36459
36933
  );
@@ -36841,7 +37315,7 @@ function ToolMessageComponent({
36841
37315
  if (!normalizedResult) return null;
36842
37316
  if (entry?.renderToolResultMessage) {
36843
37317
  const payload = normalizedResult.status === "success" ? normalizedResult.data : normalizedResult;
36844
- return entry.renderToolResultMessage(payload, { verbose: false });
37318
+ return entry.renderToolResultMessage(payload, { verbose: false, args });
36845
37319
  }
36846
37320
  return renderRawJson(normalizedResult);
36847
37321
  })();
@@ -38279,7 +38753,7 @@ var renderCode = () => {
38279
38753
  // src/app/agent/core/thread/thread_store.ts
38280
38754
  import path46 from "path";
38281
38755
  import os34 from "os";
38282
- import { promises as fs42 } from "fs";
38756
+ import { promises as fs43 } from "fs";
38283
38757
  import { randomUUID as randomUUID2 } from "crypto";
38284
38758
  var INDEX_VERSION = 1;
38285
38759
  var fileLocks2 = /* @__PURE__ */ new Map();
@@ -38314,10 +38788,10 @@ var ThreadStore = class {
38314
38788
  * Inicializa o diretório de threads
38315
38789
  */
38316
38790
  async initialize() {
38317
- await fs42.mkdir(this.threadsDir, { recursive: true });
38318
- await fs42.mkdir(this.archiveDir, { recursive: true });
38791
+ await fs43.mkdir(this.threadsDir, { recursive: true });
38792
+ await fs43.mkdir(this.archiveDir, { recursive: true });
38319
38793
  try {
38320
- await fs42.access(this.indexPath);
38794
+ await fs43.access(this.indexPath);
38321
38795
  } catch {
38322
38796
  await this.saveIndex({ version: INDEX_VERSION, threads: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() });
38323
38797
  }
@@ -38346,7 +38820,7 @@ var ThreadStore = class {
38346
38820
  async loadIndex() {
38347
38821
  return withFileLock2(this.indexPath, async () => {
38348
38822
  try {
38349
- const content = await fs42.readFile(this.indexPath, "utf-8");
38823
+ const content = await fs43.readFile(this.indexPath, "utf-8");
38350
38824
  return JSON.parse(content);
38351
38825
  } catch {
38352
38826
  return { version: INDEX_VERSION, threads: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
@@ -38357,8 +38831,8 @@ var ThreadStore = class {
38357
38831
  return withFileLock2(this.indexPath, async () => {
38358
38832
  index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
38359
38833
  const tempPath = `${this.indexPath}.${Date.now()}.tmp`;
38360
- await fs42.writeFile(tempPath, JSON.stringify(index, null, 2), "utf-8");
38361
- await fs42.rename(tempPath, this.indexPath);
38834
+ await fs43.writeFile(tempPath, JSON.stringify(index, null, 2), "utf-8");
38835
+ await fs43.rename(tempPath, this.indexPath);
38362
38836
  });
38363
38837
  }
38364
38838
  // ==================== Git Info ====================
@@ -38554,7 +39028,7 @@ var ThreadStore = class {
38554
39028
  const oldPath = this.getHistoryPath(threadId);
38555
39029
  const newPath = path46.join(this.archiveDir, `${threadId}.jsonl`);
38556
39030
  try {
38557
- await fs42.rename(oldPath, newPath);
39031
+ await fs43.rename(oldPath, newPath);
38558
39032
  } catch (e) {
38559
39033
  if (e.code !== "ENOENT") throw e;
38560
39034
  }
@@ -38578,7 +39052,7 @@ var ThreadStore = class {
38578
39052
  const oldPath = path46.join(this.archiveDir, `${threadId}.jsonl`);
38579
39053
  const newPath = this.getHistoryPath(threadId);
38580
39054
  try {
38581
- await fs42.rename(oldPath, newPath);
39055
+ await fs43.rename(oldPath, newPath);
38582
39056
  } catch (e) {
38583
39057
  if (e.code !== "ENOENT") throw e;
38584
39058
  }
@@ -38599,7 +39073,7 @@ var ThreadStore = class {
38599
39073
  if (entryIndex === -1) return false;
38600
39074
  const entry = index.threads[entryIndex];
38601
39075
  try {
38602
- await fs42.unlink(entry.historyPath);
39076
+ await fs43.unlink(entry.historyPath);
38603
39077
  } catch {
38604
39078
  }
38605
39079
  index.threads.splice(entryIndex, 1);
@@ -38626,7 +39100,7 @@ var ThreadStore = class {
38626
39100
  for (const msg of history.messages) {
38627
39101
  lines.push(JSON.stringify({ type: "message", ...msg }));
38628
39102
  }
38629
- await fs42.writeFile(historyPath, lines.join("\n") + "\n", "utf-8");
39103
+ await fs43.writeFile(historyPath, lines.join("\n") + "\n", "utf-8");
38630
39104
  }
38631
39105
  /**
38632
39106
  * Carrega o histórico de uma thread
@@ -38636,7 +39110,7 @@ var ThreadStore = class {
38636
39110
  const entry = index.threads.find((t) => t.threadId === threadId);
38637
39111
  if (!entry) return null;
38638
39112
  try {
38639
- const content = await fs42.readFile(entry.historyPath, "utf-8");
39113
+ const content = await fs43.readFile(entry.historyPath, "utf-8");
38640
39114
  const lines = content.split("\n").filter(Boolean);
38641
39115
  const history = {
38642
39116
  threadId,
@@ -38669,7 +39143,7 @@ var ThreadStore = class {
38669
39143
  const entry = index.threads.find((t) => t.threadId === threadId);
38670
39144
  if (!entry) throw new Error(`Thread not found: ${threadId}`);
38671
39145
  const lines = messages.map((msg) => JSON.stringify({ type: "message", ...msg }));
38672
- await fs42.appendFile(entry.historyPath, lines.join("\n") + "\n", "utf-8");
39146
+ await fs43.appendFile(entry.historyPath, lines.join("\n") + "\n", "utf-8");
38673
39147
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
38674
39148
  await this.saveIndex(index);
38675
39149
  }
@@ -38957,7 +39431,7 @@ function formatRelativeTime(iso) {
38957
39431
  if (diffDays < 7) return `h\xE1 ${diffDays}d`;
38958
39432
  return formatDate(iso);
38959
39433
  }
38960
- function truncate3(str, max) {
39434
+ function truncate4(str, max) {
38961
39435
  if (str.length <= max) return str;
38962
39436
  return str.slice(0, max - 3) + "...";
38963
39437
  }
@@ -38983,7 +39457,7 @@ async function renderCurrentThread() {
38983
39457
  ] }),
38984
39458
  /* @__PURE__ */ jsxs79(Text, { children: [
38985
39459
  /* @__PURE__ */ jsx96(Text, { dimColor: true, children: "Preview: " }),
38986
- /* @__PURE__ */ jsx96(Text, { dimColor: true, children: truncate3(metadata.preview, 60) })
39460
+ /* @__PURE__ */ jsx96(Text, { dimColor: true, children: truncate4(metadata.preview, 60) })
38987
39461
  ] }),
38988
39462
  /* @__PURE__ */ jsxs79(Text, { children: [
38989
39463
  /* @__PURE__ */ jsx96(Text, { dimColor: true, children: "Modelo: " }),
@@ -38998,7 +39472,7 @@ async function renderCurrentThread() {
38998
39472
  /* @__PURE__ */ jsxs79(Text, { dimColor: true, children: [
38999
39473
  metadata.gitInfo.branch || "?",
39000
39474
  " @ ",
39001
- truncate3(metadata.gitInfo.sha || "?", 8)
39475
+ truncate4(metadata.gitInfo.sha || "?", 8)
39002
39476
  ] })
39003
39477
  ] }),
39004
39478
  /* @__PURE__ */ jsxs79(Text, { children: [
@@ -39066,7 +39540,7 @@ async function renderThreadList(args) {
39066
39540
  }
39067
39541
  var ThreadListItem = ({ thread, isActive, index }) => {
39068
39542
  const statusColor = thread.status === "archived" ? BLUMA_TERMINAL.warning : BLUMA_TERMINAL.success;
39069
- const name = thread.name || truncate3(thread.preview, 40);
39543
+ const name = thread.name || truncate4(thread.preview, 40);
39070
39544
  return /* @__PURE__ */ jsxs79(Box_default, { flexDirection: "column", children: [
39071
39545
  /* @__PURE__ */ jsxs79(Box_default, { gap: 1, children: [
39072
39546
  /* @__PURE__ */ jsxs79(Text, { dimColor: true, children: [
@@ -41535,15 +42009,15 @@ import semverGt from "semver/functions/gt.js";
41535
42009
  import semverValid from "semver/functions/valid.js";
41536
42010
  import { fileURLToPath as fileURLToPath6 } from "url";
41537
42011
  import path47 from "path";
41538
- import fs43 from "fs";
42012
+ import fs44 from "fs";
41539
42013
  var BLUMA_PACKAGE_NAME = "@nomad-e/bluma-cli";
41540
42014
  function findBlumaPackageJson(startDir) {
41541
42015
  let dir = startDir;
41542
42016
  for (let i = 0; i < 12; i++) {
41543
42017
  const candidate = path47.join(dir, "package.json");
41544
- if (fs43.existsSync(candidate)) {
42018
+ if (fs44.existsSync(candidate)) {
41545
42019
  try {
41546
- const raw = fs43.readFileSync(candidate, "utf8");
42020
+ const raw = fs44.readFileSync(candidate, "utf8");
41547
42021
  const parsed = JSON.parse(raw);
41548
42022
  if (parsed?.name === BLUMA_PACKAGE_NAME && parsed?.version) {
41549
42023
  return { name: parsed.name, version: String(parsed.version) };
@@ -41574,8 +42048,8 @@ function resolveInstalledBlumaPackage() {
41574
42048
  if (argv1 && !argv1.startsWith("-")) {
41575
42049
  try {
41576
42050
  let resolved = argv1;
41577
- if (path47.isAbsolute(argv1) && fs43.existsSync(argv1)) {
41578
- resolved = fs43.realpathSync(argv1);
42051
+ if (path47.isAbsolute(argv1) && fs44.existsSync(argv1)) {
42052
+ resolved = fs44.realpathSync(argv1);
41579
42053
  } else {
41580
42054
  resolved = path47.resolve(process.cwd(), argv1);
41581
42055
  }
@@ -42398,16 +42872,16 @@ function usePlanMode() {
42398
42872
 
42399
42873
  // src/app/hooks/useAgentMode.ts
42400
42874
  import { useState as useState22, useEffect as useEffect20, useCallback as useCallback9 } from "react";
42401
- import * as fs44 from "fs";
42875
+ import * as fs45 from "fs";
42402
42876
  import * as path48 from "path";
42403
42877
  import { homedir as homedir3 } from "os";
42404
42878
  var SETTINGS_PATH = path48.join(homedir3(), ".bluma", "settings.json");
42405
42879
  function readAgentModeFromFile() {
42406
42880
  try {
42407
- if (!fs44.existsSync(SETTINGS_PATH)) {
42881
+ if (!fs45.existsSync(SETTINGS_PATH)) {
42408
42882
  return "default";
42409
42883
  }
42410
- const content = fs44.readFileSync(SETTINGS_PATH, "utf-8");
42884
+ const content = fs45.readFileSync(SETTINGS_PATH, "utf-8");
42411
42885
  const settings = JSON.parse(content);
42412
42886
  return settings.agentMode || "default";
42413
42887
  } catch (error) {
@@ -42426,16 +42900,16 @@ function useAgentMode() {
42426
42900
  }, []);
42427
42901
  const updateAgentMode = useCallback9((mode) => {
42428
42902
  try {
42429
- if (!fs44.existsSync(SETTINGS_PATH)) {
42430
- fs44.mkdirSync(path48.dirname(SETTINGS_PATH), { recursive: true });
42903
+ if (!fs45.existsSync(SETTINGS_PATH)) {
42904
+ fs45.mkdirSync(path48.dirname(SETTINGS_PATH), { recursive: true });
42431
42905
  }
42432
42906
  let settings = {};
42433
- if (fs44.existsSync(SETTINGS_PATH)) {
42434
- const content = fs44.readFileSync(SETTINGS_PATH, "utf-8");
42907
+ if (fs45.existsSync(SETTINGS_PATH)) {
42908
+ const content = fs45.readFileSync(SETTINGS_PATH, "utf-8");
42435
42909
  settings = JSON.parse(content);
42436
42910
  }
42437
42911
  settings.agentMode = mode;
42438
- fs44.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
42912
+ fs45.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
42439
42913
  setAgentMode(mode);
42440
42914
  } catch (error) {
42441
42915
  console.error("Failed to update agent mode:", error);
@@ -43691,9 +44165,9 @@ async function runAgentMode() {
43691
44165
  try {
43692
44166
  if (inputFileIndex !== -1 && args[inputFileIndex + 1]) {
43693
44167
  const filePath = args[inputFileIndex + 1];
43694
- rawPayload = fs45.readFileSync(filePath, "utf-8");
44168
+ rawPayload = fs46.readFileSync(filePath, "utf-8");
43695
44169
  } else {
43696
- rawPayload = fs45.readFileSync(0, "utf-8");
44170
+ rawPayload = fs46.readFileSync(0, "utf-8");
43697
44171
  }
43698
44172
  } catch (err) {
43699
44173
  writeAgentEvent(registrySessionId, {
@@ -43893,7 +44367,7 @@ function readCliPackageVersion() {
43893
44367
  try {
43894
44368
  const base = path49.dirname(fileURLToPath7(import.meta.url));
43895
44369
  const pkgPath = path49.join(base, "..", "package.json");
43896
- const j = JSON.parse(fs45.readFileSync(pkgPath, "utf8"));
44370
+ const j = JSON.parse(fs46.readFileSync(pkgPath, "utf8"));
43897
44371
  return String(j.version || "0.0.0");
43898
44372
  } catch {
43899
44373
  return "0.0.0";
@@ -44019,7 +44493,7 @@ function startBackgroundAgent() {
44019
44493
  process.exit(1);
44020
44494
  }
44021
44495
  const filePath = args[inputFileIndex + 1];
44022
- const rawPayload = fs45.readFileSync(filePath, "utf-8");
44496
+ const rawPayload = fs46.readFileSync(filePath, "utf-8");
44023
44497
  const envelope = JSON.parse(rawPayload);
44024
44498
  const sessionId = envelope.session_id || envelope.message_id || uuidv412();
44025
44499
  registerSession({