@nomad-e/bluma-cli 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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),
@@ -15362,7 +15366,7 @@ function createDiff(filename, oldContent, newContent) {
15362
15366
  const prefix = part.added ? "+" : part.removed ? "-" : " ";
15363
15367
  const lines = part.value.split("\n").slice(0, -1);
15364
15368
  for (const line of lines) {
15365
- if (line === "\") continue;
15369
+ if (line === "") continue;
15366
15370
  diffString += `${prefix}${line}
15367
15371
  `;
15368
15372
  lineCount++;
@@ -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,90 +26813,477 @@ ${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({
26756
- messages: contextWindow,
26757
- temperature: 0,
26758
- tools: this.mcpClient.getAvailableTools(),
26759
- parallel_tool_calls: true,
26760
- userContext: this.getLlmUserContext()
26761
- });
26762
- if (this.isInterrupted) {
26763
- this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26764
- return;
26765
- }
26766
- let message2 = response.choices[0].message;
26767
- message2 = ToolCallNormalizer.normalizeAssistantMessage(message2);
26768
- if (message2.reasoning_content || message2.reasoning) {
26769
- const reasoningText = message2.reasoning_content || message2.reasoning;
26770
- this.eventBus.emit("backend_message", {
26771
- type: "reasoning",
26772
- content: typeof reasoningText === "string" ? reasoningText : JSON.stringify(reasoningText)
26773
- });
26774
- }
26775
- this.emitSyntheticStreamForNonStreamingMessage(message2);
26776
- this.history.push(message2);
26777
- if (message2.tool_calls && message2.tool_calls.length > 0) {
26778
- this.emptyAssistantReplySteps = 0;
26779
- this.invalidToolCallRetrySteps = 0;
26780
- const validToolCalls = message2.tool_calls.filter(
26781
- (call) => ToolCallNormalizer.isValidToolCall(call)
26782
- );
26783
- if (validToolCalls.length === 0) {
26784
- await this.handleInvalidToolCallRetry(message2);
26785
- return;
26825
+ async handleNonStreamingResponse(contextWindow) {
26826
+ const response = await this.deps.llm.chatCompletion({
26827
+ messages: contextWindow,
26828
+ temperature: 0,
26829
+ tools: this.deps.mcpClient.getAvailableTools(),
26830
+ parallel_tool_calls: true,
26831
+ userContext: this.deps.getLlmUserContext()
26832
+ });
26833
+ if (this.deps.isInterrupted()) {
26834
+ this.deps.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
26835
+ return;
26836
+ }
26837
+ let message2 = response.choices[0].message;
26838
+ message2 = ToolCallNormalizer.normalizeAssistantMessage(message2);
26839
+ if (message2.reasoning_content || message2.reasoning) {
26840
+ const reasoningText = message2.reasoning_content || message2.reasoning;
26841
+ this.deps.eventBus.emit("backend_message", {
26842
+ type: "reasoning",
26843
+ content: typeof reasoningText === "string" ? reasoningText : JSON.stringify(reasoningText)
26844
+ });
26845
+ }
26846
+ this.deps.history.push(message2);
26847
+ if (message2.tool_calls && message2.tool_calls.length > 0) {
26848
+ this.emptyAssistantReplySteps = 0;
26849
+ this.invalidToolCallRetrySteps = 0;
26850
+ const validToolCalls = message2.tool_calls.filter(
26851
+ (call) => ToolCallNormalizer.isValidToolCall(call)
26852
+ );
26853
+ if (validToolCalls.length === 0) {
26854
+ await this.handleInvalidToolCallRetry(message2);
26855
+ return;
26856
+ }
26857
+ const autoApprovedToolCalls = validToolCalls.filter(
26858
+ (tc) => effectiveToolAutoApprove(tc, this.deps.sessionId)
26859
+ );
26860
+ const confirmationToolCalls = validToolCalls.filter(
26861
+ (tc) => !effectiveToolAutoApprove(tc, this.deps.sessionId)
26862
+ );
26863
+ if (autoApprovedToolCalls.length > 0) {
26864
+ await this.handleToolResponse({
26865
+ type: "user_decision_execute",
26866
+ tool_calls: autoApprovedToolCalls,
26867
+ continueConversation: confirmationToolCalls.length === 0
26868
+ });
26869
+ }
26870
+ if (confirmationToolCalls.length > 0) {
26871
+ const toolToCall = confirmationToolCalls[0];
26872
+ const decision = decideToolExecution(toolToCall.function.name, void 0, this.deps.sessionId);
26873
+ const toolName = toolToCall.function.name;
26874
+ let previewContent;
26875
+ if (toolName === "edit_tool") {
26876
+ const args = JSON.parse(toolToCall.function.arguments);
26877
+ previewContent = await this.deps.generateEditPreview(args);
26878
+ } else if (toolName === "file_write") {
26879
+ const args = JSON.parse(toolToCall.function.arguments);
26880
+ previewContent = this.deps.generateFileWritePreview(args);
26881
+ }
26882
+ this.deps.eventBus.emit("backend_message", {
26883
+ type: "confirmation_request",
26884
+ tool_calls: confirmationToolCalls,
26885
+ preview: previewContent,
26886
+ tool_policy: decision
26887
+ });
26888
+ }
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);
26786
27225
  }
26787
- const autoApprovedToolCalls = validToolCalls.filter(
26788
- (tc) => effectiveToolAutoApprove(tc, this.sessionId)
26789
- );
26790
- const confirmationToolCalls = validToolCalls.filter(
26791
- (tc) => !effectiveToolAutoApprove(tc, this.sessionId)
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
26792
27234
  );
26793
- if (autoApprovedToolCalls.length > 0) {
26794
- await this.handleToolResponse({
26795
- type: "user_decision_execute",
26796
- tool_calls: autoApprovedToolCalls,
26797
- continueConversation: confirmationToolCalls.length === 0
26798
- });
27235
+ if (editData.error) {
27236
+ return `Failed to generate diff:
27237
+
27238
+ ${editData.error.display}`;
26799
27239
  }
26800
- if (confirmationToolCalls.length > 0) {
26801
- const toolToCall = confirmationToolCalls[0];
26802
- const decision = decideToolExecution(toolToCall.function.name, void 0, this.sessionId);
26803
- const toolName = toolToCall.function.name;
26804
- let previewContent;
26805
- if (toolName === "edit_tool") {
26806
- const args = JSON.parse(toolToCall.function.arguments);
26807
- previewContent = await this._generateEditPreview(args);
26808
- } else if (toolName === "file_write") {
26809
- const args = JSON.parse(toolToCall.function.arguments);
26810
- previewContent = this._generateFileWritePreview(args);
26811
- }
26812
- this.eventBus.emit("backend_message", {
26813
- type: "confirmation_request",
26814
- tool_calls: confirmationToolCalls,
26815
- preview: previewContent,
26816
- tool_policy: decision
26817
- });
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)";
26818
27251
  }
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();
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}`;
26828
27261
  }
26829
27262
  }
27263
+ getLlmUserContext() {
27264
+ if (!this.activeTurnContext) {
27265
+ throw new Error("BluMaAgent: activeTurnContext ausente (processTurn n\xE3o iniciou o turno).");
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);
27286
+ }
26830
27287
  emitTurnCompleted() {
26831
27288
  this.eventBus.emit("backend_message", { type: "done", status: "completed" });
26832
27289
  void this.runAutoDreamIfEnabled();
@@ -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,11 @@ 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__ */ jsx12(Box_default, { width: "100%", flexDirection: "row", children: /* @__PURE__ */ jsx12(Text, { children: tokens.map((t, ti) => /* @__PURE__ */ jsx12(Text, { color: t.color, children: t.text }, ti)) }) }, idx);
28668
29090
  }),
28669
29091
  hidden > 0 && /* @__PURE__ */ jsxs5(Text, { color: THEME.lineNumber, dimColor: true, children: [
28670
29092
  "\u2026 +",
@@ -28735,6 +29157,11 @@ function headingAccent(depth) {
28735
29157
  if (depth === 3) return BLUMA_TERMINAL.magenta;
28736
29158
  return BLUMA_TERMINAL.inactive;
28737
29159
  }
29160
+ function normalizeMarkdownText(content) {
29161
+ const trimmed = content.trim();
29162
+ if (!trimmed) return "";
29163
+ return trimmed.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n");
29164
+ }
28738
29165
  function listMarker(ordered, index, start, task, checked) {
28739
29166
  if (task) return checked ? "[x]" : "[ ]";
28740
29167
  if (ordered) {
@@ -28928,6 +29355,46 @@ function renderParagraph(p, key, compact) {
28928
29355
  const nodes = walkInline(p.tokens, key);
28929
29356
  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
29357
  }
29358
+ function renderCodeBlock(code, key) {
29359
+ const raw = code.text.replace(/\n$/, "");
29360
+ return /* @__PURE__ */ jsxs6(
29361
+ Box_default,
29362
+ {
29363
+ flexDirection: "column",
29364
+ marginBottom: 1,
29365
+ paddingX: 1,
29366
+ paddingY: 0,
29367
+ backgroundColor: BLUMA_TERMINAL.surfaceContainer,
29368
+ borderStyle: "round",
29369
+ borderColor: BLUMA_TERMINAL.outlineVariant,
29370
+ children: [
29371
+ code.lang ? /* @__PURE__ */ jsx13(Box_default, { marginBottom: 0, children: /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, bold: true, children: code.lang }) }) : null,
29372
+ /* @__PURE__ */ jsx13(HighlightedCode, { code: raw, filePath: code.lang || "snippet" })
29373
+ ]
29374
+ },
29375
+ key
29376
+ );
29377
+ }
29378
+ function renderBlockquote(q, key) {
29379
+ 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 });
29380
+ return /* @__PURE__ */ jsxs6(
29381
+ Box_default,
29382
+ {
29383
+ flexDirection: "row",
29384
+ marginBottom: 1,
29385
+ alignItems: "flex-start",
29386
+ paddingX: 1,
29387
+ borderStyle: "round",
29388
+ borderColor: BLUMA_TERMINAL.outlineVariant,
29389
+ backgroundColor: BLUMA_TERMINAL.surfaceContainer,
29390
+ children: [
29391
+ /* @__PURE__ */ jsx13(Text, { color: BLUMA_TERMINAL.blue, children: "\u2502 " }),
29392
+ /* @__PURE__ */ jsx13(Box_default, { flexDirection: "column", flexGrow: 1, children: inner })
29393
+ ]
29394
+ },
29395
+ key
29396
+ );
29397
+ }
28931
29398
  function renderListItemBlocks(item, depth, keyBase) {
28932
29399
  if (!item.tokens?.length) {
28933
29400
  const nodes = walkInline(
@@ -28963,13 +29430,6 @@ function renderListBlock(list, depth, keyBase) {
28963
29430
  ] }, `${keyBase}-row-${idx}`);
28964
29431
  }) }, keyBase);
28965
29432
  }
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
29433
  function renderBlockTokens(tokens, keyRoot) {
28974
29434
  const elements = [];
28975
29435
  for (let i = 0; i < tokens.length; i++) {
@@ -28992,17 +29452,9 @@ function renderBlockTokens(tokens, keyRoot) {
28992
29452
  case "list":
28993
29453
  elements.push(renderListBlock(token, 0, key));
28994
29454
  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
- );
29455
+ case "code":
29456
+ elements.push(renderCodeBlock(token, key));
29004
29457
  break;
29005
- }
29006
29458
  case "blockquote":
29007
29459
  elements.push(renderBlockquote(token, key));
29008
29460
  break;
@@ -29059,7 +29511,8 @@ var MarkdownRendererComponent = ({ markdown }) => {
29059
29511
  if (!markdown || markdown.trim() === "") {
29060
29512
  return null;
29061
29513
  }
29062
- const tokens = cachedLexer(markdown);
29514
+ const normalized = normalizeMarkdownText(markdown);
29515
+ const tokens = cachedLexer(normalized);
29063
29516
  return /* @__PURE__ */ jsx13(Box_default, { flexDirection: "column", paddingRight: 1, children: renderBlockTokens(tokens, "md") });
29064
29517
  };
29065
29518
  var MarkdownRenderer = memo4(MarkdownRendererComponent, (prevProps, nextProps) => {
@@ -30618,9 +31071,12 @@ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
30618
31071
  function renderToolHeader6({ args }) {
30619
31072
  const cmd = args?.command ?? "";
30620
31073
  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: [
31074
+ return /* @__PURE__ */ jsxs20(Box_default, { flexDirection: "row", alignItems: "flex-end", children: [
30622
31075
  /* @__PURE__ */ jsx33(Text, { bold: true, color: BLUMA_TERMINAL.m3OnSurface, children: "Ran" }),
30623
- /* @__PURE__ */ jsx33(Text, { color: BLUMA_TERMINAL.dim, children: displayCmd })
31076
+ /* @__PURE__ */ jsxs20(Text, { color: BLUMA_TERMINAL.dim, children: [
31077
+ " ",
31078
+ displayCmd
31079
+ ] })
30624
31080
  ] });
30625
31081
  }
30626
31082
  function renderToolUseMessage6({ args }) {
@@ -31965,6 +32421,46 @@ function getPatchForEdit({
31965
32421
  }));
31966
32422
  return { patch, updatedFile: unescapeFromDiff(updatedFile) };
31967
32423
  }
32424
+ function getPatchFromContents({
32425
+ filePath,
32426
+ oldContent,
32427
+ newContent
32428
+ }) {
32429
+ const result = structuredPatch(
32430
+ filePath,
32431
+ filePath,
32432
+ escapeForDiff(oldContent),
32433
+ escapeForDiff(newContent),
32434
+ void 0,
32435
+ void 0,
32436
+ {
32437
+ context: CONTEXT_LINES,
32438
+ timeout: DIFF_TIMEOUT_MS
32439
+ }
32440
+ );
32441
+ if (!result) {
32442
+ return [];
32443
+ }
32444
+ return result.hunks.map((h) => ({
32445
+ ...h,
32446
+ lines: h.lines.map(unescapeFromDiff)
32447
+ }));
32448
+ }
32449
+ function patchToUnifiedDiffText(filePath, patch) {
32450
+ if (patch.length === 0) return "";
32451
+ let text = `--- a/${filePath}
32452
+ +++ b/${filePath}
32453
+ `;
32454
+ for (const hunk of patch) {
32455
+ text += `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@
32456
+ `;
32457
+ for (const line of hunk.lines) {
32458
+ text += `${line}
32459
+ `;
32460
+ }
32461
+ }
32462
+ return text;
32463
+ }
31968
32464
 
31969
32465
  // src/app/ui/utils/readEditContext.ts
31970
32466
  import { promises as fs41 } from "fs";
@@ -32064,14 +32560,64 @@ function diffSummary(additions, removals, hidden) {
32064
32560
  if (hidden > 0) bits.push(`\u22EF ${hidden} hidden`);
32065
32561
  return bits.join(" \xB7 ");
32066
32562
  }
32563
+ var THEME2 = {
32564
+ keyword: "#569CD6",
32565
+ string: "#CE9178",
32566
+ number: "#B5CEA8",
32567
+ comment: "#6A9955",
32568
+ //#22c55e
32569
+ function: "#DCDCAA",
32570
+ variable: "#9CDCFE",
32571
+ operator: "#D4D4D4",
32572
+ punctuation: "#D4D4D4",
32573
+ default: "#D4D4D4",
32574
+ lineNumber: "#858585",
32575
+ gutter: "#404040"
32576
+ };
32577
+ var KEYWORDS2 = /* @__PURE__ */ new Set([
32578
+ "const",
32579
+ "let",
32580
+ "var",
32581
+ "function",
32582
+ "return",
32583
+ "if",
32584
+ "else",
32585
+ "for",
32586
+ "while",
32587
+ "switch",
32588
+ "case",
32589
+ "break",
32590
+ "continue",
32591
+ "class",
32592
+ "extends",
32593
+ "import",
32594
+ "from",
32595
+ "export",
32596
+ "default",
32597
+ "async",
32598
+ "await",
32599
+ "try",
32600
+ "catch",
32601
+ "finally",
32602
+ "new",
32603
+ "typeof",
32604
+ "instanceof",
32605
+ "null",
32606
+ "undefined",
32607
+ "true",
32608
+ "false",
32609
+ "type",
32610
+ "interface",
32611
+ "enum",
32612
+ "implements",
32613
+ "abstract",
32614
+ "readonly",
32615
+ "override"
32616
+ ]);
32067
32617
  function hunkToRows(hunk) {
32068
32618
  const rows = [];
32069
32619
  let oldLine = hunk.oldStart;
32070
32620
  let newLine = hunk.newStart;
32071
- rows.push({
32072
- kind: "meta",
32073
- text: `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`
32074
- });
32075
32621
  for (const raw of hunk.lines) {
32076
32622
  if (!raw.length) continue;
32077
32623
  const prefix = raw[0];
@@ -32112,22 +32658,22 @@ function pairWordDiffs(rows) {
32112
32658
  }
32113
32659
  return out;
32114
32660
  }
32115
- function maxLineNums(rows) {
32116
- let maxO = 0;
32661
+ function calcGutterW(rows) {
32117
32662
  let maxN = 0;
32118
32663
  for (const r of rows) {
32119
- if (r.kind === "ctx") {
32120
- maxO = Math.max(maxO, r.old);
32121
- maxN = Math.max(maxN, r.new);
32122
- } else if (r.kind === "rem") {
32123
- maxO = Math.max(maxO, r.old);
32124
- } else if (r.kind === "add") {
32125
- maxN = Math.max(maxN, r.new);
32126
- }
32664
+ if (r.kind === "ctx") maxN = Math.max(maxN, r.old, r.new);
32665
+ else if (r.kind === "rem") maxN = Math.max(maxN, r.old);
32666
+ else if (r.kind === "add") maxN = Math.max(maxN, r.new);
32127
32667
  }
32128
- const wOld = Math.max(2, String(maxO || 0).length);
32129
- const wNew = Math.max(2, String(maxN || 0).length);
32130
- return { wOld, wNew };
32668
+ return Math.max(2, String(maxN || 0).length);
32669
+ }
32670
+ function calcLegacyGutterW(segs) {
32671
+ let maxN = 0;
32672
+ for (const s of segs) {
32673
+ if (s.kind === "rem") maxN = Math.max(maxN, s.oldLine);
32674
+ if (s.kind === "add") maxN = Math.max(maxN, s.newLine);
32675
+ }
32676
+ return Math.max(2, String(maxN || 0).length);
32131
32677
  }
32132
32678
  function rowLineCounts(rows) {
32133
32679
  let additions = 0;
@@ -32138,21 +32684,11 @@ function rowLineCounts(rows) {
32138
32684
  }
32139
32685
  return { additions, removals };
32140
32686
  }
32141
- function gutterCtx(wOld, wNew, oldN, newN) {
32142
- return `${String(oldN).padStart(wOld)} ${String(newN).padStart(wNew)} \u2502 `;
32143
- }
32144
- function gutterRem(wOld, wNew, oldN) {
32145
- return `${String(oldN).padStart(wOld)} ${" ".repeat(wNew)} \u2502\u2212`;
32687
+ function gutterLine(w, n) {
32688
+ return ` ${String(n).padEnd(w)} `;
32146
32689
  }
32147
- function gutterAdd(wOld, wNew, newN) {
32148
- return `${" ".repeat(wOld)} ${String(newN).padStart(wNew)} \u2502+`;
32149
- }
32150
- function gutterOmit(wOld, wNew) {
32151
- const cols = wOld + 1 + wNew;
32152
- const dot = "\u22EE";
32153
- const padL = Math.max(0, Math.floor((cols - dot.length) / 2));
32154
- const padR = Math.max(0, cols - dot.length - padL);
32155
- return `${" ".repeat(padL)}${dot}${" ".repeat(padR)} \u2502 `;
32690
+ function gutterOmit(w) {
32691
+ return ` ${"\u22EE".padEnd(w)} `;
32156
32692
  }
32157
32693
  function WordDiffChunks({
32158
32694
  parts,
@@ -32170,11 +32706,84 @@ function WordDiffChunks({
32170
32706
  /* @__PURE__ */ jsx41(Text, { backgroundColor: bg, color: BLUMA_TERMINAL.m3OnSurface, wrap: "wrap", children: p.value }, k++)
32171
32707
  );
32172
32708
  }
32173
- return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", children: chunks });
32709
+ return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", flexGrow: 1, children: chunks });
32710
+ }
32711
+ function collapseContextRows(rows) {
32712
+ const out = [];
32713
+ let ctx = [];
32714
+ const flush = () => {
32715
+ if (ctx.length <= 3) {
32716
+ out.push(...ctx);
32717
+ } else {
32718
+ out.push({ kind: "omit", count: ctx.length });
32719
+ }
32720
+ ctx = [];
32721
+ };
32722
+ for (const r of rows) {
32723
+ if (r.kind === "ctx") {
32724
+ ctx.push(r);
32725
+ continue;
32726
+ }
32727
+ flush();
32728
+ out.push(r);
32729
+ }
32730
+ flush();
32731
+ return out;
32732
+ }
32733
+ function tokenizeLine2(line, _language) {
32734
+ if (!line.trim()) return [{ text: line || " ", color: THEME2.default }];
32735
+ const tokens = [];
32736
+ let i = 0;
32737
+ while (i < line.length) {
32738
+ if (/\s/.test(line[i])) {
32739
+ let spaces = "";
32740
+ while (i < line.length && /\s/.test(line[i])) spaces += line[i++];
32741
+ tokens.push({ text: spaces, color: THEME2.default });
32742
+ continue;
32743
+ }
32744
+ if (line[i] === "/" && line[i + 1] === "/") {
32745
+ tokens.push({ text: line.slice(i), color: THEME2.comment });
32746
+ break;
32747
+ }
32748
+ if (line[i] === "#") {
32749
+ tokens.push({ text: line.slice(i), color: THEME2.comment });
32750
+ break;
32751
+ }
32752
+ if (line[i] === '"' || line[i] === "'" || line[i] === "`") {
32753
+ const quote = line[i];
32754
+ let j = i + 1;
32755
+ while (j < line.length && line[j] !== quote) {
32756
+ if (line[j] === "\\") j++;
32757
+ j++;
32758
+ }
32759
+ tokens.push({ text: line.slice(i, j + 1), color: THEME2.string });
32760
+ i = j + 1;
32761
+ continue;
32762
+ }
32763
+ if (/\d/.test(line[i])) {
32764
+ let num = "";
32765
+ while (i < line.length && /[\d.]/.test(line[i])) num += line[i++];
32766
+ tokens.push({ text: num, color: THEME2.number });
32767
+ continue;
32768
+ }
32769
+ if (/[a-zA-Z_$]/.test(line[i])) {
32770
+ let word = "";
32771
+ while (i < line.length && /[\w$]/.test(line[i])) word += line[i++];
32772
+ const isCall = line[i] === "(";
32773
+ let color = THEME2.variable;
32774
+ if (KEYWORDS2.has(word)) color = THEME2.keyword;
32775
+ else if (isCall) color = THEME2.function;
32776
+ tokens.push({ text: word, color });
32777
+ continue;
32778
+ }
32779
+ tokens.push({ text: line[i], color: THEME2.punctuation });
32780
+ i++;
32781
+ }
32782
+ return tokens;
32174
32783
  }
32175
32784
  function renderStructuredRows(rows, maxHeight) {
32176
32785
  const paired = collapseContextRows(pairWordDiffs(rows));
32177
- const { wOld, wNew } = maxLineNums(paired);
32786
+ const w = calcGutterW(paired);
32178
32787
  const { additions, removals } = rowLineCounts(paired);
32179
32788
  const nodes = [];
32180
32789
  let lineBudget = maxHeight > 0 ? maxHeight : Number.POSITIVE_INFINITY;
@@ -32191,11 +32800,13 @@ function renderStructuredRows(rows, maxHeight) {
32191
32800
  for (const r of paired) {
32192
32801
  if (r.kind === "omit") {
32193
32802
  push(
32194
- /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", children: [
32195
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: gutterOmit(wOld, wNew) }),
32196
- r.count > 1 ? /* @__PURE__ */ jsxs26(Text, { dimColor: true, wrap: "wrap", children: [
32803
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, paddingLeft: 5, children: [
32804
+ /* @__PURE__ */ jsxs26(Text, { dimColor: true, children: [
32197
32805
  " ",
32198
- "\xB7 ",
32806
+ gutterOmit(w)
32807
+ ] }),
32808
+ r.count > 1 ? /* @__PURE__ */ jsxs26(Text, { dimColor: true, wrap: "wrap", children: [
32809
+ " \xB7 ",
32199
32810
  r.count
32200
32811
  ] }) : null
32201
32812
  ] }, `o-${idx++}`)
@@ -32204,33 +32815,32 @@ function renderStructuredRows(rows, maxHeight) {
32204
32815
  }
32205
32816
  if (r.kind === "meta") {
32206
32817
  push(
32207
- /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", width: "100%", children: /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: r.text }) }, `m-${idx++}`)
32818
+ /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, children: tokenizeLine2(r.text || " ", "ts").map((t, i) => /* @__PURE__ */ jsx41(Text, { color: t.color, children: t.text }, i)) }, `m-${idx++}`)
32208
32819
  );
32209
32820
  continue;
32210
32821
  }
32211
32822
  if (r.kind === "ctx") {
32212
32823
  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) }),
32215
- /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.muted, wrap: "wrap", children: r.text || " " })
32216
- ] }, `c-${idx++}`)
32824
+ /* @__PURE__ */ jsx41(Box_default, { flexDirection: "row", flexWrap: "wrap", width: "100%", flexGrow: 1, children: tokenizeLine2(r.text || " ", "ts").map((t, i) => /* @__PURE__ */ jsx41(Text, { color: t.color, children: t.text }, i)) }, `c-${idx++}`)
32217
32825
  );
32218
32826
  continue;
32219
32827
  }
32220
32828
  if (r.kind === "rem") {
32221
32829
  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 || " " })
32830
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffRemoved, paddingLeft: 3, children: [
32831
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.dim, children: gutterLine(w, r.old) }),
32832
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: "-" }),
32833
+ 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", children: tokenizeLine2(r.text || " ", "ts").map((t, i) => /* @__PURE__ */ jsx41(Text, { color: t.color, children: t.text }, i)) })
32225
32834
  ] }, `r-${idx++}`)
32226
32835
  );
32227
32836
  continue;
32228
32837
  }
32229
32838
  if (r.kind === "add") {
32230
32839
  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 || " " })
32840
+ /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "row", width: "100%", flexGrow: 1, flexWrap: "wrap", backgroundColor: BLUMA_TERMINAL.diffAdded, paddingLeft: 3, children: [
32841
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.dim, children: gutterLine(w, r.new) }),
32842
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: "+" }),
32843
+ 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(Box_default, { flexDirection: "row", flexWrap: "wrap", children: tokenizeLine2(r.text || " ", "ts").map((t, i) => /* @__PURE__ */ jsx41(Text, { color: t.color, children: t.text }, i)) })
32234
32844
  ] }, `a-${idx++}`)
32235
32845
  );
32236
32846
  }
@@ -32255,15 +32865,18 @@ function legacyDiffToSegs(lines) {
32255
32865
  const isNewHeader = line.startsWith("+++");
32256
32866
  if (isOldHeader || isNewHeader) {
32257
32867
  flushCtx();
32258
- out.push({ kind: "header", line });
32259
32868
  if (isNewHeader) {
32260
32869
  oldLine = 1;
32261
32870
  newLine = 1;
32262
32871
  }
32263
32872
  continue;
32264
32873
  }
32265
- const isRem = line.startsWith("-") && !isOldHeader;
32266
- const isAdd = line.startsWith("+") && !isNewHeader;
32874
+ if (line.startsWith("\\")) {
32875
+ flushCtx();
32876
+ continue;
32877
+ }
32878
+ const isRem = line.startsWith("-");
32879
+ const isAdd = line.startsWith("+");
32267
32880
  if (isRem) {
32268
32881
  flushCtx();
32269
32882
  out.push({ kind: "rem", body: line.slice(1), oldLine });
@@ -32281,11 +32894,6 @@ function legacyDiffToSegs(lines) {
32281
32894
  ctx += 1;
32282
32895
  continue;
32283
32896
  }
32284
- if (line.startsWith("\\")) {
32285
- flushCtx();
32286
- out.push({ kind: "raw", line });
32287
- continue;
32288
- }
32289
32897
  if (line.startsWith("[")) {
32290
32898
  flushCtx();
32291
32899
  out.push({ kind: "raw", line });
@@ -32301,18 +32909,6 @@ function legacyDiffToSegs(lines) {
32301
32909
  flushCtx();
32302
32910
  return out;
32303
32911
  }
32304
- function legacySegWidths(segs) {
32305
- let maxO = 0;
32306
- let maxN = 0;
32307
- for (const s of segs) {
32308
- if (s.kind === "rem") maxO = Math.max(maxO, s.oldLine);
32309
- if (s.kind === "add") maxN = Math.max(maxN, s.newLine);
32310
- }
32311
- return {
32312
- wOld: Math.max(2, String(maxO || 0).length),
32313
- wNew: Math.max(2, String(maxN || 0).length)
32314
- };
32315
- }
32316
32912
  function legacySegLineCounts(lines) {
32317
32913
  let additions = 0;
32318
32914
  let removals = 0;
@@ -32322,75 +32918,104 @@ function legacySegLineCounts(lines) {
32322
32918
  }
32323
32919
  return { additions, removals };
32324
32920
  }
32325
- function collapseContextRows(rows) {
32326
- const out = [];
32327
- let ctxCount = 0;
32328
- const flush = () => {
32329
- if (ctxCount > 0) {
32330
- out.push({ kind: "omit", count: ctxCount });
32331
- ctxCount = 0;
32332
- }
32333
- };
32334
- for (const r of rows) {
32335
- if (r.kind === "ctx") {
32336
- ctxCount += 1;
32337
- continue;
32338
- }
32339
- if (r.kind === "meta" && r.text.startsWith("@@")) {
32340
- ctxCount += 1;
32341
- continue;
32342
- }
32343
- flush();
32344
- out.push(r);
32345
- }
32346
- flush();
32347
- return out;
32348
- }
32349
32921
  function LegacyDiffBody({
32350
32922
  lines,
32351
32923
  maxHeight
32352
32924
  }) {
32353
32925
  const segs = legacyDiffToSegs(lines);
32354
- const { wOld, wNew } = legacySegWidths(segs);
32926
+ const w = calcLegacyGutterW(segs);
32355
32927
  const truncated = maxHeight > 0 && segs.length > maxHeight;
32356
32928
  const toRender = truncated ? segs.slice(0, maxHeight) : segs;
32357
32929
  const hiddenBelow = truncated ? segs.length - toRender.length : 0;
32358
32930
  const { additions, removals } = legacySegLineCounts(lines);
32359
- const summary = diffSummary(additions, removals, hiddenBelow);
32360
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32361
- toRender.map((seg, index) => {
32362
- if (seg.kind === "header") {
32363
- return /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: seg.line }, index);
32364
- }
32365
- 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) }),
32368
- seg.count > 1 ? /* @__PURE__ */ jsxs26(Text, { dimColor: true, wrap: "wrap", children: [
32369
- " ",
32370
- "\xB7 ",
32371
- seg.count
32372
- ] }) : null
32373
- ] }, index);
32374
- }
32375
- if (seg.kind === "raw") {
32376
- return /* @__PURE__ */ jsx41(Text, { dimColor: true, wrap: "wrap", children: seg.line }, index);
32377
- }
32378
- 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 || " " })
32382
- ] }, index);
32383
- }
32384
- 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 || " " })
32388
- ] }, index);
32389
- }
32390
- return null;
32391
- }),
32392
- summary ? /* @__PURE__ */ jsx41(Text, { dimColor: true, children: summary }) : null
32393
- ] });
32931
+ const summary = diffSummary(
32932
+ additions,
32933
+ removals,
32934
+ hiddenBelow
32935
+ );
32936
+ const renderTokenizedLine = (text, backgroundColor) => tokenizeLine2(text || " ", "ts").map((t, i) => /* @__PURE__ */ jsx41(Text, { color: t.color, backgroundColor, children: t.text }, i));
32937
+ return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", width: "100%", children: toRender.map((seg, index) => {
32938
+ if (seg.kind === "omit") {
32939
+ return /* @__PURE__ */ jsx41(
32940
+ Box_default,
32941
+ {
32942
+ flexDirection: "row",
32943
+ width: "100%",
32944
+ flexGrow: 1,
32945
+ paddingLeft: 6,
32946
+ children: /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.dim, children: gutterOmit(w) })
32947
+ },
32948
+ index
32949
+ );
32950
+ }
32951
+ if (seg.kind === "raw") {
32952
+ return /* @__PURE__ */ jsx41(
32953
+ Box_default,
32954
+ {
32955
+ flexDirection: "row",
32956
+ width: "100%",
32957
+ flexWrap: "wrap",
32958
+ children: renderTokenizedLine(seg.line)
32959
+ },
32960
+ index
32961
+ );
32962
+ }
32963
+ if (seg.kind === "rem") {
32964
+ return /* @__PURE__ */ jsxs26(
32965
+ Box_default,
32966
+ {
32967
+ flexDirection: "row",
32968
+ width: "100%",
32969
+ flexGrow: 1,
32970
+ flexWrap: "wrap",
32971
+ backgroundColor: BLUMA_TERMINAL.diffRemoved,
32972
+ paddingLeft: 3,
32973
+ children: [
32974
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.dim, children: gutterLine(w, seg.oldLine) }),
32975
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: "-" }),
32976
+ /* @__PURE__ */ jsx41(
32977
+ Box_default,
32978
+ {
32979
+ flexDirection: "row",
32980
+ flexWrap: "wrap",
32981
+ backgroundColor: BLUMA_TERMINAL.diffRemoved,
32982
+ children: renderTokenizedLine(seg.body, BLUMA_TERMINAL.diffRemoved)
32983
+ }
32984
+ )
32985
+ ]
32986
+ },
32987
+ index
32988
+ );
32989
+ }
32990
+ if (seg.kind === "add") {
32991
+ return /* @__PURE__ */ jsxs26(
32992
+ Box_default,
32993
+ {
32994
+ flexDirection: "row",
32995
+ width: "100%",
32996
+ flexGrow: 1,
32997
+ flexWrap: "wrap",
32998
+ backgroundColor: BLUMA_TERMINAL.diffAdded,
32999
+ paddingLeft: 3,
33000
+ children: [
33001
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.dim, children: gutterLine(w, seg.newLine) }),
33002
+ /* @__PURE__ */ jsx41(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: "+" }),
33003
+ /* @__PURE__ */ jsx41(
33004
+ Box_default,
33005
+ {
33006
+ flexDirection: "row",
33007
+ flexWrap: "wrap",
33008
+ backgroundColor: BLUMA_TERMINAL.diffAdded,
33009
+ children: renderTokenizedLine(seg.body, BLUMA_TERMINAL.diffAdded)
33010
+ }
33011
+ )
33012
+ ]
33013
+ },
33014
+ index
33015
+ );
33016
+ }
33017
+ return null;
33018
+ }) });
32394
33019
  }
32395
33020
  var SimpleDiff = ({ text, maxHeight, frame = false }) => {
32396
33021
  const raw = stripOptionalMarkdownFence(String(text ?? ""));
@@ -32406,28 +33031,16 @@ var SimpleDiff = ({ text, maxHeight, frame = false }) => {
32406
33031
  const allLines = raw.split("\n");
32407
33032
  const body2 = /* @__PURE__ */ jsx41(LegacyDiffBody, { lines: allLines, maxHeight });
32408
33033
  if (!frame) return body2;
32409
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32410
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule }),
32411
- /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", paddingX: 1, children: body2 }),
32412
- /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule })
32413
- ] });
33034
+ return /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", children: /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", paddingX: 1, children: body2 }) });
32414
33035
  }
32415
33036
  const rows = [];
32416
33037
  for (const h of hunks) {
32417
33038
  rows.push(...hunkToRows(h));
32418
33039
  }
32419
- const { nodes, hidden, additions, removals } = renderStructuredRows(
32420
- rows,
32421
- maxHeight
32422
- );
32423
- const body = /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
32424
- (hidden > 0 || additions > 0 || removals > 0) && /* @__PURE__ */ jsx41(Text, { dimColor: true, children: diffSummary(additions, removals, hidden) }),
32425
- nodes
32426
- ] });
32427
- if (!frame) {
32428
- return body;
32429
- }
32430
- return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", children: [
33040
+ const { nodes, hidden, additions, removals } = renderStructuredRows(rows, maxHeight);
33041
+ const body = /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", width: "100%", children: nodes });
33042
+ if (!frame) return body;
33043
+ return /* @__PURE__ */ jsxs26(Box_default, { flexDirection: "column", width: "100%", children: [
32431
33044
  /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule }),
32432
33045
  /* @__PURE__ */ jsx41(Box_default, { flexDirection: "column", paddingX: 1, children: body }),
32433
33046
  /* @__PURE__ */ jsx41(Text, { dimColor: true, children: rule })
@@ -32549,7 +33162,20 @@ function EditToolDiffPanel({
32549
33162
  }
32550
33163
 
32551
33164
  // src/app/agent/tools/EditTool/UI.tsx
33165
+ import { diffLines as diffLines2 } from "diff";
32552
33166
  import { Fragment as Fragment5, jsx as jsx44, jsxs as jsxs27 } from "react/jsx-runtime";
33167
+ function countLineDiff(oldText, newText) {
33168
+ const parts = diffLines2(oldText.replace(/\r\n/g, "\n"), newText.replace(/\r\n/g, "\n"));
33169
+ let added = 0;
33170
+ let removed = 0;
33171
+ for (const part of parts) {
33172
+ const lines = part.value.replace(/\n$/, "").split("\n");
33173
+ const lineCount = part.value === "" ? 0 : lines.length;
33174
+ if (part.added) added += lineCount;
33175
+ if (part.removed) removed += lineCount;
33176
+ }
33177
+ return { added, removed };
33178
+ }
32553
33179
  function userFacingName11(args) {
32554
33180
  if (!args) return "Updated";
32555
33181
  if (args.edits && Array.isArray(args.edits) && args.edits.length > 0) {
@@ -32564,11 +33190,32 @@ function renderToolUseMessage12({ args }) {
32564
33190
  return /* @__PURE__ */ jsx44(Text, { color: BLUMA_TERMINAL.blue, children: p });
32565
33191
  }
32566
33192
  function renderToolHeader12({ args }) {
32567
- const label = userFacingName11(args);
32568
33193
  const path50 = args?.file_path ?? ".";
33194
+ const oldText = typeof args?.old_string === "string" ? args.old_string : "";
33195
+ const newText = typeof args?.new_string === "string" ? args.new_string : "";
33196
+ const counts = countLineDiff(oldText, newText);
33197
+ const action = oldText === "" ? "Created" : "Update";
32569
33198
  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 })
33199
+ /* @__PURE__ */ jsxs27(Text, { bold: true, color: BLUMA_TERMINAL.m3OnSurface, children: [
33200
+ action,
33201
+ /* @__PURE__ */ jsxs27(Text, { dimColor: true, children: [
33202
+ " ",
33203
+ /* @__PURE__ */ jsx44(FilePathLink, { filePath: path50, color: BLUMA_TERMINAL.dim })
33204
+ ] })
33205
+ ] }),
33206
+ /* @__PURE__ */ jsxs27(Text, { dimColor: true, children: [
33207
+ "(",
33208
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
33209
+ "+",
33210
+ counts.added
33211
+ ] }),
33212
+ " ",
33213
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
33214
+ "-",
33215
+ counts.removed
33216
+ ] }),
33217
+ ")"
33218
+ ] })
32572
33219
  ] });
32573
33220
  }
32574
33221
  function renderSingleEditDiff(edit, index, total, state2) {
@@ -32584,7 +33231,20 @@ function renderSingleEditDiff(edit, index, total, state2) {
32584
33231
  total
32585
33232
  ] }),
32586
33233
  /* @__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 })
33234
+ /* @__PURE__ */ jsx44(FilePathLink, { filePath, color: BLUMA_TERMINAL.dim }),
33235
+ /* @__PURE__ */ jsxs27(Text, { dimColor: true, children: [
33236
+ "(",
33237
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
33238
+ "+",
33239
+ countLineDiff(oldString, newString).added
33240
+ ] }),
33241
+ " ",
33242
+ /* @__PURE__ */ jsxs27(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
33243
+ "-",
33244
+ countLineDiff(oldString, newString).removed
33245
+ ] }),
33246
+ ")"
33247
+ ] })
32588
33248
  ] }),
32589
33249
  /* @__PURE__ */ jsx44(Box_default, { marginLeft: 2, marginTop: 0, children: /* @__PURE__ */ jsx44(
32590
33250
  EditToolDiffPanel,
@@ -32653,7 +33313,7 @@ function renderToolResultMessage12(result) {
32653
33313
  {
32654
33314
  filePath: edit.file_path,
32655
33315
  isNewFile: edit.is_new_file ?? false,
32656
- diffText: edit.diff
33316
+ diffText: typeof edit.diff === "string" ? edit.diff : null
32657
33317
  }
32658
33318
  ) }, `result-edit-${idx}`)) });
32659
33319
  }
@@ -32691,7 +33351,7 @@ function prettifyToolToken(value) {
32691
33351
  if (!s) return "";
32692
33352
  return s.replace(/[_-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
32693
33353
  }
32694
- function parseArgsRecord(args) {
33354
+ function parseArgsRecord2(args) {
32695
33355
  if (args == null) return {};
32696
33356
  if (typeof args === "string") {
32697
33357
  try {
@@ -32748,7 +33408,7 @@ var TOOL_INVOCATION_TITLES = {
32748
33408
  function getToolInvocationTitle(internalName, args) {
32749
33409
  const key = internalName.trim();
32750
33410
  if (key === "edit_tool") {
32751
- const p = parseArgsRecord(args);
33411
+ const p = parseArgsRecord2(args);
32752
33412
  const edits = p.edits;
32753
33413
  if (Array.isArray(edits) && edits.length > 0) {
32754
33414
  const allCreate = edits.every(
@@ -33458,6 +34118,8 @@ var loadSkillTool = createTool({
33458
34118
  });
33459
34119
 
33460
34120
  // src/app/agent/tools/FileWriteTool/UI.tsx
34121
+ import fs42 from "fs";
34122
+ import { diffLines as diffLines3 } from "diff";
33461
34123
  import { jsx as jsx54, jsxs as jsxs37 } from "react/jsx-runtime";
33462
34124
  function getFilePath(args) {
33463
34125
  if (typeof args?.filepath === "string" && args.filepath.trim()) {
@@ -33468,6 +34130,49 @@ function getFilePath(args) {
33468
34130
  }
33469
34131
  return void 0;
33470
34132
  }
34133
+ function readExistingFileText(filePath) {
34134
+ if (!filePath) return "";
34135
+ try {
34136
+ return fs42.readFileSync(filePath, "utf-8");
34137
+ } catch {
34138
+ return "";
34139
+ }
34140
+ }
34141
+ function countLineDiff2(oldText, newText) {
34142
+ const parts = diffLines3(oldText.replace(/\r\n/g, "\n"), newText.replace(/\r\n/g, "\n"));
34143
+ let added = 0;
34144
+ let removed = 0;
34145
+ for (const part of parts) {
34146
+ const lines = part.value.replace(/\n$/, "").split("\n");
34147
+ const lineCount = part.value === "" ? 0 : lines.length;
34148
+ if (part.added) added += lineCount;
34149
+ if (part.removed) removed += lineCount;
34150
+ }
34151
+ return { added, removed };
34152
+ }
34153
+ function renderChangeCount(added, removed) {
34154
+ return /* @__PURE__ */ jsxs37(Text, { dimColor: true, children: [
34155
+ "(",
34156
+ /* @__PURE__ */ jsxs37(Text, { color: BLUMA_TERMINAL.diffAddedWord, children: [
34157
+ "+",
34158
+ added
34159
+ ] }),
34160
+ " ",
34161
+ /* @__PURE__ */ jsxs37(Text, { color: BLUMA_TERMINAL.diffRemovedWord, children: [
34162
+ "-",
34163
+ removed
34164
+ ] }),
34165
+ ")"
34166
+ ] });
34167
+ }
34168
+ function buildUnifiedDiffText(filePath, oldContent, newContent) {
34169
+ const patch = getPatchFromContents({
34170
+ filePath,
34171
+ oldContent,
34172
+ newContent
34173
+ });
34174
+ return patchToUnifiedDiffText(filePath, patch);
34175
+ }
33471
34176
  function userFacingName20(args) {
33472
34177
  if (!args) return "Wrote";
33473
34178
  const p = getFilePath(args)?.split("/").pop() ?? "...";
@@ -33480,9 +34185,12 @@ function renderToolUseMessage21({ args }) {
33480
34185
  function renderToolHeader21({ args }) {
33481
34186
  const filePath = getFilePath(args) ?? "";
33482
34187
  const isNewFile = !args?.hasExistingContent;
34188
+ const content = typeof args?.content === "string" ? args.content : "";
34189
+ const counts = content ? countLineDiff2(isNewFile ? "" : readExistingFileText(filePath), content) : { added: 0, removed: 0 };
33483
34190
  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 })
34191
+ /* @__PURE__ */ jsx54(Text, { bold: true, color: isNewFile ? BLUMA_TERMINAL.m3OnSurface : BLUMA_TERMINAL.err, children: isNewFile ? "Created" : "Updated" }),
34192
+ /* @__PURE__ */ jsx54(FilePathLink, { filePath, color: isNewFile ? BLUMA_TERMINAL.dim : BLUMA_TERMINAL.err }),
34193
+ content ? renderChangeCount(counts.added, counts.removed) : null
33486
34194
  ] });
33487
34195
  }
33488
34196
  function renderToolMessage20({
@@ -33494,12 +34202,15 @@ function renderToolMessage20({
33494
34202
  const filePath = getFilePath(p);
33495
34203
  const payload = resolveToolPayload(result);
33496
34204
  const content = typeof p?.content === "string" ? p.content : "";
34205
+ const isNewFile = !p?.hasExistingContent;
34206
+ const previousContent = isNewFile ? "" : readExistingFileText(filePath);
34207
+ const diffText = content && filePath ? buildUnifiedDiffText(filePath, previousContent, content) : "";
33497
34208
  return /* @__PURE__ */ jsx54(ChatBlock, { marginBottom: 1, children: /* @__PURE__ */ jsx54(MessageResponse, { children: /* @__PURE__ */ jsxs37(Box_default, { flexDirection: "column", marginTop: 1, children: [
33498
34209
  /* @__PURE__ */ jsxs37(Box_default, { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-end", children: [
33499
34210
  /* @__PURE__ */ jsx54(ToolUseLoader, { state: state2 ?? "pending" }),
33500
34211
  renderToolHeader21({ args: p })
33501
34212
  ] }),
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: [
34213
+ /* @__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", children: /* @__PURE__ */ jsx54(SimpleDiff, { text: diffText, maxHeight: 2e3 }) }) : /* @__PURE__ */ jsxs37(Text, { dimColor: true, children: [
33503
34214
  "Writing to ",
33504
34215
  filePath ?? "...",
33505
34216
  " (",
@@ -33520,15 +34231,31 @@ function renderToolResultMessage21(result, context) {
33520
34231
  }
33521
34232
  const filePath = payload.filepath ?? payload.file_path ?? "";
33522
34233
  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,
34234
+ const resultFilePath = filePath || context?.filePath || "file.txt";
34235
+ const previousContent = typeof payload.previous_content === "string" ? payload.previous_content : void 0;
34236
+ const currentContent = typeof payload.content === "string" ? payload.content : typeof context?.args?.content === "string" ? (context?.args).content : typeof context?.content === "string" ? context.content : void 0;
34237
+ const canRenderDiff = typeof currentContent === "string" && currentContent.trim() && typeof previousContent === "string";
34238
+ return /* @__PURE__ */ jsx54(Box_default, { flexDirection: "column", children: canRenderDiff ? /* @__PURE__ */ jsx54(Box_default, { width: "100%", flexGrow: 1, children: /* @__PURE__ */ jsx54(
34239
+ SimpleDiff,
34240
+ {
34241
+ text: buildUnifiedDiffText(resultFilePath, previousContent ?? "", currentContent ?? ""),
34242
+ maxHeight: 2e3
34243
+ }
34244
+ ) }) : typeof currentContent === "string" && currentContent.trim() ? /* @__PURE__ */ jsx54(
34245
+ Box_default,
33526
34246
  {
33527
- code: context.content,
33528
- filePath: filePath || context.filePath || "file.txt",
33529
- maxLines: 200
34247
+ width: "100%",
34248
+ backgroundColor: BLUMA_TERMINAL.diffAdded,
34249
+ children: /* @__PURE__ */ jsx54(
34250
+ HighlightedCode,
34251
+ {
34252
+ code: currentContent,
34253
+ filePath: resultFilePath,
34254
+ maxLines: 2e3
34255
+ }
34256
+ )
33530
34257
  }
33531
- ) }) : null });
34258
+ ) : null });
33532
34259
  }
33533
34260
 
33534
34261
  // src/app/agent/tools/FileWriteTool/index.ts
@@ -35961,162 +36688,6 @@ var ConfirmationPrompt = memo13(ConfirmationPromptComponent);
35961
36688
  // src/app/ui/WorkingTimer.tsx
35962
36689
  import { useState as useState15, useEffect as useEffect13, memo as memo14, useRef as useRef6 } from "react";
35963
36690
  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
36691
  import { Fragment as Fragment8, jsx as jsx77, jsxs as jsxs60 } from "react/jsx-runtime";
36121
36692
  var SHIMMER_CYCLE_MS = 2e3;
36122
36693
  var SHIMMER_TICK_MS = 80;
@@ -36163,9 +36734,6 @@ var WorkingTimerComponent = ({
36163
36734
  finalElapsedMs: externalFinalElapsedMs
36164
36735
  // External final elapsed time (from parent)
36165
36736
  }) => {
36166
- if (startedAtMs == null && externalFinalElapsedMs == null) {
36167
- return null;
36168
- }
36169
36737
  const [currentAction, setCurrentAction] = useState15("working");
36170
36738
  const [nowTick, setNowTick] = useState15(() => Date.now());
36171
36739
  const [internalFinalElapsedMs, setInternalFinalElapsedMs] = useState15(null);
@@ -36206,11 +36774,10 @@ var WorkingTimerComponent = ({
36206
36774
  }, [startedAtMs]);
36207
36775
  useEffect13(() => {
36208
36776
  const localBus = localEventBusRef.current;
36209
- if (startedAtMs == null) return;
36210
36777
  let isMounted = true;
36211
36778
  completionHandledRef.current = false;
36212
36779
  const handleTurnComplete = (data) => {
36213
- if (!isMounted || completionHandledRef.current) return;
36780
+ if (!isMounted || completionHandledRef.current || startedAtMs == null) return;
36214
36781
  const elapsedMs2 = typeof data?.elapsedMs === "number" ? data.elapsedMs : null;
36215
36782
  if (elapsedMs2 == null) return;
36216
36783
  completionHandledRef.current = true;
@@ -36235,7 +36802,10 @@ var WorkingTimerComponent = ({
36235
36802
  localBus.removeAllListeners();
36236
36803
  };
36237
36804
  }, [eventBus, startedAtMs]);
36238
- const displayAction = dynamicActionLabel || (taskStatus || (isReasoning ? "thinking" : currentAction)).trim() || "working";
36805
+ if (startedAtMs == null && externalFinalElapsedMs == null) {
36806
+ return null;
36807
+ }
36808
+ const displayAction = dynamicActionLabel || (taskStatus || (currentAction !== "working" ? currentAction : isReasoning ? getReasoningActionStatus().action : currentAction)).trim() || "working";
36239
36809
  const actionLine = `${displayAction}`;
36240
36810
  const elapsedMs = finalElapsedMs != null ? finalElapsedMs : startedAtMs != null ? Math.max(0, nowTick - startedAtMs) : 0;
36241
36811
  const elapsedLabel = formatTurnDurationMs(elapsedMs);
@@ -36379,13 +36949,13 @@ var StatusBar = memo16(() => {
36379
36949
  } = snapshot;
36380
36950
  void renderKey;
36381
36951
  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
36952
+ primary: BLUMA_TERMINAL.subtle,
36953
+ secondary: BLUMA_TERMINAL.subtle,
36954
+ accent: BLUMA_TERMINAL.subtle,
36955
+ success: BLUMA_TERMINAL.subtle,
36956
+ warning: BLUMA_TERMINAL.subtle,
36957
+ error: BLUMA_TERMINAL.subtle,
36958
+ muted: BLUMA_TERMINAL.subtle
36389
36959
  };
36390
36960
  const remainingPercent = tokenBudget > 0 ? Math.max(0, Math.round((tokenBudget - tokenCount) / tokenBudget * 100)) : 100;
36391
36961
  let remainingColor = colors.muted;
@@ -36399,27 +36969,27 @@ var StatusBar = memo16(() => {
36399
36969
  const shortWorkdir = workdir ? workdir.split("/").filter(Boolean).pop() || workdir : "";
36400
36970
  const compressionText = isCompressing ? ` \xB7 compressing ${compressionProgress}%` : "";
36401
36971
  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" })
36972
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36973
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Plan Mode" })
36404
36974
  ] }) : null;
36405
36975
  const agentModeIndicator = (() => {
36406
36976
  if (!agentMode || agentMode === "default") return null;
36407
36977
  const mode = agentMode.toLowerCase();
36408
36978
  if (mode === "coordinator") {
36409
36979
  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" })
36980
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36981
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Coordinator" })
36412
36982
  ] });
36413
36983
  }
36414
36984
  if (mode === "plan") {
36415
36985
  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" })
36986
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36987
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Plan" })
36418
36988
  ] });
36419
36989
  }
36420
36990
  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 })
36991
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" }),
36992
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: agentMode })
36423
36993
  ] });
36424
36994
  })();
36425
36995
  const modelText = modelName ? ` \xB7 model: ${modelName}` : "";
@@ -36435,25 +37005,25 @@ var StatusBar = memo16(() => {
36435
37005
  flexShrink: 0,
36436
37006
  children: [
36437
37007
  shortWorkdir && /* @__PURE__ */ jsxs62(Fragment9, { children: [
36438
- /* @__PURE__ */ jsxs62(Text, { color: BLUMA_TERMINAL.dim, children: [
37008
+ /* @__PURE__ */ jsxs62(Text, { color: BLUMA_TERMINAL.subtle, children: [
36439
37009
  "~/",
36440
37010
  shortWorkdir
36441
37011
  ] }),
36442
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "\xB7" })
37012
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "\xB7" })
36443
37013
  ] }),
36444
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: "Context" }),
37014
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: "Context" }),
36445
37015
  /* @__PURE__ */ jsxs62(Text, { color: remainingColor, children: [
36446
37016
  remainingPercent,
36447
37017
  "% left"
36448
37018
  ] }),
36449
37019
  modelText && /* @__PURE__ */ jsxs62(Fragment9, { children: [
36450
- /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: modelText }),
36451
- /* @__PURE__ */ jsx79(Text, { dimColor: true, children: "\xB7" })
37020
+ /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: modelText }),
37021
+ /* @__PURE__ */ jsx79(Text, { subtleColor: true, children: "\xB7" })
36452
37022
  ] }),
36453
- compressionText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: compressionText }),
37023
+ compressionText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: compressionText }),
36454
37024
  agentModeIndicator,
36455
37025
  planModeText,
36456
- projectsText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.dim, children: projectsText })
37026
+ projectsText && /* @__PURE__ */ jsx79(Text, { color: BLUMA_TERMINAL.subtle, children: projectsText })
36457
37027
  ]
36458
37028
  }
36459
37029
  );
@@ -36841,7 +37411,7 @@ function ToolMessageComponent({
36841
37411
  if (!normalizedResult) return null;
36842
37412
  if (entry?.renderToolResultMessage) {
36843
37413
  const payload = normalizedResult.status === "success" ? normalizedResult.data : normalizedResult;
36844
- return entry.renderToolResultMessage(payload, { verbose: false });
37414
+ return entry.renderToolResultMessage(payload, { verbose: false, args });
36845
37415
  }
36846
37416
  return renderRawJson(normalizedResult);
36847
37417
  })();
@@ -38279,7 +38849,7 @@ var renderCode = () => {
38279
38849
  // src/app/agent/core/thread/thread_store.ts
38280
38850
  import path46 from "path";
38281
38851
  import os34 from "os";
38282
- import { promises as fs42 } from "fs";
38852
+ import { promises as fs43 } from "fs";
38283
38853
  import { randomUUID as randomUUID2 } from "crypto";
38284
38854
  var INDEX_VERSION = 1;
38285
38855
  var fileLocks2 = /* @__PURE__ */ new Map();
@@ -38314,10 +38884,10 @@ var ThreadStore = class {
38314
38884
  * Inicializa o diretório de threads
38315
38885
  */
38316
38886
  async initialize() {
38317
- await fs42.mkdir(this.threadsDir, { recursive: true });
38318
- await fs42.mkdir(this.archiveDir, { recursive: true });
38887
+ await fs43.mkdir(this.threadsDir, { recursive: true });
38888
+ await fs43.mkdir(this.archiveDir, { recursive: true });
38319
38889
  try {
38320
- await fs42.access(this.indexPath);
38890
+ await fs43.access(this.indexPath);
38321
38891
  } catch {
38322
38892
  await this.saveIndex({ version: INDEX_VERSION, threads: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() });
38323
38893
  }
@@ -38346,7 +38916,7 @@ var ThreadStore = class {
38346
38916
  async loadIndex() {
38347
38917
  return withFileLock2(this.indexPath, async () => {
38348
38918
  try {
38349
- const content = await fs42.readFile(this.indexPath, "utf-8");
38919
+ const content = await fs43.readFile(this.indexPath, "utf-8");
38350
38920
  return JSON.parse(content);
38351
38921
  } catch {
38352
38922
  return { version: INDEX_VERSION, threads: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
@@ -38357,8 +38927,8 @@ var ThreadStore = class {
38357
38927
  return withFileLock2(this.indexPath, async () => {
38358
38928
  index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
38359
38929
  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);
38930
+ await fs43.writeFile(tempPath, JSON.stringify(index, null, 2), "utf-8");
38931
+ await fs43.rename(tempPath, this.indexPath);
38362
38932
  });
38363
38933
  }
38364
38934
  // ==================== Git Info ====================
@@ -38554,7 +39124,7 @@ var ThreadStore = class {
38554
39124
  const oldPath = this.getHistoryPath(threadId);
38555
39125
  const newPath = path46.join(this.archiveDir, `${threadId}.jsonl`);
38556
39126
  try {
38557
- await fs42.rename(oldPath, newPath);
39127
+ await fs43.rename(oldPath, newPath);
38558
39128
  } catch (e) {
38559
39129
  if (e.code !== "ENOENT") throw e;
38560
39130
  }
@@ -38578,7 +39148,7 @@ var ThreadStore = class {
38578
39148
  const oldPath = path46.join(this.archiveDir, `${threadId}.jsonl`);
38579
39149
  const newPath = this.getHistoryPath(threadId);
38580
39150
  try {
38581
- await fs42.rename(oldPath, newPath);
39151
+ await fs43.rename(oldPath, newPath);
38582
39152
  } catch (e) {
38583
39153
  if (e.code !== "ENOENT") throw e;
38584
39154
  }
@@ -38599,7 +39169,7 @@ var ThreadStore = class {
38599
39169
  if (entryIndex === -1) return false;
38600
39170
  const entry = index.threads[entryIndex];
38601
39171
  try {
38602
- await fs42.unlink(entry.historyPath);
39172
+ await fs43.unlink(entry.historyPath);
38603
39173
  } catch {
38604
39174
  }
38605
39175
  index.threads.splice(entryIndex, 1);
@@ -38626,7 +39196,7 @@ var ThreadStore = class {
38626
39196
  for (const msg of history.messages) {
38627
39197
  lines.push(JSON.stringify({ type: "message", ...msg }));
38628
39198
  }
38629
- await fs42.writeFile(historyPath, lines.join("\n") + "\n", "utf-8");
39199
+ await fs43.writeFile(historyPath, lines.join("\n") + "\n", "utf-8");
38630
39200
  }
38631
39201
  /**
38632
39202
  * Carrega o histórico de uma thread
@@ -38636,7 +39206,7 @@ var ThreadStore = class {
38636
39206
  const entry = index.threads.find((t) => t.threadId === threadId);
38637
39207
  if (!entry) return null;
38638
39208
  try {
38639
- const content = await fs42.readFile(entry.historyPath, "utf-8");
39209
+ const content = await fs43.readFile(entry.historyPath, "utf-8");
38640
39210
  const lines = content.split("\n").filter(Boolean);
38641
39211
  const history = {
38642
39212
  threadId,
@@ -38669,7 +39239,7 @@ var ThreadStore = class {
38669
39239
  const entry = index.threads.find((t) => t.threadId === threadId);
38670
39240
  if (!entry) throw new Error(`Thread not found: ${threadId}`);
38671
39241
  const lines = messages.map((msg) => JSON.stringify({ type: "message", ...msg }));
38672
- await fs42.appendFile(entry.historyPath, lines.join("\n") + "\n", "utf-8");
39242
+ await fs43.appendFile(entry.historyPath, lines.join("\n") + "\n", "utf-8");
38673
39243
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
38674
39244
  await this.saveIndex(index);
38675
39245
  }
@@ -38957,7 +39527,7 @@ function formatRelativeTime(iso) {
38957
39527
  if (diffDays < 7) return `h\xE1 ${diffDays}d`;
38958
39528
  return formatDate(iso);
38959
39529
  }
38960
- function truncate3(str, max) {
39530
+ function truncate4(str, max) {
38961
39531
  if (str.length <= max) return str;
38962
39532
  return str.slice(0, max - 3) + "...";
38963
39533
  }
@@ -38983,7 +39553,7 @@ async function renderCurrentThread() {
38983
39553
  ] }),
38984
39554
  /* @__PURE__ */ jsxs79(Text, { children: [
38985
39555
  /* @__PURE__ */ jsx96(Text, { dimColor: true, children: "Preview: " }),
38986
- /* @__PURE__ */ jsx96(Text, { dimColor: true, children: truncate3(metadata.preview, 60) })
39556
+ /* @__PURE__ */ jsx96(Text, { dimColor: true, children: truncate4(metadata.preview, 60) })
38987
39557
  ] }),
38988
39558
  /* @__PURE__ */ jsxs79(Text, { children: [
38989
39559
  /* @__PURE__ */ jsx96(Text, { dimColor: true, children: "Modelo: " }),
@@ -38998,7 +39568,7 @@ async function renderCurrentThread() {
38998
39568
  /* @__PURE__ */ jsxs79(Text, { dimColor: true, children: [
38999
39569
  metadata.gitInfo.branch || "?",
39000
39570
  " @ ",
39001
- truncate3(metadata.gitInfo.sha || "?", 8)
39571
+ truncate4(metadata.gitInfo.sha || "?", 8)
39002
39572
  ] })
39003
39573
  ] }),
39004
39574
  /* @__PURE__ */ jsxs79(Text, { children: [
@@ -39066,7 +39636,7 @@ async function renderThreadList(args) {
39066
39636
  }
39067
39637
  var ThreadListItem = ({ thread, isActive, index }) => {
39068
39638
  const statusColor = thread.status === "archived" ? BLUMA_TERMINAL.warning : BLUMA_TERMINAL.success;
39069
- const name = thread.name || truncate3(thread.preview, 40);
39639
+ const name = thread.name || truncate4(thread.preview, 40);
39070
39640
  return /* @__PURE__ */ jsxs79(Box_default, { flexDirection: "column", children: [
39071
39641
  /* @__PURE__ */ jsxs79(Box_default, { gap: 1, children: [
39072
39642
  /* @__PURE__ */ jsxs79(Text, { dimColor: true, children: [
@@ -41535,15 +42105,15 @@ import semverGt from "semver/functions/gt.js";
41535
42105
  import semverValid from "semver/functions/valid.js";
41536
42106
  import { fileURLToPath as fileURLToPath6 } from "url";
41537
42107
  import path47 from "path";
41538
- import fs43 from "fs";
42108
+ import fs44 from "fs";
41539
42109
  var BLUMA_PACKAGE_NAME = "@nomad-e/bluma-cli";
41540
42110
  function findBlumaPackageJson(startDir) {
41541
42111
  let dir = startDir;
41542
42112
  for (let i = 0; i < 12; i++) {
41543
42113
  const candidate = path47.join(dir, "package.json");
41544
- if (fs43.existsSync(candidate)) {
42114
+ if (fs44.existsSync(candidate)) {
41545
42115
  try {
41546
- const raw = fs43.readFileSync(candidate, "utf8");
42116
+ const raw = fs44.readFileSync(candidate, "utf8");
41547
42117
  const parsed = JSON.parse(raw);
41548
42118
  if (parsed?.name === BLUMA_PACKAGE_NAME && parsed?.version) {
41549
42119
  return { name: parsed.name, version: String(parsed.version) };
@@ -41574,8 +42144,8 @@ function resolveInstalledBlumaPackage() {
41574
42144
  if (argv1 && !argv1.startsWith("-")) {
41575
42145
  try {
41576
42146
  let resolved = argv1;
41577
- if (path47.isAbsolute(argv1) && fs43.existsSync(argv1)) {
41578
- resolved = fs43.realpathSync(argv1);
42147
+ if (path47.isAbsolute(argv1) && fs44.existsSync(argv1)) {
42148
+ resolved = fs44.realpathSync(argv1);
41579
42149
  } else {
41580
42150
  resolved = path47.resolve(process.cwd(), argv1);
41581
42151
  }
@@ -42398,16 +42968,16 @@ function usePlanMode() {
42398
42968
 
42399
42969
  // src/app/hooks/useAgentMode.ts
42400
42970
  import { useState as useState22, useEffect as useEffect20, useCallback as useCallback9 } from "react";
42401
- import * as fs44 from "fs";
42971
+ import * as fs45 from "fs";
42402
42972
  import * as path48 from "path";
42403
42973
  import { homedir as homedir3 } from "os";
42404
42974
  var SETTINGS_PATH = path48.join(homedir3(), ".bluma", "settings.json");
42405
42975
  function readAgentModeFromFile() {
42406
42976
  try {
42407
- if (!fs44.existsSync(SETTINGS_PATH)) {
42977
+ if (!fs45.existsSync(SETTINGS_PATH)) {
42408
42978
  return "default";
42409
42979
  }
42410
- const content = fs44.readFileSync(SETTINGS_PATH, "utf-8");
42980
+ const content = fs45.readFileSync(SETTINGS_PATH, "utf-8");
42411
42981
  const settings = JSON.parse(content);
42412
42982
  return settings.agentMode || "default";
42413
42983
  } catch (error) {
@@ -42426,16 +42996,16 @@ function useAgentMode() {
42426
42996
  }, []);
42427
42997
  const updateAgentMode = useCallback9((mode) => {
42428
42998
  try {
42429
- if (!fs44.existsSync(SETTINGS_PATH)) {
42430
- fs44.mkdirSync(path48.dirname(SETTINGS_PATH), { recursive: true });
42999
+ if (!fs45.existsSync(SETTINGS_PATH)) {
43000
+ fs45.mkdirSync(path48.dirname(SETTINGS_PATH), { recursive: true });
42431
43001
  }
42432
43002
  let settings = {};
42433
- if (fs44.existsSync(SETTINGS_PATH)) {
42434
- const content = fs44.readFileSync(SETTINGS_PATH, "utf-8");
43003
+ if (fs45.existsSync(SETTINGS_PATH)) {
43004
+ const content = fs45.readFileSync(SETTINGS_PATH, "utf-8");
42435
43005
  settings = JSON.parse(content);
42436
43006
  }
42437
43007
  settings.agentMode = mode;
42438
- fs44.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
43008
+ fs45.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
42439
43009
  setAgentMode(mode);
42440
43010
  } catch (error) {
42441
43011
  console.error("Failed to update agent mode:", error);
@@ -43691,9 +44261,9 @@ async function runAgentMode() {
43691
44261
  try {
43692
44262
  if (inputFileIndex !== -1 && args[inputFileIndex + 1]) {
43693
44263
  const filePath = args[inputFileIndex + 1];
43694
- rawPayload = fs45.readFileSync(filePath, "utf-8");
44264
+ rawPayload = fs46.readFileSync(filePath, "utf-8");
43695
44265
  } else {
43696
- rawPayload = fs45.readFileSync(0, "utf-8");
44266
+ rawPayload = fs46.readFileSync(0, "utf-8");
43697
44267
  }
43698
44268
  } catch (err) {
43699
44269
  writeAgentEvent(registrySessionId, {
@@ -43893,7 +44463,7 @@ function readCliPackageVersion() {
43893
44463
  try {
43894
44464
  const base = path49.dirname(fileURLToPath7(import.meta.url));
43895
44465
  const pkgPath = path49.join(base, "..", "package.json");
43896
- const j = JSON.parse(fs45.readFileSync(pkgPath, "utf8"));
44466
+ const j = JSON.parse(fs46.readFileSync(pkgPath, "utf8"));
43897
44467
  return String(j.version || "0.0.0");
43898
44468
  } catch {
43899
44469
  return "0.0.0";
@@ -44019,7 +44589,7 @@ function startBackgroundAgent() {
44019
44589
  process.exit(1);
44020
44590
  }
44021
44591
  const filePath = args[inputFileIndex + 1];
44022
- const rawPayload = fs45.readFileSync(filePath, "utf-8");
44592
+ const rawPayload = fs46.readFileSync(filePath, "utf-8");
44023
44593
  const envelope = JSON.parse(rawPayload);
44024
44594
  const sessionId = envelope.session_id || envelope.message_id || uuidv412();
44025
44595
  registerSession({