@aiderdesk/aiderdesk 0.64.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/out/cli.js +1 -0
  2. package/out/{open-telemetry-CcefKvbB.js → open-telemetry-baOvr6sK.js} +1 -1
  3. package/out/renderer/assets/{arc-DoIK-bD2.js → arc-BXuUWHai.js} +1 -1
  4. package/out/renderer/assets/{architectureDiagram-Q4EWVU46-B8_dgBXp.js → architectureDiagram-3BPJPVTR-Bm9oFTP_.js} +11 -9
  5. package/out/renderer/assets/{blockDiagram-DXYQGD6D-BDOvGPDN.js → blockDiagram-GPEHLZMM-DQ2DevZl.js} +218 -30
  6. package/out/renderer/assets/{c4Diagram-AHTNJAMY-1ABZnJ2v.js → c4Diagram-AAUBKEIU-BdR46VbA.js} +2 -2
  7. package/out/renderer/assets/{channel-Cr_H2zdE.js → channel-DUdp1NJ7.js} +1 -1
  8. package/out/renderer/assets/{chunk-EDXVE4YY-Dt80V_EG.js → chunk-2J33WTMH-Dzi-idRV.js} +1 -1
  9. package/out/renderer/assets/{chunk-4BX2VUAB-d88VZY9C.js → chunk-4BX2VUAB-DEyFpixF.js} +1 -1
  10. package/out/renderer/assets/{chunk-55IACEB6-BO1oJBQV.js → chunk-55IACEB6-DyyQMIZa.js} +1 -1
  11. package/out/renderer/assets/{chunk-4TB4RGXK-DLcMuHVI.js → chunk-727SXJPM-vXEiesCP.js} +245 -149
  12. package/out/renderer/assets/{chunk-OYMX7WX6-DBFhtMcs.js → chunk-AQP2D5EJ-B0SWmbrS.js} +92 -78
  13. package/out/renderer/assets/{chunk-FMBD7UC4-D5MNbIWZ.js → chunk-FMBD7UC4-CoPwvBCa.js} +1 -1
  14. package/out/renderer/assets/{chunk-YZCP3GAM-gAcMGuhT.js → chunk-ND2GUHAM-ku5t5SwP.js} +1 -1
  15. package/out/renderer/assets/{chunk-QZHKN3VN-Bxwt_pyh.js → chunk-QZHKN3VN-DBGBAqit.js} +1 -1
  16. package/out/renderer/assets/{classDiagram-6PBFFD2Q-B7lgamMP.js → classDiagram-4FO5ZUOK-DTyjsHX9.js} +6 -6
  17. package/out/renderer/assets/{classDiagram-v2-HSJHXN6E-B7lgamMP.js → classDiagram-v2-Q7XG4LA2-DTyjsHX9.js} +6 -6
  18. package/out/renderer/assets/{cose-bilkent-S5V4N54A-BZNBIG2x.js → cose-bilkent-S5V4N54A-wGfE9wIx.js} +1 -1
  19. package/out/renderer/assets/{dagre-KV5264BT-C3hXUNb-.js → dagre-BM42HDAG-cwyj1-l0.js} +17 -6
  20. package/out/renderer/assets/{diagram-MMDJMWI5-BcI1Ek4N.js → diagram-2AECGRRQ-BYvCxkOs.js} +3 -5
  21. package/out/renderer/assets/{diagram-5BDNPKRD-DNh45EqP.js → diagram-5GNKFQAL--ZlqvgmY.js} +4 -6
  22. package/out/renderer/assets/diagram-KO2AKTUF-CoPNrPhx.js +632 -0
  23. package/out/renderer/assets/{diagram-TYMM5635-DuHcW-s7.js → diagram-LMA3HP47-wjhxYTWf.js} +3 -5
  24. package/out/renderer/assets/{diagram-G4DWMVQ6-8lhqJfPk.js → diagram-OG6HWLK6-DhYpewbd.js} +4 -6
  25. package/out/renderer/assets/{erDiagram-SMLLAGMA-I6Q9HYdF.js → erDiagram-TEJ5UH35-DolRLBng.js} +4 -4
  26. package/out/renderer/assets/{flowDiagram-DWJPFMVM-BzRjtX5C.js → flowDiagram-I6XJVG4X-DjAbl_XC.js} +6 -6
  27. package/out/renderer/assets/{ganttDiagram-T4ZO3ILL-DVkem_IA.js → ganttDiagram-6RSMTGT7-AF7-XgtX.js} +7 -1
  28. package/out/renderer/assets/{gitGraphDiagram-UUTBAWPF-BYpvdMpK.js → gitGraphDiagram-PVQCEYII-BMZLakzH.js} +4 -6
  29. package/out/renderer/assets/{graph-CAtr5PoG.js → graph-B_ifajWk.js} +490 -135
  30. package/out/renderer/assets/{index-CNL53LoL.js → index-3bI-dJm8.js} +9752 -6469
  31. package/out/renderer/assets/{index-Duw36zwk.css → index-B62bIfbt.css} +107 -11
  32. package/out/renderer/assets/{infoDiagram-42DDH7IO-BcmBthOY.js → infoDiagram-5YYISTIA-0f7Qxxvp.js} +3 -5
  33. package/out/renderer/assets/{ishikawaDiagram-UXIWVN3A-moTWny-V.js → ishikawaDiagram-YF4QCWOH-BX_EIAMn.js} +1 -1
  34. package/out/renderer/assets/{journeyDiagram-VCZTEJTY-DOW8zaZt.js → journeyDiagram-JHISSGLW-Dmitv8wD.js} +4 -4
  35. package/out/renderer/assets/{kanban-definition-6JOO6SKY-DpJjTob4.js → kanban-definition-UN3LZRKU-By2GFUNB.js} +2 -2
  36. package/out/renderer/assets/{layout-BvH51Ui9.js → layout-DAkKffy1.js} +459 -32
  37. package/out/renderer/assets/{mindmap-definition-QFDTVHPH-DggFFNHq.js → mindmap-definition-RKZ34NQL-yIrV1m0y.js} +3 -3
  38. package/out/renderer/assets/{pieDiagram-DEJITSTG-BED4dnMF.js → pieDiagram-4H26LBE5-PV9y5rw_.js} +4 -6
  39. package/out/renderer/assets/{quadrantDiagram-34T5L4WZ-RpQ3qNU5.js → quadrantDiagram-W4KKPZXB-DeX0zTCp.js} +22 -20
  40. package/out/renderer/assets/{requirementDiagram-MS252O5E-VQt4zBMB.js → requirementDiagram-4Y6WPE33-Bzfk_KE-.js} +3 -3
  41. package/out/renderer/assets/{sankeyDiagram-XADWPNL6-DywR7qAk.js → sankeyDiagram-5OEKKPKP-BuCv8QIY.js} +80 -11
  42. package/out/renderer/assets/{sequenceDiagram-FGHM5R23-CVPfZD4e.js → sequenceDiagram-3UESZ5HK-Zg7Ukud8.js} +21 -9
  43. package/out/renderer/assets/{stateDiagram-FHFEXIEX-BrH8Q8ZG.js → stateDiagram-AJRCARHV-CLaqfYR8.js} +6 -8
  44. package/out/renderer/assets/{stateDiagram-v2-QKLJ7IA2-BTWk2K0H.js → stateDiagram-v2-BHNVJYJU-Cmm1ljQ4.js} +4 -4
  45. package/out/renderer/assets/{timeline-definition-GMOUNBTQ-DwDUCrTb.js → timeline-definition-PNZ67QCA-DQBaAVcC.js} +2 -2
  46. package/out/renderer/assets/{vennDiagram-DHZGUBPP-Bjvr7yGM.js → vennDiagram-CIIHVFJN-CuplbU_R.js} +1 -1
  47. package/out/renderer/assets/{wardley-RL74JXVD-Bo-sW7uQ.js → wardley-L42UT6IY-BiqfHMim.js} +25605 -19118
  48. package/out/renderer/assets/{wardleyDiagram-NUSXRM2D-DRW_1PCJ.js → wardleyDiagram-YWT4CUSO-BaV0FnUu.js} +112 -38
  49. package/out/renderer/assets/worker-CfJUABHG.js +12626 -0
  50. package/out/renderer/assets/{xychartDiagram-5P7HB3ND-Ds-qS4nC.js → xychartDiagram-2RQKCTM6-DA_Miw-n.js} +1 -1
  51. package/out/renderer/index.html +2 -2
  52. package/out/renderer/progress.html +4 -48
  53. package/out/resources/connector/connector.py +5 -0
  54. package/out/resources/mcp-server/aider-desk-mcp-server.js +1051 -501
  55. package/out/runner.js +1711 -329
  56. package/package.json +29 -21
  57. package/scripts/generate-package.mjs +10 -2
  58. package/out/renderer/assets/_baseUniq-C6Q8LpuQ.js +0 -381
  59. package/out/renderer/assets/clone-DKkqtIT8.js +0 -8
  60. package/out/renderer/assets/min-CowxrbD6.js +0 -41
package/out/runner.js CHANGED
@@ -31,22 +31,25 @@ const node_sqlite = require("node:sqlite");
31
31
  const path = require("path");
32
32
  const os = require("os");
33
33
  const http = require("http");
34
- const gpt4o = require("gpt-tokenizer/model/gpt-4o");
35
34
  const istextorbinary = require("istextorbinary");
36
35
  const child_process = require("child_process");
36
+ const util = require("util");
37
37
  const treeKill = require("tree-kill");
38
+ const ai = require("ai");
38
39
  const glob = require("glob");
39
40
  const fs$1 = require("fs");
40
- const util = require("util");
41
41
  const dotenvx = require("@dotenvx/dotenvx");
42
42
  const YAML = require("yaml");
43
43
  const posthogNode = require("posthog-node");
44
44
  const langfuseVercel = require("langfuse-vercel");
45
+ const otel = require("@posthog/ai/otel");
45
46
  const simpleGit = require("simple-git");
46
47
  const filenamifyImport = require("filenamify");
47
48
  const slugify = require("slugify");
49
+ const AdmZip = require("adm-zip");
48
50
  const Turndown = require("turndown");
49
51
  const cheerio = require("cheerio");
52
+ const gpt4o = require("gpt-tokenizer/model/gpt-4o");
50
53
  const uuid = require("uuid");
51
54
  const index_js = require("@modelcontextprotocol/sdk/client/index.js");
52
55
  const stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
@@ -63,7 +66,6 @@ const debounce = require("lodash/debounce.js");
63
66
  const crypto = require("crypto");
64
67
  const undici = require("undici");
65
68
  const globalAgent = require("global-agent");
66
- const ai = require("ai");
67
69
  const fileType = require("file-type");
68
70
  const anthropic = require("@ai-sdk/anthropic");
69
71
  const azure = require("@ai-sdk/azure");
@@ -234,7 +236,7 @@ const TASKS_TOOL_DESCRIPTIONS = {
234
236
  [TASKS_TOOL_LIST_TASKS]: "List all tasks in the current project. Returns basic information for each task including id, name, and creation/update timestamps. Use this to get an overview of all available tasks before performing specific task operations.",
235
237
  [TASKS_TOOL_GET_TASK]: "Get comprehensive details about a specific task by its ID. Returns task metadata, current state, list of context files with their read-only status, and the total count of context messages. Use this to understand a task's configuration and context before working with it or its messages.",
236
238
  [TASKS_TOOL_GET_TASK_MESSAGE]: "Retrieve a specific message from a task's conversation history by message index and task ID. The first message (index 0) is always the user's initial prompt, and subsequent messages alternate between user and assistant. Use this to examine the conversation flow, understand previous interactions, or extract specific information from the task history.",
237
- [TASKS_TOOL_CREATE_TASK]: "Create a new task in the current project with an initial prompt. Optionally specify an agent profile ID to use different capabilities, a model ID to override the default model, or a parentTaskId to create a subtask of another task. The parentTaskId parameter is only available for top-level tasks; if the current task is itself a subtask, you cannot create subtasks from it. The new task will start with the provided prompt as its first user message. Use this to begin new work streams, separate different aspects of a project, or break down complex tasks into manageable subtasks. Use execute to create a task and wait for the task to be finished. Use executeInBackground to create a task in the background without waiting for the task to be finished.",
239
+ [TASKS_TOOL_CREATE_TASK]: "Create a new task in the current project with an initial prompt. Optionally use user-provided agentProfileId to use a different agent, a modelId to override the model, or a parentTaskId (if available) to create a subtask of another task. The new task will start with the provided prompt as its first user message. Use this to begin new work streams, separate different aspects of a project, or break down complex tasks into manageable subtasks. Use execute to create a task and wait for the task to be finished. Use executeInBackground to create a task in the background without waiting for the task to be finished.",
238
240
  [TASKS_TOOL_DELETE_TASK]: "Permanently delete a task and all its associated data including messages, context files, and metadata. This action cannot be undone. Note that you cannot delete the currently active task. Use this to clean up completed or abandoned tasks, but be cautious as this removes all task history permanently.",
239
241
  [TASKS_TOOL_SEARCH_TASK]: "Search content within a specific task using semantic search. Use natural language queries with 2-5 descriptive words including key concepts and context. Searches through task conversation history and context files. Use this to find relevant information, discussions, or code snippets within a task.",
240
242
  [TASKS_TOOL_SEARCH_PARENT_TASK]: "Search content within parent task using semantic search. Use natural language queries with 2-5 descriptive words including key concepts and context. Automatically searches parent task conversation history and context files."
@@ -297,7 +299,6 @@ const ProjectSettingsSchema = zod.z.object({
297
299
  reasoningEffort: zod.z.string().optional(),
298
300
  thinkingTokens: zod.z.string().optional(),
299
301
  currentMode: zod.z.string(),
300
- contextCompactingThreshold: zod.z.number().optional(),
301
302
  weakModelLocked: zod.z.boolean().optional(),
302
303
  autoApproveLocked: zod.z.boolean().optional(),
303
304
  updatedFilesGroupMode: zod.z.enum(["grouped", "flat"]).default("flat"),
@@ -335,6 +336,7 @@ var MemoryEmbeddingProvider = /* @__PURE__ */ ((MemoryEmbeddingProvider2) => {
335
336
  var ContextCompactionType = /* @__PURE__ */ ((ContextCompactionType2) => {
336
337
  ContextCompactionType2["Compact"] = "compact";
337
338
  ContextCompactionType2["Handoff"] = "handoff";
339
+ ContextCompactionType2["Smart"] = "smart";
338
340
  return ContextCompactionType2;
339
341
  })(ContextCompactionType || {});
340
342
  var MemoryEmbeddingProgressPhase = /* @__PURE__ */ ((MemoryEmbeddingProgressPhase2) => {
@@ -380,7 +382,7 @@ const TaskDataSchema = zod.z.object({
380
382
  reasoningEffort: zod.z.string().optional(),
381
383
  thinkingTokens: zod.z.string().optional(),
382
384
  currentMode: zod.z.string().optional(),
383
- contextCompactingThreshold: zod.z.number().optional(),
385
+ contextCompactingThresholdTokens: zod.z.number().optional(),
384
386
  weakModelLocked: zod.z.boolean().optional(),
385
387
  handoff: zod.z.boolean().optional(),
386
388
  lastAgentProviderMetadata: zod.z.unknown().optional(),
@@ -455,6 +457,20 @@ const extractTextContent = (content) => {
455
457
  }
456
458
  return "";
457
459
  };
460
+ const extractImagesFromContent = (content) => {
461
+ if (!Array.isArray(content)) {
462
+ return void 0;
463
+ }
464
+ const images = content.filter((part) => part.type === "image").map((part) => {
465
+ const data = part.image;
466
+ if (typeof data !== "string") {
467
+ return void 0;
468
+ }
469
+ const mediaType = part.mediaType || "image/png";
470
+ return data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
471
+ }).filter((v) => v !== void 0);
472
+ return images.length > 0 ? images : void 0;
473
+ };
458
474
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
459
475
  const parseUsageReport = (model, report) => {
460
476
  const sentMatch = report.match(/Tokens: ([\d.]+k?) sent/);
@@ -547,6 +563,7 @@ const extractProviderModel = (modelId) => {
547
563
  const [providerId, ...modelParts] = modelId.split("/");
548
564
  return [providerId, modelParts.join("/")];
549
565
  };
566
+ const isUuid = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
550
567
  const DEFAULT_PAGE_SIZE = 100;
551
568
  class LogBuffer {
552
569
  db = null;
@@ -793,6 +810,7 @@ const AIDER_DESK_CONNECTOR_DIR = path.join(AIDER_DESK_DATA_DIR, "aider-connector
793
810
  const AIDER_DESK_MCP_SERVER_DIR = path.join(AIDER_DESK_DATA_DIR, "mcp-server");
794
811
  const AIDER_DESK_BIN_DIR = path.join(AIDER_DESK_DATA_DIR, "bin");
795
812
  const UV_EXECUTABLE = process.platform === "win32" ? path.join(AIDER_DESK_BIN_DIR, "uv.exe") : path.join(AIDER_DESK_BIN_DIR, "uv");
813
+ const RIPGREP_BINARY_PATH = process.platform === "win32" ? path.join(AIDER_DESK_BIN_DIR, "rg.exe") : path.join(AIDER_DESK_BIN_DIR, "rg");
796
814
  const SERVER_PORT = process.env.AIDER_DESK_PORT ? parseInt(process.env.AIDER_DESK_PORT) : 24337;
797
815
  const PID_FILES_DIR = path.join(AIDER_DESK_DATA_DIR, "aider-processes");
798
816
  const AIDER_DESK_DIR = ".aider-desk";
@@ -816,7 +834,9 @@ const AIDER_DESK_MEMORY_FILE = path.join(AIDER_DESK_DATA_DIR, "memory.db");
816
834
  const EXTENSIONS_REPOS_CACHE_DIR = path.join(AIDER_DESK_CACHE_DIR, "extensions");
817
835
  const POSTHOG_PUBLIC_API_KEY = "phc_AF4zkjrcziXLh8PBFsRSvVr4VZ38p3ezsdX0KDYuElI";
818
836
  const POSTHOG_HOST = "https://eu.i.posthog.com";
819
- process.env.AIDER_DESK_HEADLESS === "true";
837
+ const HEADLESS_MODE = process.env.AIDER_DESK_HEADLESS === "true";
838
+ const APP_TYPE = process.env.AIDER_DESK_APP_TYPE || (HEADLESS_MODE ? "docker" : "electron");
839
+ process.env.AIDER_DESK_DISABLE_MENU === "true";
820
840
  const AUTH_USERNAME = process.env.AIDER_DESK_USERNAME;
821
841
  const AUTH_PASSWORD = process.env.AIDER_DESK_PASSWORD;
822
842
  const CORS_ALLOWED_ORIGINS = process.env.AIDER_DESK_CORS_ALLOWED_ORIGINS;
@@ -875,7 +895,7 @@ class ApprovalManager {
875
895
  async handleToolApproval(toolName, input, key, text, subject) {
876
896
  const extensionResult = await this.task.dispatchExtensionEvent("onToolApproval", { toolName, input });
877
897
  if (extensionResult.blocked) {
878
- return [false, void 0];
898
+ return [false, typeof extensionResult.blocked === "string" ? extensionResult.blocked : void 0];
879
899
  }
880
900
  if (extensionResult.allowed) {
881
901
  return [true, void 0];
@@ -1548,7 +1568,7 @@ const DEFAULT_AGENT_PROFILE = {
1548
1568
  name: "Default Agent",
1549
1569
  provider: "anthropic",
1550
1570
  model: DEFAULT_PROVIDER_MODELS.anthropic,
1551
- maxIterations: 250,
1571
+ maxIterations: 0,
1552
1572
  minTimeBetweenToolCalls: 0,
1553
1573
  toolApprovals: {
1554
1574
  // aider tools
@@ -1869,7 +1889,8 @@ const getDefaultProviderParams = (providerName) => {
1869
1889
  name: "openai-compatible",
1870
1890
  apiKey: "",
1871
1891
  baseUrl: "",
1872
- reasoningEffort: ReasoningEffort.None
1892
+ reasoningEffort: ReasoningEffort.None,
1893
+ trackTokenUsage: true
1873
1894
  };
1874
1895
  break;
1875
1896
  case "litellm":
@@ -1984,7 +2005,7 @@ class TelemetryManager {
1984
2005
  }
1985
2006
  async init() {
1986
2007
  try {
1987
- await Promise.resolve().then(() => require("./open-telemetry-CcefKvbB.js"));
2008
+ await Promise.resolve().then(() => require("./open-telemetry-baOvr6sK.js"));
1988
2009
  const app = getElectronApp();
1989
2010
  this.client = new posthogNode.PostHog(POSTHOG_PUBLIC_API_KEY, {
1990
2011
  host: POSTHOG_HOST
@@ -1994,7 +2015,8 @@ class TelemetryManager {
1994
2015
  distinctId: this.distinctId,
1995
2016
  properties: {
1996
2017
  os: process.platform,
1997
- version: app?.getVersion()
2018
+ version: app?.getVersion(),
2019
+ appType: APP_TYPE
1998
2020
  }
1999
2021
  });
2000
2022
  } catch (error) {
@@ -2157,6 +2179,24 @@ const getLangfuseEnvironmentVariables = (baseDir, settings) => {
2157
2179
  LANGFUSE_HOST: getEffectiveEnvironmentVariable("LANGFUSE_HOST", settings, baseDir)?.value
2158
2180
  };
2159
2181
  };
2182
+ const initializePostHogExporter = () => {
2183
+ const posthogApiKey = getEffectiveEnvironmentVariable("POSTHOG_API_KEY");
2184
+ const posthogHost = getEffectiveEnvironmentVariable("POSTHOG_HOST");
2185
+ if (posthogApiKey) {
2186
+ logger.info("Initializing PostHog Trace Exporter...");
2187
+ return new otel.PostHogTraceExporter({
2188
+ apiKey: posthogApiKey.value,
2189
+ host: posthogHost?.value || "https://us.i.posthog.com"
2190
+ });
2191
+ }
2192
+ return void 0;
2193
+ };
2194
+ const getPostHogAiderEnvironmentVariables = (baseDir, settings) => {
2195
+ return {
2196
+ POSTHOG_API_KEY: getEffectiveEnvironmentVariable("POSTHOG_API_KEY", settings, baseDir)?.value,
2197
+ POSTHOG_API_URL: getEffectiveEnvironmentVariable("POSTHOG_HOST", settings, baseDir)?.value
2198
+ };
2199
+ };
2160
2200
  const readEnvFile = (filePath) => {
2161
2201
  try {
2162
2202
  if (fs$1.existsSync(filePath)) {
@@ -2353,7 +2393,8 @@ const getEnvironmentVariablesForAider = (settings, baseDir) => {
2353
2393
  };
2354
2394
  const getTelemetryEnvironmentVariablesForAider = (settings, baseDir) => {
2355
2395
  return {
2356
- ...getLangfuseEnvironmentVariables(baseDir, settings)
2396
+ ...getLangfuseEnvironmentVariables(baseDir, settings),
2397
+ ...getPostHogAiderEnvironmentVariables(baseDir, settings)
2357
2398
  };
2358
2399
  };
2359
2400
  const getDefaultProjectSettings = (store, providerModels, baseDir, defaultAgentProfileId = DEFAULT_AGENT_PROFILE.id, providers) => {
@@ -2843,8 +2884,8 @@ const downloadUv = async () => {
2843
2884
  strip: 1
2844
2885
  });
2845
2886
  } else if (filename.endsWith(".zip")) {
2846
- const AdmZip = (await import("adm-zip")).default;
2847
- const zip = new AdmZip(tempFile);
2887
+ const AdmZip2 = (await import("adm-zip")).default;
2888
+ const zip = new AdmZip2(tempFile);
2848
2889
  zip.extractAllTo(AIDER_DESK_BIN_DIR, true);
2849
2890
  }
2850
2891
  if (platform !== "win32") {
@@ -2907,6 +2948,111 @@ const getLatestPythonLibVersion = async (library) => {
2907
2948
  return null;
2908
2949
  }
2909
2950
  };
2951
+ const RIPGREP_VERSION = "15.1.0";
2952
+ const RIPGREP_BASE_URL = `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}`;
2953
+ const getRipgrepAssetName = () => {
2954
+ const platform = process.platform;
2955
+ const arch = process.arch;
2956
+ if (platform === "win32") {
2957
+ return arch === "arm64" ? `ripgrep-${RIPGREP_VERSION}-aarch64-pc-windows-msvc.zip` : `ripgrep-${RIPGREP_VERSION}-x86_64-pc-windows-msvc.zip`;
2958
+ }
2959
+ if (platform === "darwin") {
2960
+ return arch === "arm64" ? `ripgrep-${RIPGREP_VERSION}-aarch64-apple-darwin.tar.gz` : `ripgrep-${RIPGREP_VERSION}-x86_64-apple-darwin.tar.gz`;
2961
+ }
2962
+ if (arch === "arm64") {
2963
+ return `ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`;
2964
+ }
2965
+ return `ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz`;
2966
+ };
2967
+ const isRipgrepAvailable = () => {
2968
+ try {
2969
+ if (!fs$1.existsSync(RIPGREP_BINARY_PATH)) {
2970
+ return false;
2971
+ }
2972
+ const result = child_process.execSync(`"${RIPGREP_BINARY_PATH}" --version`, {
2973
+ encoding: "utf8",
2974
+ timeout: 5e3,
2975
+ windowsHide: true
2976
+ });
2977
+ if (!result.includes("ripgrep")) {
2978
+ return false;
2979
+ }
2980
+ logger.debug(`ripgrep is available at ${RIPGREP_BINARY_PATH}: ${result.trim()}`);
2981
+ return true;
2982
+ } catch {
2983
+ return false;
2984
+ }
2985
+ };
2986
+ const downloadRipgrep = async () => {
2987
+ const filename = getRipgrepAssetName();
2988
+ const url = `${RIPGREP_BASE_URL}/${filename}`;
2989
+ if (!fs$1.existsSync(AIDER_DESK_BIN_DIR)) {
2990
+ fs$1.mkdirSync(AIDER_DESK_BIN_DIR, { recursive: true });
2991
+ }
2992
+ const tempFile = path.join(AIDER_DESK_BIN_DIR, filename);
2993
+ const rgExeName = process.platform === "win32" ? "rg.exe" : "rg";
2994
+ try {
2995
+ logger.info(`Downloading ripgrep from ${url}`);
2996
+ const response = await fetch(url);
2997
+ if (!response.ok) {
2998
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2999
+ }
3000
+ const buffer = Buffer.from(await response.arrayBuffer());
3001
+ fs$1.writeFileSync(tempFile, buffer);
3002
+ if (filename.endsWith(".tar.gz")) {
3003
+ const { extract } = await import("tar");
3004
+ await extract({
3005
+ cwd: AIDER_DESK_BIN_DIR,
3006
+ file: tempFile,
3007
+ strip: 1
3008
+ });
3009
+ } else if (filename.endsWith(".zip")) {
3010
+ const zip = new AdmZip(tempFile);
3011
+ const entry = zip.getEntry(rgExeName) || zip.getEntries().find((e) => e.entryName.endsWith(rgExeName));
3012
+ if (entry) {
3013
+ fs$1.writeFileSync(path.join(AIDER_DESK_BIN_DIR, rgExeName), entry.getData());
3014
+ } else {
3015
+ throw new Error(`Could not find ${rgExeName} in zip archive`);
3016
+ }
3017
+ }
3018
+ if (process.platform !== "win32") {
3019
+ fs$1.chmodSync(path.join(AIDER_DESK_BIN_DIR, rgExeName), 493);
3020
+ }
3021
+ logger.info(`ripgrep downloaded successfully to ${AIDER_DESK_BIN_DIR}`);
3022
+ } finally {
3023
+ if (fs$1.existsSync(tempFile)) {
3024
+ fs$1.unlinkSync(tempFile);
3025
+ }
3026
+ }
3027
+ };
3028
+ let ripgrepEnsured = false;
3029
+ let ripgrepEnsurePromise = null;
3030
+ const ensureRipgrepBinary = async () => {
3031
+ if (ripgrepEnsured) {
3032
+ return true;
3033
+ }
3034
+ if (!ripgrepEnsurePromise) {
3035
+ ripgrepEnsurePromise = (async () => {
3036
+ if (isRipgrepAvailable()) {
3037
+ ripgrepEnsured = true;
3038
+ return;
3039
+ }
3040
+ await downloadRipgrep();
3041
+ if (!isRipgrepAvailable()) {
3042
+ throw new Error("ripgrep binary not found after download");
3043
+ }
3044
+ ripgrepEnsured = true;
3045
+ })();
3046
+ }
3047
+ try {
3048
+ await ripgrepEnsurePromise;
3049
+ return true;
3050
+ } catch (error) {
3051
+ ripgrepEnsurePromise = null;
3052
+ logger.error("Failed to ensure ripgrep binary:", error);
3053
+ return false;
3054
+ }
3055
+ };
2910
3056
  const isAbortError = (error) => {
2911
3057
  if (!(error instanceof Error)) {
2912
3058
  return false;
@@ -3078,18 +3224,25 @@ const expandTilde = (filePath) => {
3078
3224
  }
3079
3225
  return filePath;
3080
3226
  };
3081
- const readFileContent = async (absolutePath, withLines = false, lineOffset = 0, lineLimit = 1e3, sizeLimit = 0.05 * lineLimit) => {
3227
+ const readFileContent = async (absolutePath, withLines = false, lineOffset = 0, lineLimit = 1e3, sizeLimit = Math.max(50, 0.05 * lineLimit)) => {
3082
3228
  const fileContentBuffer = await fs.readFile(absolutePath);
3083
3229
  if (istextorbinary.isBinary(absolutePath, fileContentBuffer)) {
3084
3230
  throw new Error("Binary files cannot be read.");
3085
3231
  }
3086
- const fileSizeKB = fileContentBuffer.length / 1024;
3087
- if (fileSizeKB > sizeLimit) {
3088
- const truncatedBytes = fileContentBuffer.subarray(0, Math.floor(sizeLimit * 1024));
3232
+ const fileContent = fileContentBuffer.toString("utf8");
3233
+ const lines = fileContent.split("\n");
3234
+ const totalLines = lines.length;
3235
+ const startIndex = Math.max(0, lineOffset);
3236
+ const endIndex = Math.min(totalLines, startIndex + lineLimit);
3237
+ const limitedLines = lines.slice(startIndex, endIndex);
3238
+ const limitedContent = limitedLines.join("\n");
3239
+ const limitedSizeKB = Buffer.byteLength(limitedContent, "utf8") / 1024;
3240
+ if (limitedSizeKB > sizeLimit) {
3241
+ const truncatedBytes = Buffer.from(limitedContent, "utf8").subarray(0, Math.floor(sizeLimit * 1024));
3089
3242
  const truncatedContent = truncatedBytes.toString("utf8");
3090
3243
  const truncatedLines = truncatedContent.split("\n");
3091
3244
  if (withLines) {
3092
- return truncatedLines.map((line, index) => `${index + 1}|${line}`).join("\n") + `
3245
+ return truncatedLines.map((line, index) => `${startIndex + index + 1}|${line}`).join("\n") + `
3093
3246
 
3094
3247
  File size limit (${sizeLimit.toFixed(1)} KB) exceeded. Use shell commands (e.g., head, tail, grep) to read specific parts.`;
3095
3248
  }
@@ -3097,33 +3250,27 @@ File size limit (${sizeLimit.toFixed(1)} KB) exceeded. Use shell commands (e.g.,
3097
3250
 
3098
3251
  File size limit (${sizeLimit.toFixed(1)} KB) exceeded. Use shell commands (e.g., head, tail, grep) to read specific parts.`;
3099
3252
  }
3100
- const fileContent = fileContentBuffer.toString("utf8");
3101
- const lines = fileContent.split("\n");
3102
- const totalLines = lines.length;
3103
- const startIndex = Math.max(0, lineOffset);
3104
- const endIndex = Math.min(totalLines, startIndex + lineLimit);
3105
- let limitedLines = lines.slice(startIndex, endIndex);
3253
+ let resultLines = limitedLines;
3106
3254
  if (withLines) {
3107
- limitedLines = limitedLines.map((line, index) => `${startIndex + index + 1}|${line}`);
3255
+ resultLines = limitedLines.map((line, index) => `${startIndex + index + 1}|${line}`);
3108
3256
  }
3109
3257
  if (endIndex < totalLines) {
3110
- limitedLines.push("...");
3111
- limitedLines.push(`Total lines in the file: ${totalLines}`);
3258
+ resultLines = [...resultLines, "...", `Total lines in the file: ${totalLines}`];
3112
3259
  }
3113
- return limitedLines.join("\n");
3260
+ return resultLines.join("\n");
3114
3261
  };
3115
- const truncateToolResult = async (content, maxLines = 1e3, maxSizeKB = 50) => {
3262
+ const truncateToolResult = async (content, maxLines = 1e3, maxSizeKB = 50, maxTokens = 5e4) => {
3116
3263
  const lines = content.split("\n");
3117
3264
  const sizeBytes = Buffer.byteLength(content, "utf8");
3118
3265
  const sizeKB = sizeBytes / 1024;
3119
- if (lines.length <= maxLines && sizeKB <= maxSizeKB) {
3266
+ const tokenCount = maxTokens === Infinity ? 0 : gpt4o.encode(content).length;
3267
+ if (lines.length <= maxLines && sizeKB <= maxSizeKB && tokenCount <= maxTokens) {
3120
3268
  return content;
3121
3269
  }
3122
3270
  const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
3123
3271
  const tmpFileName = `aider-desk-tool-result-${id}.txt`;
3124
3272
  const tmpFilePath = path.join(os.tmpdir(), tmpFileName);
3125
3273
  await fs.writeFile(tmpFilePath, content, "utf8");
3126
- const previewLines = lines.slice(0, maxLines);
3127
3274
  const reasons = [];
3128
3275
  if (lines.length > maxLines) {
3129
3276
  reasons.push(`${lines.length} lines exceeded limit of ${maxLines}`);
@@ -3131,9 +3278,126 @@ const truncateToolResult = async (content, maxLines = 1e3, maxSizeKB = 50) => {
3131
3278
  if (sizeKB > maxSizeKB) {
3132
3279
  reasons.push(`${sizeKB.toFixed(1)} KB exceeded limit of ${maxSizeKB} KB`);
3133
3280
  }
3134
- return previewLines.join("\n") + `
3281
+ if (tokenCount > maxTokens) {
3282
+ reasons.push(`${tokenCount} tokens exceeded limit of ${maxTokens}`);
3283
+ }
3284
+ if (tokenCount > maxTokens) {
3285
+ const headBudget = Math.floor(maxTokens / 2);
3286
+ const tailBudget = maxTokens - headBudget;
3287
+ const headLines = [];
3288
+ let headTokens = 0;
3289
+ for (const line of lines) {
3290
+ const lineTokens = gpt4o.encode(line).length;
3291
+ if (headTokens + lineTokens > headBudget) {
3292
+ break;
3293
+ }
3294
+ headLines.push(line);
3295
+ headTokens += lineTokens;
3296
+ }
3297
+ const tailLines = [];
3298
+ let tailTokens = 0;
3299
+ for (let i = lines.length - 1; i >= 0; i--) {
3300
+ if (headLines.length + tailLines.length >= lines.length) {
3301
+ break;
3302
+ }
3303
+ const lineTokens = gpt4o.encode(lines[i]).length;
3304
+ if (tailTokens + lineTokens > tailBudget) {
3305
+ break;
3306
+ }
3307
+ tailLines.unshift(lines[i]);
3308
+ tailTokens += lineTokens;
3309
+ }
3310
+ const omittedLines = lines.length - headLines.length - tailLines.length;
3311
+ const truncationNotice = `
3312
+
3313
+ ... ${omittedLines} lines omitted (${reasons.join(", ")}). Full content saved to ${tmpFilePath}.
3314
+
3315
+ `;
3316
+ return headLines.join("\n") + truncationNotice + tailLines.join("\n");
3317
+ }
3318
+ let preview;
3319
+ if (sizeKB > maxSizeKB) {
3320
+ const maxBytes = Math.floor(maxSizeKB * 1024);
3321
+ const contentBuffer = Buffer.from(content, "utf8");
3322
+ preview = contentBuffer.subarray(0, maxBytes).toString("utf8");
3323
+ } else {
3324
+ preview = lines.slice(0, maxLines).join("\n");
3325
+ }
3326
+ return preview + `
3135
3327
  ... Content truncated (${reasons.join(", ")}). Full content saved to ${tmpFilePath}.`;
3136
3328
  };
3329
+ const NETWORK_ERROR_CODES = ["ECONNRESET", "EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ENETUNREACH", "EAI_AGAIN"];
3330
+ const UNDICI_ERROR_PREFIX = "UND_ERR_";
3331
+ const isNetworkError = (error) => {
3332
+ if (!(error instanceof Error)) {
3333
+ return false;
3334
+ }
3335
+ if (error instanceof TypeError && error.message === "terminated") {
3336
+ return true;
3337
+ }
3338
+ if ("code" in error) {
3339
+ const code = error.code;
3340
+ if (typeof code === "string") {
3341
+ if (code.startsWith(UNDICI_ERROR_PREFIX)) {
3342
+ return true;
3343
+ }
3344
+ if (NETWORK_ERROR_CODES.includes(code)) {
3345
+ return true;
3346
+ }
3347
+ }
3348
+ }
3349
+ if (error.cause instanceof Error && isNetworkError(error.cause)) {
3350
+ return true;
3351
+ }
3352
+ return false;
3353
+ };
3354
+ const IMAGE_TOKEN_ESTIMATE = 1e3;
3355
+ const extractToolResultText = (output) => {
3356
+ if (!output || typeof output !== "object") {
3357
+ return "";
3358
+ }
3359
+ const o = output;
3360
+ if (o.type === "text" || o.type === "error-text") {
3361
+ return String(o.value ?? "");
3362
+ }
3363
+ if (o.type === "json" || o.type === "error-json") {
3364
+ return JSON.stringify(o.value);
3365
+ }
3366
+ if (o.type === "content" && Array.isArray(o.value)) {
3367
+ return o.value.map((v) => v.type === "text" ? v.text ?? "" : v.type === "media" ? "[media]" : "").join("\n");
3368
+ }
3369
+ return JSON.stringify(output);
3370
+ };
3371
+ const estimateMessageTokens = (messages) => {
3372
+ let estimatedImageTokens = 0;
3373
+ const textOnlyMessages = messages.map((msg) => {
3374
+ if (typeof msg.content === "string") {
3375
+ return msg;
3376
+ }
3377
+ if (!Array.isArray(msg.content)) {
3378
+ return { role: msg.role, content: "" };
3379
+ }
3380
+ const parts = msg.content;
3381
+ const textParts = [];
3382
+ for (const part of parts) {
3383
+ const type = part.type;
3384
+ if (type === "text" && typeof part.text === "string") {
3385
+ textParts.push(part.text);
3386
+ } else if (type === "tool-call") {
3387
+ textParts.push(JSON.stringify(part.input ?? ""));
3388
+ } else if (type === "tool-result") {
3389
+ textParts.push(extractToolResultText(part.output));
3390
+ } else if (type === "reasoning" && typeof part.text === "string") {
3391
+ textParts.push(part.text);
3392
+ } else if (type === "image") {
3393
+ estimatedImageTokens += IMAGE_TOKEN_ESTIMATE;
3394
+ }
3395
+ }
3396
+ return { role: msg.role, content: textParts.join("\n\n") };
3397
+ });
3398
+ return textOnlyMessages.reduce((sum, msg) => sum + gpt4o.encode(typeof msg.content === "string" ? msg.content : "").length, 0) + estimatedImageTokens;
3399
+ };
3400
+ const execFileAsync = util.promisify(child_process.execFile);
3137
3401
  const fileLocks = /* @__PURE__ */ new Map();
3138
3402
  const withFileLock = (filePath, operation) => {
3139
3403
  logger.debug("Acquiring file lock:", { filePath });
@@ -3445,50 +3709,80 @@ Do not use escape characters \\ in the string like \\n or \\" and others. Do not
3445
3709
  if (!isApproved) {
3446
3710
  return `Grep search for '${searchTerm}' in files matching '${filePattern}' denied by user. Reason: ${userInput}`;
3447
3711
  }
3712
+ const rgAvailable = await ensureRipgrepBinary();
3713
+ if (!rgAvailable) {
3714
+ return "Error: ripgrep binary is not available. Please try again or check the logs for details.";
3715
+ }
3448
3716
  try {
3449
- const files = await glob.glob(filePattern, {
3450
- cwd: task.getTaskDir(),
3451
- nodir: true,
3452
- absolute: true,
3453
- signal: abortSignal
3717
+ const rgArgs = ["--no-heading", "--line-number", "--color", "never", "--max-count", String(maxResults)];
3718
+ if (!caseSensitive) {
3719
+ rgArgs.push("-i");
3720
+ }
3721
+ if (contextLines > 0) {
3722
+ rgArgs.push("-C", String(contextLines));
3723
+ }
3724
+ rgArgs.push("-g", filePattern);
3725
+ rgArgs.push("--", searchTerm, ".");
3726
+ const taskDir = task.getTaskDir();
3727
+ logger.debug("Executing ripgrep:", {
3728
+ binary: RIPGREP_BINARY_PATH,
3729
+ args: rgArgs,
3730
+ cwd: taskDir
3454
3731
  });
3455
- if (files.length === 0) {
3456
- return `No files found matching pattern '${filePattern}'.`;
3457
- }
3458
- const filteredFiles = await filterIgnoredFiles(task.getTaskDir(), files);
3459
- if (filteredFiles.length === 0) {
3460
- return `No files found matching pattern '${filePattern}' (all files were ignored).`;
3732
+ const { stdout, stderr } = await execFileAsync(RIPGREP_BINARY_PATH, rgArgs, {
3733
+ cwd: taskDir,
3734
+ env: { ...process.env, TERM: "dumb", PATH: getShellPath() },
3735
+ maxBuffer: 10 * 1024 * 1024,
3736
+ windowsHide: true
3737
+ });
3738
+ logger.debug("ripgrep stdout raw:", { stdout: stdout.substring(0, 2e3) });
3739
+ logger.debug("ripgrep stderr:", { stderr: stderr.substring(0, 2e3) });
3740
+ const outputLines = stdout.split("\n").filter(Boolean);
3741
+ logger.debug("outputLines count:", { count: outputLines.length, lines: outputLines.slice(0, 20) });
3742
+ if (outputLines.length === 0) {
3743
+ return `No matches found for pattern '${searchTerm}' in files matching '${filePattern}'.`;
3461
3744
  }
3462
3745
  const results = [];
3463
- const searchRegex = new RegExp(searchTerm, caseSensitive ? void 0 : "i");
3464
- for (const absoluteFilePath of filteredFiles) {
3465
- const fileContent = await fs.readFile(absoluteFilePath, {
3466
- encoding: "utf8",
3467
- signal: abortSignal
3468
- });
3469
- const lines2 = fileContent.split("\n");
3470
- const relativeFilePath = path.relative(task.getTaskDir(), absoluteFilePath);
3471
- for (let index = 0; index < lines2.length; index++) {
3472
- const line = lines2[index];
3473
- if (searchRegex.test(line)) {
3474
- if (results.length >= maxResults) {
3475
- break;
3476
- }
3477
- const matchResult = {
3478
- filePath: relativeFilePath,
3479
- lineNumber: index + 1,
3480
- lineContent: line
3481
- };
3482
- if (contextLines > 0) {
3483
- const start = Math.max(0, index - contextLines);
3484
- const end = Math.min(lines2.length - 1, index + contextLines);
3485
- matchResult.context = lines2.slice(start, end + 1);
3746
+ const outputLineRegex = /^(.+?)([:-])(\d+)\2(.*)$/;
3747
+ for (let rawLine of outputLines) {
3748
+ if (rawLine.endsWith("\r")) {
3749
+ rawLine = rawLine.slice(0, -1);
3750
+ }
3751
+ const line = rawLine;
3752
+ const match = outputLineRegex.exec(line);
3753
+ if (!match) {
3754
+ continue;
3755
+ }
3756
+ const [, rawFilePath, separator, lineNumStr, content] = match;
3757
+ const lineNum = parseInt(lineNumStr, 10);
3758
+ if (isNaN(lineNum)) {
3759
+ continue;
3760
+ }
3761
+ const filePath = rawFilePath.startsWith("./") ? rawFilePath.slice(2) : rawFilePath;
3762
+ const isMatch = separator === ":";
3763
+ if (isMatch) {
3764
+ results.push({
3765
+ filePath,
3766
+ lineNumber: lineNum,
3767
+ lineContent: content
3768
+ });
3769
+ } else {
3770
+ const lastResult = results[results.length - 1];
3771
+ if (lastResult) {
3772
+ if (!lastResult.context) {
3773
+ lastResult.context = [];
3486
3774
  }
3487
- results.push(matchResult);
3775
+ lastResult.context.push(content);
3488
3776
  }
3489
3777
  }
3778
+ if (results.length >= maxResults) {
3779
+ break;
3780
+ }
3490
3781
  }
3491
3782
  if (results.length === 0) {
3783
+ if (stderr.trim()) {
3784
+ return `Error during grep: ${stderr.trim()}`;
3785
+ }
3492
3786
  return `No matches found for pattern '${searchTerm}' in files matching '${filePattern}'.`;
3493
3787
  }
3494
3788
  const grouped = {};
@@ -3498,33 +3792,33 @@ Do not use escape characters \\ in the string like \\n or \\" and others. Do not
3498
3792
  }
3499
3793
  grouped[r.filePath].push(r);
3500
3794
  }
3501
- const lines = [];
3502
- lines.push(`## Grep Results: \`${searchTerm}\` in \`${filePattern}\` (${results.length} matches)`);
3503
- lines.push("");
3795
+ const markdownLines = [];
3796
+ markdownLines.push(`## Grep Results: \`${searchTerm}\` in \`${filePattern}\` (${results.length} matches)`);
3797
+ markdownLines.push("");
3504
3798
  for (const [filePath, matches] of Object.entries(grouped)) {
3505
- lines.push(`### ${filePath} (${matches.length} ${matches.length === 1 ? "match" : "matches"})`);
3799
+ markdownLines.push(`### ${filePath} (${matches.length} ${matches.length === 1 ? "match" : "matches"})`);
3506
3800
  for (const match of matches) {
3507
3801
  const escapedContent = match.lineContent.replace(/`/g, "\\`");
3508
- lines.push(`- **L${match.lineNumber}:** \`${escapedContent}\``);
3802
+ markdownLines.push(`- **L${match.lineNumber}:** \`${escapedContent}\``);
3509
3803
  if (match.context && match.context.length > 0) {
3510
- lines.push(" ```");
3804
+ markdownLines.push(" ```");
3511
3805
  for (const ctxLine of match.context) {
3512
- lines.push(` ${ctxLine}`);
3806
+ markdownLines.push(` ${ctxLine}`);
3513
3807
  }
3514
- lines.push(" ```");
3808
+ markdownLines.push(" ```");
3515
3809
  }
3516
3810
  }
3517
- lines.push("");
3811
+ markdownLines.push("");
3518
3812
  }
3519
3813
  const notices = [];
3520
3814
  if (results.length >= maxResults) {
3521
3815
  notices.push(`${maxResults} matches limit reached. Use maxResults=${maxResults * 2} for more, or refine pattern`);
3522
3816
  }
3523
3817
  if (notices.length > 0) {
3524
- lines.push("---");
3525
- lines.push(`[${notices.join(". ")}]`);
3818
+ markdownLines.push("---");
3819
+ markdownLines.push(`[${notices.join(". ")}]`);
3526
3820
  }
3527
- return lines.join("\n");
3821
+ return await truncateToolResult(markdownLines.join("\n"), 1e3, 50, 1e4);
3528
3822
  } catch (error) {
3529
3823
  if (isAbortError(error)) {
3530
3824
  return "Operation was cancelled by user.";
@@ -3581,6 +3875,18 @@ Timeout: ${timeout}ms`;
3581
3875
  return `Bash command execution denied by user. Reason: ${userInput}`;
3582
3876
  }
3583
3877
  const absoluteCwd = expandedCwd ? path.resolve(task.getTaskDir(), expandedCwd) : task.getTaskDir();
3878
+ const isPosix = process.platform !== "win32";
3879
+ const killProcess = (pid) => {
3880
+ if (isPosix) {
3881
+ try {
3882
+ process.kill(-pid, "SIGKILL");
3883
+ } catch {
3884
+ treeKill(pid, "SIGKILL");
3885
+ }
3886
+ } else {
3887
+ treeKill(pid, "SIGKILL");
3888
+ }
3889
+ };
3584
3890
  return await new Promise((resolve) => {
3585
3891
  let stdout = "";
3586
3892
  let stderr = "";
@@ -3604,8 +3910,8 @@ Timeout: ${timeout}ms`;
3604
3910
  }
3605
3911
  isResolved = true;
3606
3912
  cleanup();
3607
- const truncatedStdout = await truncateToolResult(stdout);
3608
- const truncatedStderr = await truncateToolResult(stderr);
3913
+ const truncatedStdout = await truncateToolResult(stdout, 1e3, 50, 1e4);
3914
+ const truncatedStderr = await truncateToolResult(stderr, 1e3, 50, 1e4);
3609
3915
  resolve({ stdout: truncatedStdout, stderr: truncatedStderr, exitCode });
3610
3916
  };
3611
3917
  abortListener = () => {
@@ -3613,7 +3919,7 @@ Timeout: ${timeout}ms`;
3613
3919
  return;
3614
3920
  }
3615
3921
  if (childProcess?.pid) {
3616
- treeKill(childProcess.pid, "SIGKILL");
3922
+ killProcess(childProcess.pid);
3617
3923
  }
3618
3924
  stderr = "Operation was cancelled by user.";
3619
3925
  exitCode = 130;
@@ -3627,6 +3933,7 @@ Timeout: ${timeout}ms`;
3627
3933
  env: { ...process.env, TERM: "dumb", DEBIAN_FRONTEND: "noninteractive", PATH: getShellPath() },
3628
3934
  stdio: ["ignore", "pipe", "pipe"],
3629
3935
  // Explicitly pipe stdout and stderr to capture output from piped commands
3936
+ detached: isPosix,
3630
3937
  signal: abortSignal
3631
3938
  });
3632
3939
  timeoutHandle = setTimeout(() => {
@@ -3634,7 +3941,7 @@ Timeout: ${timeout}ms`;
3634
3941
  return;
3635
3942
  }
3636
3943
  if (childProcess?.pid) {
3637
- treeKill(childProcess.pid, "SIGKILL");
3944
+ killProcess(childProcess.pid);
3638
3945
  }
3639
3946
  stderr = `Error: Command timed out after ${timeout}ms. Consider increasing the timeout parameter.`;
3640
3947
  exitCode = 124;
@@ -3745,7 +4052,8 @@ Format: ${format}`;
3745
4052
  return `Error: Invalid URL provided: ${url}. Please provide a valid URL.`;
3746
4053
  }
3747
4054
  try {
3748
- return await scrapeWeb(url, timeout, abortSignal, format);
4055
+ const content = await scrapeWeb(url, timeout, abortSignal, format);
4056
+ return await truncateToolResult(content, 1e3, 50, 1e4);
3749
4057
  } catch (error) {
3750
4058
  if (isAbortError(error)) {
3751
4059
  return "Operation was cancelled by user.";
@@ -4014,6 +4322,11 @@ const createTasksToolset = (settings, task, profile, promptContext) => {
4014
4322
  if (state) {
4015
4323
  allTasks = allTasks.filter((t) => t.state === state);
4016
4324
  }
4325
+ allTasks.sort((a, b) => {
4326
+ const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
4327
+ const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
4328
+ return dateB - dateA;
4329
+ });
4017
4330
  const startIndex = Math.max(0, offset);
4018
4331
  const endIndex = startIndex + Math.max(0, limit || allTasks.length);
4019
4332
  const paginatedTasks = allTasks.slice(startIndex, endIndex);
@@ -4130,34 +4443,35 @@ const createTasksToolset = (settings, task, profile, promptContext) => {
4130
4443
  }
4131
4444
  }
4132
4445
  });
4133
- const isSubtask = task.task.parentId !== null;
4134
4446
  const autoGenerateTaskName = settings.taskSettings.autoGenerateTaskName;
4135
4447
  const nameProperty = autoGenerateTaskName ? zod.z.string().optional().describe("Optional concise name for the new task. If not provided, the task name will be auto-generated from the prompt.") : zod.z.string().describe("Concise name for the new task.");
4136
- const CreateTaskInputSchema = zod.z.object({
4137
- prompt: zod.z.string().describe("The initial prompt for the new task"),
4138
- name: nameProperty,
4139
- agentProfileId: zod.z.string().optional().describe("Optional agent profile ID to use for the task. Use only when explicitly requested by the user."),
4140
- modelId: zod.z.string().optional().describe("Optional model ID to use for the task. Use only when explicitly requested by the user."),
4141
- mode: zod.z.string().optional().default("agent").describe("Optional mode to use for the task. Use only when explicitly requested by the user."),
4142
- asSubtask: zod.z.boolean().optional().default(false).describe("If true, the task will be created as a subtask of the current task. Use only when explicitly requested by the user."),
4143
- autoApprove: zod.z.boolean().optional().default(false).describe(
4144
- "If true, the task will be created with auto-approve enabled. Set autoApprove to true when no planning is needed, just execution of the task with all the work."
4145
- ),
4146
- worktree: zod.z.boolean().optional().default(false).describe("If true, the task will be created in worktree mode. Use only when explicitly requested by the user."),
4147
- executeAndWait: zod.z.boolean().optional().default(false).describe("If true, the task will be created and executed immediately and the tool will wait for it to complete before returning."),
4148
- executeInBackground: zod.z.boolean().optional().default(false).describe("If true, the task will be created and executed in the background.")
4149
- });
4150
- const CreateTaskWithParentInputSchema = CreateTaskInputSchema.extend({
4151
- parentTaskId: zod.z.string().nullable().optional().describe(
4152
- `Optional ID of the parent task. If provided, the new task will be created as a subtask of the specified parent. Use the current task's ID (${task.taskId}) to create a subtask of the current task. When asSubtask is true, this tasks ID will be used automatically.`
4153
- )
4154
- });
4448
+ const availableAgents = task.getProject().getAgentProfiles().map((p) => {
4449
+ const slug = deriveDirName(p.name, /* @__PURE__ */ new Set());
4450
+ const shortId = isUuid(p.id) ? p.id.substring(0, 8) : p.id;
4451
+ return `${slug}(${shortId})`;
4452
+ }).join(",");
4155
4453
  const createTaskTool = ai.tool({
4156
4454
  description: TASKS_TOOL_DESCRIPTIONS[TASKS_TOOL_CREATE_TASK],
4157
- inputSchema: isSubtask ? CreateTaskInputSchema : CreateTaskWithParentInputSchema,
4455
+ inputSchema: zod.z.object({
4456
+ prompt: zod.z.string().describe("The initial prompt for the new task"),
4457
+ name: nameProperty,
4458
+ agentProfileId: zod.z.string().optional().describe(`Optional agent profile ID or name. Available agents: ${availableAgents}. Use only when explicitly requested by the user.`),
4459
+ modelId: zod.z.string().optional().describe("Optional model ID in the format `provider/model` to use for the task. Use only when explicitly requested by the user."),
4460
+ mode: zod.z.string().optional().default("agent").describe("Optional mode to use for the task. Use only when explicitly requested by the user."),
4461
+ asSubtask: zod.z.boolean().optional().default(false).describe("If true, the task will be created as a subtask of the current task. Use only when explicitly requested by the user."),
4462
+ parentTaskId: zod.z.string().nullable().optional().describe(
4463
+ "Optional ID of the parent task. If provided, the new task will be created as a subtask of the specified parent. When asSubtask is specified as true, this is not needed as parent ID is resolved automatically."
4464
+ ),
4465
+ autoApprove: zod.z.boolean().optional().default(false).describe(
4466
+ "If true, the task will be created with auto-approve enabled. Set autoApprove to true when no planning is needed, just execution of the task with all the work."
4467
+ ),
4468
+ worktree: zod.z.boolean().optional().default(false).describe("If true, the task will be created in worktree mode. Use only when explicitly requested by the user."),
4469
+ executeAndWait: zod.z.boolean().optional().default(false).describe("If true, the task will be created and executed immediately and the tool will wait for it to complete before returning."),
4470
+ executeInBackground: zod.z.boolean().optional().default(false).describe("If true, the task will be created and executed in the background.")
4471
+ }),
4158
4472
  execute: async (input, { toolCallId }) => {
4159
4473
  const { prompt, name, agentProfileId, modelId, mode = "agent", asSubtask = false, autoApprove, worktree, executeAndWait, executeInBackground } = input;
4160
- const parentTaskId = asSubtask ? task.taskId : "parentTaskId" in input ? input.parentTaskId : void 0;
4474
+ const parentTaskId = asSubtask ? task.task.parentId || task.taskId : "parentTaskId" in input ? input.parentTaskId : void 0;
4161
4475
  task.addToolMessage(toolCallId, TASKS_TOOL_GROUP_NAME, TASKS_TOOL_CREATE_TASK, input, void 0, void 0, promptContext);
4162
4476
  const toolName = `${TASKS_TOOL_GROUP_NAME}${TOOL_GROUP_NAME_SEPARATOR}${TASKS_TOOL_CREATE_TASK}`;
4163
4477
  const questionKey = toolName;
@@ -4179,13 +4493,22 @@ Parent Task ID: ${parentTaskId || "none (top-level task)"}` : ""}`;
4179
4493
  });
4180
4494
  const updates = {};
4181
4495
  if (agentProfileId) {
4182
- updates.agentProfileId = agentProfileId;
4496
+ const resolvedProfile = task.getProject().resolveAgentProfile(agentProfileId);
4497
+ if (resolvedProfile) {
4498
+ updates.agentProfileId = resolvedProfile.id;
4499
+ } else {
4500
+ const availableProfiles = task.getProject().getAgentProfiles().map((p) => `"${p.name}"`).join(", ");
4501
+ return `Agent profile '${agentProfileId}' not found. Available profiles: ${availableProfiles}`;
4502
+ }
4183
4503
  }
4184
4504
  if (modelId) {
4185
4505
  const [provider, ...modelParts] = modelId.split("/");
4186
4506
  updates.provider = provider;
4187
4507
  updates.model = modelParts.join("/");
4188
4508
  updates.mainModel = modelId;
4509
+ } else {
4510
+ updates.provider = void 0;
4511
+ updates.model = void 0;
4189
4512
  }
4190
4513
  const taskInstance = task.getProject().getTask(newTask.id);
4191
4514
  if (!taskInstance) {
@@ -5774,6 +6097,7 @@ ${reminder}
5774
6097
  "onImportantReminders",
5775
6098
  {
5776
6099
  profile,
6100
+ agentProfile: profile,
5777
6101
  remindersContent
5778
6102
  },
5779
6103
  task.project,
@@ -5791,9 +6115,12 @@ ${reminder}
5791
6115
  ${remindersContent}
5792
6116
  </ThisIsImportant>`;
5793
6117
  }
5794
- const updatedFirstUserMessage = {
6118
+ const updatedFirstUserMessage = typeof userRequestMessage.content === "string" ? {
5795
6119
  ...userRequestMessage,
5796
6120
  content: `${userRequestMessage.content}${remindersContent}`
6121
+ } : {
6122
+ ...userRequestMessage,
6123
+ content: [...userRequestMessage.content, { type: "text", text: remindersContent }]
5797
6124
  };
5798
6125
  const newMessages = [...messages];
5799
6126
  newMessages[userRequestMessageIndex] = updatedFirstUserMessage;
@@ -6237,9 +6564,9 @@ ${fileList}`
6237
6564
  const extensionTools = this.extensionManager.createExtensionToolset(task, mode, profile, toolSet, abortSignal);
6238
6565
  Object.assign(toolSet, extensionTools);
6239
6566
  }
6240
- return this.wrapToolsWithHooks(task, toolSet, abortSignal, promptContext);
6567
+ return this.wrapToolsWithHooks(task, profile, toolSet, abortSignal, promptContext);
6241
6568
  }
6242
- wrapToolsWithHooks(task, toolSet, abortSignal, promptContext) {
6569
+ wrapToolsWithHooks(task, profile, toolSet, abortSignal, promptContext) {
6243
6570
  const wrappedToolSet = {};
6244
6571
  for (const [toolName, toolDef] of Object.entries(toolSet)) {
6245
6572
  wrappedToolSet[toolName] = {
@@ -6250,7 +6577,7 @@ ${fileList}`
6250
6577
  task.addToolMessage(options.toolCallId, serverName, messageToolName, effectiveInput, void 0, void 0, promptContext);
6251
6578
  const toolCalledExtensionResult = await this.extensionManager.dispatchEvent(
6252
6579
  "onToolCalled",
6253
- { toolName, input: effectiveInput, abortSignal: options.abortSignal || abortSignal },
6580
+ { toolName, agentProfile: profile, input: effectiveInput, abortSignal: options.abortSignal || abortSignal },
6254
6581
  task.project,
6255
6582
  task
6256
6583
  );
@@ -6267,7 +6594,7 @@ ${fileList}`
6267
6594
  }
6268
6595
  const toolFinishedExtensionResult = await this.extensionManager.dispatchEvent(
6269
6596
  "onToolFinished",
6270
- { toolName, input: effectiveInput, output: result },
6597
+ { toolName, agentProfile: profile, input: effectiveInput, output: result },
6271
6598
  task.project,
6272
6599
  task
6273
6600
  );
@@ -6423,7 +6750,7 @@ ${fileList}`
6423
6750
  }
6424
6751
  return inputSchema;
6425
6752
  }
6426
- async runAgent(task, profile, prompt, mode = "agent", promptContext, initialContextMessages, initialContextFiles, systemPrompt, includeInContext = true, abortSignal) {
6753
+ async runAgent(task, profile, prompt, mode = "agent", promptContext, initialContextMessages, initialContextFiles, systemPrompt, includeInContext = true, abortSignal, images) {
6427
6754
  let contextMessages = initialContextMessages ?? await task.getContextMessages();
6428
6755
  let contextFiles = initialContextFiles ?? await task.getContextFiles();
6429
6756
  if (!systemPrompt) {
@@ -6432,9 +6759,32 @@ ${fileList}`
6432
6759
  const userRequestMessage = prompt ? {
6433
6760
  id: promptContext?.id || uuid.v4(),
6434
6761
  role: "user",
6435
- content: prompt,
6762
+ content: images && images.length > 0 ? [
6763
+ { type: "text", text: prompt },
6764
+ ...images.map((dataUrl) => {
6765
+ const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
6766
+ return {
6767
+ type: "image",
6768
+ image: match ? match[2] : dataUrl,
6769
+ mediaType: match ? match[1] : void 0
6770
+ };
6771
+ })
6772
+ ] : prompt,
6436
6773
  promptContext
6437
6774
  } : null;
6775
+ if (userRequestMessage) {
6776
+ logger.info("User request message created:", {
6777
+ id: userRequestMessage.id,
6778
+ contentType: typeof userRequestMessage.content === "string" ? "string" : "parts",
6779
+ ...typeof userRequestMessage.content === "string" ? { contentPreview: userRequestMessage.content.substring(0, 200) } : {
6780
+ parts: userRequestMessage.content.map((part) => ({
6781
+ type: part.type,
6782
+ ...part.type === "text" ? { textPreview: part.text?.substring(0, 100) } : {},
6783
+ ...part.type === "image" ? { mediaType: part.mediaType, imagePreview: typeof part.image === "string" ? part.image.substring(0, 80) : typeof part.image } : {}
6784
+ }))
6785
+ }
6786
+ });
6787
+ }
6438
6788
  const settings = this.store.getSettings();
6439
6789
  const projectProfiles = this.agentProfileManager.getProjectProfiles(task.project);
6440
6790
  let resultMessages = userRequestMessage ? [userRequestMessage] : [];
@@ -6653,7 +7003,7 @@ ${fileList}`
6653
7003
  return null;
6654
7004
  }
6655
7005
  };
6656
- const modelSettings = this.modelManager.getModelSettings(provider.provider.name, modelName);
7006
+ const modelSettings = this.modelManager.getModelSettings(provider.id, modelName);
6657
7007
  const effectiveTemperature = profile.temperature ?? modelSettings?.temperature;
6658
7008
  const effectiveMaxOutputTokens = profile.maxTokens ?? modelSettings?.maxOutputTokens;
6659
7009
  logger.debug("Parameters:", {
@@ -6687,6 +7037,31 @@ ${fileList}`
6687
7037
  return extensionResult2.optimizedMessages ?? optimized;
6688
7038
  };
6689
7039
  const optimizedMessages = await getOptimizedMessages();
7040
+ logger.info("Optimized messages for LLM:", {
7041
+ count: optimizedMessages.length,
7042
+ lastUserMessage: (() => {
7043
+ for (let i = optimizedMessages.length - 1; i >= 0; i--) {
7044
+ if (optimizedMessages[i].role === "user") {
7045
+ const msg = optimizedMessages[i];
7046
+ return {
7047
+ index: i,
7048
+ contentType: typeof msg.content,
7049
+ contentIsArray: Array.isArray(msg.content),
7050
+ contentPreview: typeof msg.content === "string" ? msg.content.substring(0, 200) : Array.isArray(msg.content) ? msg.content.map((p) => ({
7051
+ type: p.type,
7052
+ ...p.type === "text" ? { textPreview: p.text?.substring(0, 100) } : {},
7053
+ ...p.type === "image" ? {
7054
+ mediaType: p.mediaType,
7055
+ imageType: typeof p.image,
7056
+ imagePreview: typeof p.image === "string" ? p.image.substring(0, 80) : void 0
7057
+ } : {}
7058
+ })) : String(msg.content).substring(0, 200)
7059
+ };
7060
+ }
7061
+ }
7062
+ return null;
7063
+ })()
7064
+ });
6690
7065
  return {
6691
7066
  providerOptions,
6692
7067
  model: ai.wrapLanguageModel({
@@ -6713,7 +7088,7 @@ ${fileList}`
6713
7088
  while (true) {
6714
7089
  logger.info(`Starting iteration ${iterationCount}`);
6715
7090
  iterationCount++;
6716
- if (iterationCount > profile.maxIterations) {
7091
+ if (profile.maxIterations > 0 && iterationCount > profile.maxIterations) {
6717
7092
  logger.warn(`Max iterations (${profile.maxIterations}) reached. Stopping agent.`);
6718
7093
  task.addLogMessage(
6719
7094
  "warning",
@@ -6726,7 +7101,7 @@ ${fileList}`
6726
7101
  let iterationError = null;
6727
7102
  let hasReasoning = false;
6728
7103
  let finishReason = null;
6729
- let responseMessages = [];
7104
+ let currentStepMessages = [];
6730
7105
  let responseMessageIndex = 0;
6731
7106
  const onStepFinish = async (stepResult) => {
6732
7107
  finishReason = stepResult.finishReason;
@@ -6738,7 +7113,7 @@ ${fileList}`
6738
7113
  logger.info("Prompt aborted by user");
6739
7114
  return;
6740
7115
  }
6741
- responseMessages = await this.processStep(currentResponseId, stepResult, task, provider, modelName, promptContext, abortSignal);
7116
+ currentStepMessages = await this.processStep(currentResponseId, stepResult, task, provider, modelName, promptContext, abortSignal);
6742
7117
  const extensionResult2 = await this.extensionManager.dispatchEvent(
6743
7118
  "onAgentStepFinished",
6744
7119
  {
@@ -6747,18 +7122,18 @@ ${fileList}`
6747
7122
  currentResponseId,
6748
7123
  stepResult,
6749
7124
  finishReason,
6750
- responseMessages
7125
+ responseMessages: currentStepMessages
6751
7126
  },
6752
7127
  task.project,
6753
7128
  task
6754
7129
  );
6755
- responseMessages = extensionResult2.responseMessages;
7130
+ currentStepMessages = extensionResult2.responseMessages;
6756
7131
  finishReason = extensionResult2.finishReason;
6757
7132
  currentResponseId = uuid.v4();
6758
7133
  responseMessageIndex = 0;
6759
7134
  hasReasoning = false;
6760
7135
  streamingMessageIds.clear();
6761
- if (responseMessages.length > 0) {
7136
+ if (currentStepMessages.length > 0) {
6762
7137
  retryCount = 0;
6763
7138
  }
6764
7139
  };
@@ -6831,25 +7206,46 @@ ${fileList}`
6831
7206
  onStepFinish,
6832
7207
  experimental_repairToolCall: repairToolCall
6833
7208
  });
6834
- for await (const chunk of result.fullStream) {
6835
- logger.debug("Chunk:", { chunk: chunk.type, responseMessageIndex });
6836
- const responseMessageId = responseMessageIndex > 0 ? `${currentResponseId}-${responseMessageIndex}` : currentResponseId;
6837
- if (chunk.type === "text-start") {
6838
- if (hasReasoning) {
7209
+ try {
7210
+ for await (const chunk of result.fullStream) {
7211
+ logger.debug("Chunk:", { chunk: chunk.type, responseMessageIndex });
7212
+ const responseMessageId = responseMessageIndex > 0 ? `${currentResponseId}-${responseMessageIndex}` : currentResponseId;
7213
+ if (chunk.type === "text-start") {
7214
+ if (hasReasoning) {
7215
+ streamingMessageIds.add(responseMessageId);
7216
+ await task.processResponseMessage({
7217
+ id: responseMessageId,
7218
+ action: "response",
7219
+ content: ANSWER_RESPONSE_START_TAG,
7220
+ finished: false,
7221
+ promptContext
7222
+ });
7223
+ hasReasoning = false;
7224
+ }
7225
+ } else if (chunk.type === "text-end") {
7226
+ responseMessageIndex++;
7227
+ } else if (chunk.type === "text-delta") {
7228
+ if (chunk.text.trim()) {
7229
+ streamingMessageIds.add(responseMessageId);
7230
+ await task.processResponseMessage({
7231
+ id: responseMessageId,
7232
+ action: "response",
7233
+ content: chunk.text,
7234
+ finished: false,
7235
+ promptContext
7236
+ });
7237
+ }
7238
+ } else if (chunk.type === "reasoning-start") {
6839
7239
  streamingMessageIds.add(responseMessageId);
6840
7240
  await task.processResponseMessage({
6841
7241
  id: responseMessageId,
6842
7242
  action: "response",
6843
- content: ANSWER_RESPONSE_START_TAG,
7243
+ content: THINKING_RESPONSE_STAR_TAG,
6844
7244
  finished: false,
6845
7245
  promptContext
6846
7246
  });
6847
- hasReasoning = false;
6848
- }
6849
- } else if (chunk.type === "text-end") {
6850
- responseMessageIndex++;
6851
- } else if (chunk.type === "text-delta") {
6852
- if (chunk.text.trim()) {
7247
+ hasReasoning = true;
7248
+ } else if (chunk.type === "reasoning-delta") {
6853
7249
  streamingMessageIds.add(responseMessageId);
6854
7250
  await task.processResponseMessage({
6855
7251
  id: responseMessageId,
@@ -6858,51 +7254,53 @@ ${fileList}`
6858
7254
  finished: false,
6859
7255
  promptContext
6860
7256
  });
7257
+ } else if (chunk.type === "tool-input-start") {
7258
+ task.addLogMessage("loading", "Preparing tool...", false, promptContext);
7259
+ streamingMessageIds.add(chunk.id);
7260
+ } else if (chunk.type === "tool-call") {
7261
+ task.addLogMessage("loading", "Executing tool...", false, promptContext);
7262
+ streamingMessageIds.add(chunk.toolCallId);
7263
+ } else if (chunk.type === "tool-result") {
7264
+ const [serverName, toolName] = extractServerNameToolName(chunk.toolName);
7265
+ const toolPromptContext = extractPromptContextFromToolResult(chunk.output) ?? promptContext;
7266
+ streamingMessageIds.add(chunk.toolCallId);
7267
+ task.addToolMessage(chunk.toolCallId, serverName, toolName, chunk.input, JSON.stringify(chunk.output), void 0, toolPromptContext);
7268
+ task.addLogMessage("loading", void 0, false, promptContext);
6861
7269
  }
6862
- } else if (chunk.type === "reasoning-start") {
6863
- streamingMessageIds.add(responseMessageId);
6864
- await task.processResponseMessage({
6865
- id: responseMessageId,
6866
- action: "response",
6867
- content: THINKING_RESPONSE_STAR_TAG,
6868
- finished: false,
6869
- promptContext
6870
- });
6871
- hasReasoning = true;
6872
- } else if (chunk.type === "reasoning-delta") {
6873
- streamingMessageIds.add(responseMessageId);
6874
- await task.processResponseMessage({
6875
- id: responseMessageId,
6876
- action: "response",
6877
- content: chunk.text,
6878
- finished: false,
6879
- promptContext
6880
- });
6881
- } else if (chunk.type === "tool-input-start") {
6882
- task.addLogMessage("loading", "Preparing tool...", false, promptContext);
6883
- streamingMessageIds.add(chunk.id);
6884
- } else if (chunk.type === "tool-call") {
6885
- task.addLogMessage("loading", "Executing tool...", false, promptContext);
6886
- streamingMessageIds.add(chunk.toolCallId);
6887
- } else if (chunk.type === "tool-result") {
6888
- const [serverName, toolName] = extractServerNameToolName(chunk.toolName);
6889
- const toolPromptContext = extractPromptContextFromToolResult(chunk.output) ?? promptContext;
6890
- streamingMessageIds.add(chunk.toolCallId);
6891
- task.addToolMessage(chunk.toolCallId, serverName, toolName, chunk.input, JSON.stringify(chunk.output), void 0, toolPromptContext);
6892
- task.addLogMessage("loading", void 0, false, promptContext);
7270
+ }
7271
+ } catch (streamError) {
7272
+ if (effectiveAbortSignal?.aborted) {
7273
+ throw streamError;
7274
+ }
7275
+ if (isNetworkError(streamError) && retryCount < MAX_RETRIES) {
7276
+ logger.warn(`Network error during streaming, retrying (${retryCount + 1}/${MAX_RETRIES})...`, { error: streamError });
7277
+ iterationError = streamError;
7278
+ this.removeUnfinishedStreamingMessages(task, streamingMessageIds);
7279
+ task.addLogMessage("loading", "Network error occured. Retrying...");
7280
+ } else if (isNetworkError(streamError)) {
7281
+ const error = streamError;
7282
+ const underlyingMessage = error.cause instanceof Error ? error.cause.message : error.message;
7283
+ throw new Error(`Network error after ${MAX_RETRIES} retries (check your connection): ${underlyingMessage}`, { cause: streamError });
7284
+ } else {
7285
+ throw streamError;
6893
7286
  }
6894
7287
  }
6895
7288
  }
6896
7289
  if (iterationError) {
6897
- logger.error("Error during prompt:", iterationError);
6898
- if (iterationError instanceof ai.APICallError && iterationError.isRetryable && this.modelManager.isRetryable(resolvedProvider, modelName, iterationError)) {
7290
+ logger.error("Error during iteration:", iterationError);
7291
+ if (isNetworkError(iterationError) && retryCount < MAX_RETRIES) {
7292
+ logger.info(`Network error, retrying (${retryCount + 1}/${MAX_RETRIES})...`);
7293
+ this.removeUnfinishedStreamingMessages(task, streamingMessageIds);
7294
+ retryCount++;
7295
+ continue;
7296
+ } else if (iterationError instanceof ai.APICallError && iterationError.isRetryable && this.modelManager.isRetryable(resolvedProvider, modelName, iterationError)) {
6899
7297
  continue;
6900
7298
  } else {
6901
7299
  break;
6902
7300
  }
6903
7301
  }
6904
- const newMessages = this.filterResultMessages(responseMessages);
6905
- messages.push(...responseMessages);
7302
+ const newMessages = this.filterResultMessages(currentStepMessages);
7303
+ messages.push(...currentStepMessages);
6906
7304
  resultMessages.push(...newMessages);
6907
7305
  if (includeInContext) {
6908
7306
  try {
@@ -6928,7 +7326,7 @@ ${fileList}`
6928
7326
  retryCount++;
6929
7327
  continue;
6930
7328
  }
6931
- const lastMessage = responseMessages[responseMessages.length - 1];
7329
+ const lastMessage = currentStepMessages[currentStepMessages.length - 1];
6932
7330
  if (finishReason === "stop" && lastMessage?.role === "tool") {
6933
7331
  logger.debug('Finish reason is "stop" but last message is a tool call. Retrying...');
6934
7332
  retryCount++;
@@ -6990,6 +7388,9 @@ ${fileList}`
6990
7388
  } else {
6991
7389
  task.addLogMessage("error", `${error instanceof Error ? error.message : String(error)}`, false, promptContext);
6992
7390
  }
7391
+ await task.updateTask({
7392
+ state: DefaultTaskState.Interrupted
7393
+ });
6993
7394
  } finally {
6994
7395
  if (controllerId) {
6995
7396
  this.abortControllers.delete(controllerId);
@@ -7286,11 +7687,9 @@ ${fileList}`
7286
7687
  optimizedMessages.unshift({ role: "system", content: systemPrompt });
7287
7688
  const chatMessages = optimizedMessages.map((msg) => ({
7288
7689
  role: msg.role === "tool" ? "user" : msg.role,
7289
- // Map 'tool' role to user message as gpt-tokenizer does not support tool messages
7290
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
7291
- // Handle potential non-string content if necessary
7690
+ content: msg.content
7292
7691
  }));
7293
- return gpt4o.countTokens(chatMessages);
7692
+ return estimateMessageTokens(chatMessages);
7294
7693
  } catch (error) {
7295
7694
  logger.error(`Error counting tokens: ${error}`);
7296
7695
  return 0;
@@ -7398,18 +7797,64 @@ ${fileList}`
7398
7797
  return messages;
7399
7798
  }
7400
7799
  async compactMessagesIfNeeded(task, profile, provider, model, userRequestMessage, contextMessages, contextFiles, messages, resultMessages, promptContext, abortSignal) {
7401
- const contextCompactingThreshold = task.task.contextCompactingThreshold ?? this.store.getProjectSettings(task.getProjectDir())?.contextCompactingThreshold ?? 0;
7402
- const contextCompactionType = profile.isSubagent ? ContextCompactionType.Compact : this.store.getSettings().taskSettings.contextCompactionType ?? ContextCompactionType.Compact;
7800
+ const taskSettings = this.store.getSettings().taskSettings;
7801
+ const thresholdConfig = {
7802
+ percentage: profile.autoCompactThresholdPercentage ?? taskSettings.contextCompactingThreshold.percentage,
7803
+ tokens: profile.autoCompactThresholdTokens ?? taskSettings.contextCompactingThreshold.tokens
7804
+ };
7805
+ const taskTokensOverride = task.task.contextCompactingThresholdTokens;
7806
+ let contextCompactionType = profile.autoCompactionType ?? taskSettings.contextCompactionType ?? ContextCompactionType.Compact;
7807
+ if (profile.isSubagent && contextCompactionType === ContextCompactionType.Handoff) {
7808
+ contextCompactionType = ContextCompactionType.Compact;
7809
+ }
7810
+ logger.debug("Compaction threshold", {
7811
+ thresholdConfig,
7812
+ profile: {
7813
+ autoCompactThresholdPercentage: profile.autoCompactThresholdPercentage,
7814
+ autoCompactThresholdTokens: profile.autoCompactThresholdTokens
7815
+ },
7816
+ taskSettings: {
7817
+ contextCompactingThreshold: taskSettings.contextCompactingThreshold
7818
+ },
7819
+ taskTokensOverride,
7820
+ contextCompactionType
7821
+ });
7403
7822
  const lastAssistantMessage = [...resultMessages].reverse().find((m) => m.role === "assistant");
7404
7823
  const usageReport = lastAssistantMessage?.usageReport;
7405
- const maxTokens = this.modelManager.getModelSettings(provider.provider.name, model)?.maxInputTokens;
7406
- if (contextCompactingThreshold === 0 || !usageReport || !maxTokens) {
7824
+ const maxTokens = this.modelManager.getModelSettings(provider.id, model)?.maxInputTokens;
7825
+ if (!usageReport || !maxTokens) {
7826
+ logger.debug("No usageReport or maxTokens", {
7827
+ usageReport,
7828
+ maxTokens
7829
+ });
7407
7830
  return true;
7408
7831
  }
7409
7832
  const totalTokens = usageReport.sentTokens + usageReport.receivedTokens + (usageReport.cacheReadTokens ?? 0);
7410
- if (maxTokens && totalTokens > maxTokens * contextCompactingThreshold / 100) {
7833
+ let effectiveThreshold;
7834
+ let thresholdDescription;
7835
+ if (taskTokensOverride !== void 0) {
7836
+ if (taskTokensOverride === 0) {
7837
+ return true;
7838
+ }
7839
+ effectiveThreshold = taskTokensOverride;
7840
+ thresholdDescription = `task override: ${taskTokensOverride}`;
7841
+ } else {
7842
+ if (thresholdConfig.percentage === 0) {
7843
+ return true;
7844
+ }
7845
+ const percentageThreshold = maxTokens * thresholdConfig.percentage / 100;
7846
+ const tokenThreshold = thresholdConfig.tokens;
7847
+ effectiveThreshold = Math.min(percentageThreshold, tokenThreshold);
7848
+ thresholdDescription = `percentage: ${percentageThreshold}, tokens: ${tokenThreshold}`;
7849
+ }
7850
+ logger.debug("Checking total tokens vs effective threshold", {
7851
+ totalTokens,
7852
+ effectiveThreshold,
7853
+ thresholdDescription
7854
+ });
7855
+ if (totalTokens > effectiveThreshold) {
7411
7856
  logger.info(
7412
- `Token usage ${totalTokens} exceeds threshold of ${contextCompactingThreshold}%. Compacting conversation with type: ${contextCompactionType}.`
7857
+ `Token usage ${totalTokens} exceeds effective threshold of ${effectiveThreshold} (${thresholdDescription}). Compacting conversation with type: ${contextCompactionType}.`
7413
7858
  );
7414
7859
  if (contextCompactionType === ContextCompactionType.Compact) {
7415
7860
  await task.compactConversation(
@@ -7425,15 +7870,24 @@ ${fileList}`
7425
7870
  messages.length = 0;
7426
7871
  resultMessages.length = 0;
7427
7872
  messages.push(...await this.prepareMessages(task, profile, await task.getContextMessages(), contextFiles));
7873
+ const continuationText = `Based on your compacted summary of our previous conversation, please continue our work with my request:
7874
+
7875
+ ${extractTextContent(userRequestMessage.content)}`;
7876
+ const originalImageParts = Array.isArray(userRequestMessage.content) ? userRequestMessage.content.filter((part) => part.type === "image") : [];
7428
7877
  resultMessages.push({
7429
7878
  id: uuid.v4(),
7430
7879
  role: "user",
7431
- content: `Based on your compacted summary of our previous conversation, please continue our work with my request:
7432
-
7433
- ${userRequestMessage.content}`,
7880
+ content: originalImageParts.length > 0 ? [{ type: "text", text: continuationText }, ...originalImageParts] : continuationText,
7434
7881
  promptContext
7435
7882
  });
7436
7883
  messages.push(...resultMessages);
7884
+ } else if (contextCompactionType === ContextCompactionType.Smart) {
7885
+ const compactedMessages = await task.smartCompactConversation([...contextMessages, ...resultMessages], "Previous conversation has been compacted.");
7886
+ contextMessages.length = 0;
7887
+ messages.length = 0;
7888
+ resultMessages.length = 0;
7889
+ contextMessages.push(...compactedMessages);
7890
+ messages.push(...await this.prepareMessages(task, profile, compactedMessages, contextFiles));
7437
7891
  } else if (contextCompactionType === ContextCompactionType.Handoff) {
7438
7892
  await task.handoffConversation(
7439
7893
  "agent",
@@ -7874,6 +8328,37 @@ class AgentProfileManager {
7874
8328
  }
7875
8329
  return this.profiles.get(profileId)?.agentProfile;
7876
8330
  }
8331
+ resolveAgentProfile(id) {
8332
+ const lowerId = id.toLowerCase();
8333
+ const directMatch = this.getProfile(id);
8334
+ if (directMatch) {
8335
+ return directMatch;
8336
+ }
8337
+ const allProfiles = this.getAllProfiles();
8338
+ for (const profile of allProfiles) {
8339
+ if (profile.name.toLowerCase() === lowerId) {
8340
+ return profile;
8341
+ }
8342
+ }
8343
+ for (const context of this.profiles.values()) {
8344
+ if (context.dirName.toLowerCase() === lowerId) {
8345
+ return context.agentProfile;
8346
+ }
8347
+ }
8348
+ const slugifiedId = deriveDirName(id, /* @__PURE__ */ new Set()).toLowerCase();
8349
+ for (const context of this.profiles.values()) {
8350
+ if (context.dirName.toLowerCase() === slugifiedId) {
8351
+ return context.agentProfile;
8352
+ }
8353
+ }
8354
+ for (const profile of allProfiles) {
8355
+ const derivedName = deriveDirName(profile.name, /* @__PURE__ */ new Set()).toLowerCase();
8356
+ if (derivedName === slugifiedId) {
8357
+ return profile;
8358
+ }
8359
+ }
8360
+ return null;
8361
+ }
7877
8362
  getOrderedProfiles(profileContexts) {
7878
8363
  return profileContexts.sort((a, b) => {
7879
8364
  const aIsProject = !!a.agentProfile.projectDir;
@@ -8213,7 +8698,8 @@ const RunPromptSchema = zod.z.object({
8213
8698
  projectDir: zod.z.string().min(1, "Project directory is required"),
8214
8699
  taskId: zod.z.string().min(1, "Task ID is required"),
8215
8700
  prompt: zod.z.string().min(1, "Prompt is required"),
8216
- mode: zod.z.string().optional()
8701
+ mode: zod.z.string().optional(),
8702
+ images: zod.z.array(zod.z.string()).optional()
8217
8703
  });
8218
8704
  const SavePromptSchema = zod.z.object({
8219
8705
  projectDir: zod.z.string().min(1, "Project directory is required"),
@@ -8233,8 +8719,8 @@ class PromptApi extends BaseApi {
8233
8719
  if (!parsed) {
8234
8720
  return;
8235
8721
  }
8236
- const { projectDir, taskId, prompt, mode } = parsed;
8237
- const responses = await this.eventsHandler.runPrompt(projectDir, taskId, prompt, mode);
8722
+ const { projectDir, taskId, prompt, mode, images } = parsed;
8723
+ const responses = await this.eventsHandler.runPrompt(projectDir, taskId, prompt, mode, images);
8238
8724
  res.status(200).json(responses);
8239
8725
  })
8240
8726
  );
@@ -8422,6 +8908,10 @@ const ClearContextSchema = zod.z.object({
8422
8908
  projectDir: zod.z.string().min(1, "Project directory is required"),
8423
8909
  taskId: zod.z.string().min(1, "Task id is required")
8424
8910
  });
8911
+ const UndoContextChangeSchema = zod.z.object({
8912
+ projectDir: zod.z.string().min(1, "Project directory is required"),
8913
+ taskId: zod.z.string().min(1, "Task id is required")
8914
+ });
8425
8915
  const AnswerQuestionSchema = zod.z.object({
8426
8916
  projectDir: zod.z.string().min(1, "Project directory is required"),
8427
8917
  taskId: zod.z.string().min(1, "Task id is required"),
@@ -8500,7 +8990,8 @@ const RedoUserPromptSchema = zod.z.object({
8500
8990
  taskId: zod.z.string().min(1, "Task id is required"),
8501
8991
  messageId: zod.z.string().min(1, "Message id is required"),
8502
8992
  mode: zod.z.string().min(1, "Mode is required"),
8503
- updatedPrompt: zod.z.string().optional()
8993
+ updatedPrompt: zod.z.string().optional(),
8994
+ updatedImages: zod.z.array(zod.z.string()).optional()
8504
8995
  });
8505
8996
  const ResumeTaskSchema = zod.z.object({
8506
8997
  projectDir: zod.z.string().min(1, "Project directory is required"),
@@ -8607,6 +9098,10 @@ const HandoffConversationSchema = zod.z.object({
8607
9098
  taskId: zod.z.string().min(1, "Task id is required"),
8608
9099
  focus: zod.z.string().optional()
8609
9100
  });
9101
+ const SmartCompactConversationSchema = zod.z.object({
9102
+ projectDir: zod.z.string().min(1, "Project directory is required"),
9103
+ taskId: zod.z.string().min(1, "Task id is required")
9104
+ });
8610
9105
  const ChangeRequestItemSchema = zod.z.object({
8611
9106
  filename: zod.z.string().min(1, "Filename is required"),
8612
9107
  lineNumber: zod.z.number().int().min(1, "Line number is required"),
@@ -8727,8 +9222,8 @@ class ProjectApi extends BaseApi {
8727
9222
  if (!parsed) {
8728
9223
  return;
8729
9224
  }
8730
- const { projectDir, taskId, messageId, mode, updatedPrompt } = parsed;
8731
- await this.eventsHandler.redoUserPrompt(projectDir, taskId, messageId, mode, updatedPrompt);
9225
+ const { projectDir, taskId, messageId, mode, updatedPrompt, updatedImages } = parsed;
9226
+ await this.eventsHandler.redoUserPrompt(projectDir, taskId, messageId, mode, updatedPrompt, updatedImages);
8732
9227
  res.status(200).json({ message: "Redo user prompt initiated" });
8733
9228
  })
8734
9229
  );
@@ -9013,6 +9508,30 @@ class ProjectApi extends BaseApi {
9013
9508
  res.status(200).json({ message: "Conversation compacted" });
9014
9509
  })
9015
9510
  );
9511
+ router.post(
9512
+ "/project/smart-compact-conversation",
9513
+ this.handleRequest(async (req, res) => {
9514
+ const parsed = this.validateRequest(SmartCompactConversationSchema, req.body, res);
9515
+ if (!parsed) {
9516
+ return;
9517
+ }
9518
+ const { projectDir, taskId } = parsed;
9519
+ await this.eventsHandler.smartCompactConversation(projectDir, taskId);
9520
+ res.status(200).json({ message: "Conversation smart-compacted" });
9521
+ })
9522
+ );
9523
+ router.post(
9524
+ "/project/undo-context-change",
9525
+ this.handleRequest(async (req, res) => {
9526
+ const parsed = this.validateRequest(UndoContextChangeSchema, req.body, res);
9527
+ if (!parsed) {
9528
+ return;
9529
+ }
9530
+ const { projectDir, taskId } = parsed;
9531
+ const undone = await this.eventsHandler.undoContextChange(projectDir, taskId);
9532
+ res.status(200).json({ undone });
9533
+ })
9534
+ );
9016
9535
  router.post(
9017
9536
  "/project/handoff-conversation",
9018
9537
  this.handleRequest(async (req, res) => {
@@ -10985,13 +11504,15 @@ class CustomCommandManager {
10985
11504
  const template = parsed.__content?.trim() || "";
10986
11505
  const includeContext = typeof parsed.includeContext === "boolean" ? parsed.includeContext : true;
10987
11506
  const autoApprove = typeof parsed.autoApprove === "boolean" ? parsed.autoApprove : void 0;
11507
+ const skills = typeof parsed.skills === "string" && parsed.skills.trim() ? parsed.skills.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
10988
11508
  commands.set(name, {
10989
11509
  name,
10990
11510
  description: parsed.description || "Not specified",
10991
11511
  arguments: args,
10992
11512
  template,
10993
11513
  includeContext,
10994
- autoApprove
11514
+ autoApprove,
11515
+ skills
10995
11516
  });
10996
11517
  } catch (err) {
10997
11518
  logger.error(`Failed to parse command file ${filePath}: ${err}`);
@@ -11225,10 +11746,20 @@ class ContextManager {
11225
11746
  }
11226
11747
  messages;
11227
11748
  files;
11749
+ undoSnapshot = null;
11228
11750
  loadPromise = null;
11229
11751
  loaded = false;
11230
11752
  autosaveEnabled = false;
11231
11753
  storagePath;
11754
+ hasUndoSnapshot() {
11755
+ return this.undoSnapshot !== null;
11756
+ }
11757
+ undoContextChange() {
11758
+ const snapshot = this.undoSnapshot;
11759
+ this.undoSnapshot = null;
11760
+ this.task.sendContextInfoUpdated();
11761
+ return snapshot;
11762
+ }
11232
11763
  enableAutosave() {
11233
11764
  logger.debug("Enabling autosave for task", { taskId: this.taskId });
11234
11765
  this.autosaveEnabled = true;
@@ -11260,6 +11791,10 @@ class ContextManager {
11260
11791
  }
11261
11792
  this.messages.push(message);
11262
11793
  logger.debug(`Task ${this.taskId}: Added ${message.role} message. Total messages: ${this.messages.length}`);
11794
+ if (this.undoSnapshot !== null) {
11795
+ this.undoSnapshot = null;
11796
+ this.task.sendContextInfoUpdated();
11797
+ }
11263
11798
  this.autosave();
11264
11799
  }
11265
11800
  async isFileIgnored(contextFile) {
@@ -11277,7 +11812,14 @@ class ContextManager {
11277
11812
  });
11278
11813
  return [];
11279
11814
  }
11280
- if (this.isContextFileAlreadyAdded(absolutePath)) {
11815
+ const existingContextFile = this.findExistingContextFile(absolutePath);
11816
+ if (existingContextFile) {
11817
+ const readOnly = contextFile.readOnly ?? false;
11818
+ if (existingContextFile.readOnly !== readOnly) {
11819
+ existingContextFile.readOnly = readOnly;
11820
+ this.autosave();
11821
+ return [existingContextFile];
11822
+ }
11281
11823
  return [];
11282
11824
  }
11283
11825
  const isDir = await isDirectory(absolutePath);
@@ -11336,11 +11878,11 @@ class ContextManager {
11336
11878
  return this.task.resolveContextFilePath(relativePath);
11337
11879
  }
11338
11880
  /**
11339
- * Checks whether a resolved absolute path matches any already-added context file.
11881
+ * Finds an already-added context file matching the given resolved absolute path, if any.
11340
11882
  * Accounts for files that may have been resolved from either taskDir or projectDir.
11341
11883
  */
11342
- isContextFileAlreadyAdded(absolutePath) {
11343
- return this.files.some((file) => {
11884
+ findExistingContextFile(absolutePath) {
11885
+ return this.files.find((file) => {
11344
11886
  const taskDirResolved = path.resolve(this.task.getTaskDir(), file.path);
11345
11887
  const projectDirResolved = path.resolve(this.task.getProjectDir(), file.path);
11346
11888
  return taskDirResolved === absolutePath || projectDirResolved === absolutePath;
@@ -11394,6 +11936,10 @@ class ContextManager {
11394
11936
  messages: contextMessages.length,
11395
11937
  save
11396
11938
  });
11939
+ if (this.messages.length > 0 && this.messages !== contextMessages) {
11940
+ this.undoSnapshot = [...this.messages];
11941
+ this.task.sendContextInfoUpdated();
11942
+ }
11397
11943
  this.messages = contextMessages;
11398
11944
  if (save) {
11399
11945
  this.autosave();
@@ -11423,8 +11969,12 @@ class ContextManager {
11423
11969
  this.autosave();
11424
11970
  }
11425
11971
  }
11426
- clearMessages(save = true) {
11972
+ clearMessages(save = true, createSnapshot = true) {
11427
11973
  logger.debug("Clearing task messages", { taskId: this.taskId });
11974
+ if (createSnapshot && this.messages.length > 0) {
11975
+ this.undoSnapshot = [...this.messages];
11976
+ this.task.sendContextInfoUpdated();
11977
+ }
11428
11978
  this.messages = [];
11429
11979
  if (save) {
11430
11980
  this.autosave();
@@ -11721,6 +12271,34 @@ ${JSON.stringify(part.output.value)}`
11721
12271
  throw error;
11722
12272
  }
11723
12273
  }
12274
+ /**
12275
+ * Creates a backup of the current context.json file before a destructive operation
12276
+ * (e.g., smart compaction). Backups are named context.backup.001.json, context.backup.002.json, etc.
12277
+ * For debugging purposes only.
12278
+ */
12279
+ async backupContext() {
12280
+ try {
12281
+ const dir = path.dirname(this.storagePath);
12282
+ const files = await fs$1.promises.readdir(dir);
12283
+ const backupPattern = /^context\.backup\.(\d+)\.json$/;
12284
+ let maxNumber = 0;
12285
+ for (const file of files) {
12286
+ const match = backupPattern.exec(file);
12287
+ if (match) {
12288
+ const num = parseInt(match[1], 10);
12289
+ if (num > maxNumber) {
12290
+ maxNumber = num;
12291
+ }
12292
+ }
12293
+ }
12294
+ const nextNumber = String(maxNumber + 1).padStart(3, "0");
12295
+ const backupPath = path.join(dir, `context.backup.${nextNumber}.json`);
12296
+ await fs$1.promises.copyFile(this.storagePath, backupPath);
12297
+ logger.debug(`Task context backed up to ${backupPath}`, { taskId: this.taskId });
12298
+ } catch (error) {
12299
+ logger.error("Failed to backup task context:", { error, taskId: this.taskId });
12300
+ }
12301
+ }
11724
12302
  async cleanupContext() {
11725
12303
  const existingFiles = [];
11726
12304
  for (const file of this.files) {
@@ -11794,8 +12372,8 @@ ${JSON.stringify(part.output.value)}`
11794
12372
  this.loaded = true;
11795
12373
  await this.cleanupContext();
11796
12374
  }
11797
- async loadMessages(messages) {
11798
- await this.task.clearContext(false, false);
12375
+ async loadMessages(messages, updateTaskState = true) {
12376
+ await this.task.clearContext(false, false, updateTaskState, false);
11799
12377
  this.messages = messages;
11800
12378
  const messagesData = this.getContextMessagesData(messages);
11801
12379
  for (const messageData of messagesData) {
@@ -12047,12 +12625,18 @@ ${JSON.stringify(part.output.value)}`
12047
12625
  }
12048
12626
  } else if (message.role === "user") {
12049
12627
  const content = extractTextContent(message.content);
12628
+ const images = Array.isArray(message.content) ? message.content.filter((part) => part.type === "image").map((part) => {
12629
+ const data = part.image;
12630
+ const mediaType = part.mediaType || "image/png";
12631
+ return data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
12632
+ }) : void 0;
12050
12633
  const userMessageData = {
12051
12634
  type: "user",
12052
12635
  id: message.id || uuid.v4(),
12053
12636
  baseDir: this.task.getProjectDir(),
12054
12637
  taskId: this.taskId,
12055
12638
  content,
12639
+ images: images && images.length > 0 ? images : void 0,
12056
12640
  promptContext: message.promptContext
12057
12641
  };
12058
12642
  messagesData.push(userMessageData);
@@ -12960,7 +13544,13 @@ class WorktreeManager {
12960
13544
  await execWithShellPath("git add -A", { cwd: projectPath });
12961
13545
  } catch {
12962
13546
  }
12963
- await execWithShellPath('git commit -m "Initial commit" --allow-empty', { cwd: projectPath });
13547
+ try {
13548
+ await execWithShellPath('git commit -m "Initial commit" --allow-empty', { cwd: projectPath });
13549
+ } catch {
13550
+ await execWithShellPath('git -c user.name="AiderDesk" -c user.email="aiderdesk@aiderdesk" commit -m "Initial commit" --allow-empty', {
13551
+ cwd: projectPath
13552
+ });
13553
+ }
12964
13554
  }
12965
13555
  let baseCommit;
12966
13556
  let newBranchName;
@@ -14114,6 +14704,11 @@ Git output: ${err.stderr || err.stdout || err.message}`
14114
14704
  logger.warn(`Failed to get files for commit ${commit.hash}:`, commitError);
14115
14705
  }
14116
14706
  }
14707
+ try {
14708
+ await execWithShellPath("git rev-parse HEAD", { cwd: worktreePath });
14709
+ } catch {
14710
+ return files;
14711
+ }
14117
14712
  try {
14118
14713
  const { stdout: uncommittedNumstat } = await execWithShellPath("git diff --numstat -z HEAD", {
14119
14714
  cwd: worktreePath
@@ -14212,6 +14807,11 @@ Git output: ${err.stderr || err.stdout || err.message}`
14212
14807
  * Non-worktree mode: simple uncommitted changes vs HEAD.
14213
14808
  */
14214
14809
  async getNonWorktreeUpdatedFiles(worktreePath) {
14810
+ try {
14811
+ await execWithShellPath("git rev-parse HEAD", { cwd: worktreePath });
14812
+ } catch {
14813
+ return [];
14814
+ }
14215
14815
  const { stdout } = await execWithShellPath("git diff --numstat -z HEAD", {
14216
14816
  cwd: worktreePath
14217
14817
  });
@@ -14515,102 +15115,691 @@ Git output: ${err.stderr || err.stdout || err.message}`
14515
15115
  }
14516
15116
  throw new Error(`Failed to apply uncommitted changes: ${error instanceof Error ? error.message : String(error)}`);
14517
15117
  }
14518
- });
15118
+ });
15119
+ }
15120
+ /**
15121
+ * Revert a merge operation using the stored MergeState
15122
+ * Restores the main branch to its pre-merge state while preserving uncommitted changes
15123
+ */
15124
+ async revertMerge(projectPath, taskId, worktreePath, mergeState, symlinkFolders = []) {
15125
+ return await withLock(`git-revert-merge-${worktreePath}`, async () => {
15126
+ const timestamp = Date.now();
15127
+ const currentWorktreeStashId = `worktree-${taskId.length > 24 ? taskId.substring(24) : taskId}-revert-${timestamp}`;
15128
+ const currentMainStashId = `main-${taskId.length > 24 ? taskId.substring(24) : taskId}-revert-${timestamp}`;
15129
+ let worktreeRevertStashId = null;
15130
+ let mainRevertStashId = null;
15131
+ try {
15132
+ logger.info("Starting merge revert operation", { mergeState });
15133
+ worktreeRevertStashId = await this.stashUncommittedChanges(
15134
+ currentWorktreeStashId,
15135
+ worktreePath,
15136
+ "Current uncommitted changes before revert",
15137
+ symlinkFolders
15138
+ );
15139
+ mainRevertStashId = await this.stashUncommittedChanges(currentMainStashId, projectPath, "Current uncommitted changes before revert", []);
15140
+ const targetBranch = mergeState.targetBranch || await this.getProjectMainBranch(projectPath);
15141
+ await execWithShellPath(`git checkout ${targetBranch}`, {
15142
+ cwd: projectPath
15143
+ });
15144
+ logger.info(`Resetting ${targetBranch} branch to ${mergeState.beforeMergeCommitHash}`);
15145
+ await execWithShellPath(`git reset --hard ${mergeState.beforeMergeCommitHash}`, { cwd: projectPath });
15146
+ logger.info(`Resetting worktree branch to ${mergeState.worktreeBranchCommitHash}`);
15147
+ await execWithShellPath(`git reset --hard ${mergeState.worktreeBranchCommitHash}`, { cwd: worktreePath });
15148
+ if (mergeState.mainOriginalStashId) {
15149
+ logger.info("Restoring main branch original uncommitted changes");
15150
+ await this.applyStash(projectPath, mergeState.mainOriginalStashId);
15151
+ await this.dropStash(projectPath, mergeState.mainOriginalStashId);
15152
+ }
15153
+ if (worktreeRevertStashId) {
15154
+ logger.info("Restoring uncommitted changes in worktree");
15155
+ await this.applyStash(worktreePath, currentWorktreeStashId);
15156
+ await this.dropStash(worktreePath, currentWorktreeStashId);
15157
+ }
15158
+ if (mainRevertStashId) {
15159
+ await this.dropStash(projectPath, currentMainStashId);
15160
+ }
15161
+ logger.info("Merge revert completed successfully");
15162
+ } catch (error) {
15163
+ logger.error("Failed to revert merge:", error);
15164
+ if (worktreeRevertStashId) {
15165
+ try {
15166
+ await this.applyStash(worktreePath, currentWorktreeStashId);
15167
+ await this.dropStash(worktreePath, currentWorktreeStashId);
15168
+ } catch (recoveryError) {
15169
+ logger.error("Failed to recover worktree stash:", recoveryError);
15170
+ }
15171
+ }
15172
+ if (mainRevertStashId) {
15173
+ try {
15174
+ await this.applyStash(projectPath, currentMainStashId);
15175
+ await this.dropStash(projectPath, currentMainStashId);
15176
+ } catch (recoveryError) {
15177
+ logger.error("Failed to recover main branch stash:", recoveryError);
15178
+ }
15179
+ }
15180
+ throw new Error(`Failed to revert merge: ${error instanceof Error ? error.message : String(error)}`);
15181
+ }
15182
+ });
15183
+ }
15184
+ async pruneDeleted(projectDir) {
15185
+ logger.info("Pruning deleted worktrees", {
15186
+ projectDir
15187
+ });
15188
+ const worktrees = await this.listWorktrees(projectDir);
15189
+ logger.debug("Found worktrees", {
15190
+ worktrees
15191
+ });
15192
+ for (const worktree of worktrees) {
15193
+ if (worktree.path.startsWith(path.join(projectDir, AIDER_DESK_TASKS_DIR)) && worktree.prunable) {
15194
+ try {
15195
+ logger.debug(`Pruning deleted worktree: ${worktree.path}`);
15196
+ await execWithShellPath(`git worktree remove ${worktree.path}`, {
15197
+ cwd: projectDir
15198
+ });
15199
+ } catch (error) {
15200
+ logger.warn("Failed to prune worktree:", {
15201
+ worktree,
15202
+ error: error instanceof Error ? error.message : String(error)
15203
+ });
15204
+ await fs.rm(worktree.path, { recursive: true, force: true });
15205
+ }
15206
+ }
15207
+ }
15208
+ }
15209
+ async close(projectDir) {
15210
+ logger.info("Closing worktree manager");
15211
+ await this.pruneDeleted(projectDir);
15212
+ }
15213
+ }
15214
+ const extractFilePath = (args) => {
15215
+ if (typeof args === "object" && args !== null && "filePath" in args) {
15216
+ return args.filePath;
15217
+ }
15218
+ return void 0;
15219
+ };
15220
+ const getToolOutputText = (output) => {
15221
+ if (!output) {
15222
+ return "";
15223
+ }
15224
+ if (output.type === "text") {
15225
+ return output.value;
15226
+ }
15227
+ if (output.type === "error-text") {
15228
+ return output.value;
15229
+ }
15230
+ return JSON.stringify(output.value);
15231
+ };
15232
+ const isErrorResult = (output) => {
15233
+ const text = getToolOutputText(output).toLowerCase();
15234
+ return text.includes("denied by user") || text.startsWith("error:") || text.includes("no files found") || text.includes("no matches found") || text.includes("operation was cancelled") || text.includes("warning:") || text.includes("already updated - no changes were needed") || text.includes("failed to");
15235
+ };
15236
+ const isNoOpResult = (output) => {
15237
+ const text = getToolOutputText(output);
15238
+ return text.includes("Already updated - no changes were needed");
15239
+ };
15240
+ const isPowerTool = (serverName) => {
15241
+ return serverName === POWER_TOOL_GROUP_NAME;
15242
+ };
15243
+ const getToolInfoFromToolMessage = (message) => {
15244
+ return message.content.filter((part) => part.type === "tool-result").map((part) => {
15245
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15246
+ return {
15247
+ messageIndex: -1,
15248
+ toolCallId: part.toolCallId,
15249
+ toolName,
15250
+ serverName,
15251
+ input: void 0,
15252
+ output: part.output
15253
+ };
15254
+ });
15255
+ };
15256
+ const findAssistantToolCall = (messages, toolCallId) => {
15257
+ for (let i = 0; i < messages.length; i++) {
15258
+ const msg = messages[i];
15259
+ if (msg.role !== "assistant") {
15260
+ continue;
15261
+ }
15262
+ if (!Array.isArray(msg.content)) {
15263
+ continue;
15264
+ }
15265
+ for (let j = 0; j < msg.content.length; j++) {
15266
+ const part = msg.content[j];
15267
+ if (part.type === "tool-call" && part.toolCallId === toolCallId) {
15268
+ const [, toolName] = extractServerNameToolName(part.toolName);
15269
+ return {
15270
+ messageId: msg.id,
15271
+ messageIndex: i,
15272
+ partIndex: j,
15273
+ toolCallId: part.toolCallId,
15274
+ toolName,
15275
+ input: part.input
15276
+ };
15277
+ }
15278
+ }
15279
+ }
15280
+ return void 0;
15281
+ };
15282
+ const cloneMessages = (messages) => {
15283
+ return messages.map((msg) => {
15284
+ if (msg.role === "tool") {
15285
+ return {
15286
+ ...msg,
15287
+ content: msg.content.map((part) => ({ ...part }))
15288
+ };
15289
+ }
15290
+ return {
15291
+ ...msg,
15292
+ content: Array.isArray(msg.content) ? msg.content.map((part) => ({ ...part })) : msg.content
15293
+ };
15294
+ });
15295
+ };
15296
+ const removeToolCallFromAssistant = (messages, toolCallId) => {
15297
+ const callInfo = findAssistantToolCall(messages, toolCallId);
15298
+ if (!callInfo) {
15299
+ return;
15300
+ }
15301
+ const assistantMsg = messages[callInfo.messageIndex];
15302
+ if (!Array.isArray(assistantMsg.content)) {
15303
+ return;
15304
+ }
15305
+ assistantMsg.content.splice(callInfo.partIndex, 1);
15306
+ const hasToolCalls = assistantMsg.content.some((p) => p.type === "tool-call");
15307
+ if (!hasToolCalls) {
15308
+ messages.splice(callInfo.messageIndex, 1);
15309
+ }
15310
+ };
15311
+ const removeToolResult = (messages, toolCallId) => {
15312
+ const toolMsgIdx = messages.findIndex((m) => m.role === "tool" && m.content.some((p) => p.type === "tool-result" && p.toolCallId === toolCallId));
15313
+ if (toolMsgIdx === -1) {
15314
+ return;
15315
+ }
15316
+ const toolMsg = messages[toolMsgIdx];
15317
+ const partIdx = toolMsg.content.findIndex((p) => p.type === "tool-result" && p.toolCallId === toolCallId);
15318
+ if (partIdx === -1) {
15319
+ return;
15320
+ }
15321
+ toolMsg.content.splice(partIdx, 1);
15322
+ if (toolMsg.content.length === 0) {
15323
+ messages.splice(toolMsgIdx, 1);
15324
+ }
15325
+ };
15326
+ const getProtectedStartIndex = (messages, protectedMessageCount) => {
15327
+ return Math.max(0, messages.length - protectedMessageCount);
15328
+ };
15329
+ const mergeConsecutiveAssistantMessages = (messages) => {
15330
+ const result = cloneMessages(messages);
15331
+ for (let i = result.length - 2; i >= 0; i--) {
15332
+ const current = result[i];
15333
+ const next = result[i + 1];
15334
+ if (current.role !== "assistant" || next.role !== "assistant") {
15335
+ continue;
15336
+ }
15337
+ const currentHasToolCalls = Array.isArray(current.content) && current.content.some((p) => p.type === "tool-call");
15338
+ const nextHasToolCalls = Array.isArray(next.content) && next.content.some((p) => p.type === "tool-call");
15339
+ if (currentHasToolCalls || nextHasToolCalls) {
15340
+ continue;
15341
+ }
15342
+ const currentTextParts = Array.isArray(current.content) ? current.content.filter((p) => p.type === "text") : [];
15343
+ const nextTextParts = Array.isArray(next.content) ? next.content.filter((p) => p.type === "text") : [];
15344
+ const mergedText = [...currentTextParts, ...nextTextParts].map((p) => p.text).filter(Boolean).join("\n\n");
15345
+ current.content = mergedText ? [{ type: "text", text: mergedText }] : [];
15346
+ result.splice(i + 1, 1);
15347
+ }
15348
+ return result.filter((msg) => {
15349
+ if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
15350
+ return false;
15351
+ }
15352
+ return true;
15353
+ });
15354
+ };
15355
+ const smartCompactMessages = (messages, protectedMessageCount = 10) => {
15356
+ let result = cloneMessages(messages);
15357
+ result = removeErroredTools(result, protectedMessageCount);
15358
+ result = collapseFileEdits(result, protectedMessageCount);
15359
+ result = removeStaleFileReads(result, protectedMessageCount);
15360
+ result = removeObsoleteSearches(result, protectedMessageCount);
15361
+ result = compactSemanticSearches(result, protectedMessageCount);
15362
+ result = deduplicateBash(result, protectedMessageCount);
15363
+ result = redactFetchOutputs(result, protectedMessageCount);
15364
+ result = mergeConsecutiveAssistantMessages(result);
15365
+ return result;
15366
+ };
15367
+ const removeErroredTools = (messages, protectedMessageCount = 10) => {
15368
+ const result = cloneMessages(messages);
15369
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15370
+ for (let i = protectedStart - 1; i >= 0; i--) {
15371
+ if (i >= result.length) {
15372
+ continue;
15373
+ }
15374
+ const msg = result[i];
15375
+ if (msg.role !== "tool") {
15376
+ continue;
15377
+ }
15378
+ const toolInfos = getToolInfoFromToolMessage(msg);
15379
+ for (const info of toolInfos) {
15380
+ if (!isPowerTool(info.serverName)) {
15381
+ continue;
15382
+ }
15383
+ if (isErrorResult(info.output) || isNoOpResult(info.output)) {
15384
+ removeToolCallFromAssistant(result, info.toolCallId);
15385
+ removeToolResult(result, info.toolCallId);
15386
+ }
15387
+ }
15388
+ }
15389
+ return result;
15390
+ };
15391
+ const collapseFileEdits = (messages, protectedMessageCount = 10) => {
15392
+ const result = cloneMessages(messages);
15393
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15394
+ const fileEditGroups = /* @__PURE__ */ new Map();
15395
+ for (let i = 0; i < protectedStart; i++) {
15396
+ if (i >= result.length) {
15397
+ break;
15398
+ }
15399
+ const msg = result[i];
15400
+ if (msg.role !== "tool") {
15401
+ continue;
15402
+ }
15403
+ for (const part of msg.content) {
15404
+ if (part.type !== "tool-result") {
15405
+ continue;
15406
+ }
15407
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15408
+ if (!isPowerTool(serverName)) {
15409
+ continue;
15410
+ }
15411
+ if (toolName !== POWER_TOOL_FILE_EDIT && toolName !== POWER_TOOL_FILE_WRITE) {
15412
+ continue;
15413
+ }
15414
+ const callInfo = findAssistantToolCall(result, part.toolCallId);
15415
+ if (!callInfo) {
15416
+ continue;
15417
+ }
15418
+ const filePath = extractFilePath(callInfo.input);
15419
+ if (!filePath) {
15420
+ continue;
15421
+ }
15422
+ if (!fileEditGroups.has(filePath)) {
15423
+ fileEditGroups.set(filePath, []);
15424
+ }
15425
+ fileEditGroups.get(filePath).push({
15426
+ assistantMessageId: callInfo.messageId,
15427
+ toolCallId: part.toolCallId
15428
+ });
15429
+ }
15430
+ }
15431
+ for (const [filePath, edits] of fileEditGroups) {
15432
+ if (edits.length === 0) {
15433
+ continue;
15434
+ }
15435
+ const lastEdit = edits[edits.length - 1];
15436
+ const syntheticMessage = {
15437
+ id: uuid.v4(),
15438
+ role: "assistant",
15439
+ content: [
15440
+ {
15441
+ type: "text",
15442
+ text: `<file-edited path="${filePath}">File was edited. Read the content again if you need to work on it.</file-edited>`
15443
+ }
15444
+ ]
15445
+ };
15446
+ const assistantIndex = result.findIndex((m) => m.id === lastEdit.assistantMessageId);
15447
+ const insertIndex = assistantIndex !== -1 ? assistantIndex + 1 : 0;
15448
+ result.splice(insertIndex, 0, syntheticMessage);
15449
+ for (const edit of edits) {
15450
+ removeToolCallFromAssistant(result, edit.toolCallId);
15451
+ removeToolResult(result, edit.toolCallId);
15452
+ }
15453
+ }
15454
+ return result;
15455
+ };
15456
+ const removeStaleFileReads = (messages, protectedMessageCount = 10) => {
15457
+ const result = cloneMessages(messages);
15458
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15459
+ const editedFilePaths = /* @__PURE__ */ new Set();
15460
+ for (let i = 0; i < result.length; i++) {
15461
+ const msg = result[i];
15462
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
15463
+ for (const part of msg.content) {
15464
+ if (part.type === "text") {
15465
+ const match = part.text.match(/<file-edited path="([^"]+)">/);
15466
+ if (match) {
15467
+ editedFilePaths.add(match[1]);
15468
+ }
15469
+ }
15470
+ }
15471
+ continue;
15472
+ }
15473
+ if (msg.role !== "tool") {
15474
+ continue;
15475
+ }
15476
+ for (const part of msg.content) {
15477
+ if (part.type !== "tool-result") {
15478
+ continue;
15479
+ }
15480
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15481
+ if (!isPowerTool(serverName)) {
15482
+ continue;
15483
+ }
15484
+ if (toolName !== POWER_TOOL_FILE_EDIT && toolName !== POWER_TOOL_FILE_WRITE) {
15485
+ continue;
15486
+ }
15487
+ const callInfo = findAssistantToolCall(result, part.toolCallId);
15488
+ if (callInfo) {
15489
+ const filePath = extractFilePath(callInfo.input);
15490
+ if (filePath) {
15491
+ editedFilePaths.add(filePath);
15492
+ }
15493
+ }
15494
+ }
15495
+ }
15496
+ const protectedReadFilePaths = /* @__PURE__ */ new Set();
15497
+ for (let i = protectedStart; i < result.length; i++) {
15498
+ const msg = result[i];
15499
+ if (msg.role !== "tool") {
15500
+ continue;
15501
+ }
15502
+ for (const part of msg.content) {
15503
+ if (part.type !== "tool-result") {
15504
+ continue;
15505
+ }
15506
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15507
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_FILE_READ) {
15508
+ continue;
15509
+ }
15510
+ const callInfo = findAssistantToolCall(result, part.toolCallId);
15511
+ if (callInfo) {
15512
+ const filePath = extractFilePath(callInfo.input);
15513
+ if (filePath) {
15514
+ protectedReadFilePaths.add(filePath);
15515
+ }
15516
+ }
15517
+ }
15518
+ }
15519
+ const readFileGroups = /* @__PURE__ */ new Map();
15520
+ for (let i = protectedStart - 1; i >= 0; i--) {
15521
+ if (i >= result.length) {
15522
+ continue;
15523
+ }
15524
+ const msg = result[i];
15525
+ if (msg.role !== "tool") {
15526
+ continue;
15527
+ }
15528
+ for (const part of msg.content) {
15529
+ if (part.type !== "tool-result") {
15530
+ continue;
15531
+ }
15532
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15533
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_FILE_READ) {
15534
+ continue;
15535
+ }
15536
+ const callInfo = findAssistantToolCall(result, part.toolCallId);
15537
+ if (!callInfo) {
15538
+ continue;
15539
+ }
15540
+ const filePath = extractFilePath(callInfo.input);
15541
+ if (!filePath) {
15542
+ continue;
15543
+ }
15544
+ if (editedFilePaths.has(filePath) || protectedReadFilePaths.has(filePath)) {
15545
+ removeToolCallFromAssistant(result, part.toolCallId);
15546
+ removeToolResult(result, part.toolCallId);
15547
+ continue;
15548
+ }
15549
+ if (!readFileGroups.has(filePath)) {
15550
+ readFileGroups.set(filePath, []);
15551
+ }
15552
+ readFileGroups.get(filePath).push({
15553
+ messageIndex: i,
15554
+ toolCallId: part.toolCallId
15555
+ });
15556
+ }
15557
+ }
15558
+ for (const [, reads] of readFileGroups) {
15559
+ if (reads.length <= 1) {
15560
+ continue;
15561
+ }
15562
+ const toRemove = reads.slice(0, -1);
15563
+ for (const read of toRemove) {
15564
+ removeToolCallFromAssistant(result, read.toolCallId);
15565
+ removeToolResult(result, read.toolCallId);
15566
+ }
15567
+ }
15568
+ return result;
15569
+ };
15570
+ const removeObsoleteSearches = (messages, protectedMessageCount = 10) => {
15571
+ const result = cloneMessages(messages);
15572
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15573
+ const fileModificationPositions = [];
15574
+ for (let i = 0; i < result.length; i++) {
15575
+ const msg = result[i];
15576
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
15577
+ for (const part of msg.content) {
15578
+ if (part.type === "text" && part.text.includes("<file-edited")) {
15579
+ fileModificationPositions.push(i);
15580
+ break;
15581
+ }
15582
+ }
15583
+ continue;
15584
+ }
15585
+ if (msg.role !== "tool") {
15586
+ continue;
15587
+ }
15588
+ for (const part of msg.content) {
15589
+ if (part.type !== "tool-result") {
15590
+ continue;
15591
+ }
15592
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15593
+ if (!isPowerTool(serverName)) {
15594
+ continue;
15595
+ }
15596
+ if (toolName === POWER_TOOL_FILE_EDIT || toolName === POWER_TOOL_FILE_WRITE) {
15597
+ fileModificationPositions.push(i);
15598
+ }
15599
+ }
15600
+ }
15601
+ const hasFileModifications = fileModificationPositions.length > 0;
15602
+ if (!hasFileModifications) {
15603
+ return result;
15604
+ }
15605
+ for (let i = protectedStart - 1; i >= 0; i--) {
15606
+ if (i >= result.length) {
15607
+ continue;
15608
+ }
15609
+ const msg = result[i];
15610
+ if (msg.role !== "tool") {
15611
+ continue;
15612
+ }
15613
+ for (const part of msg.content) {
15614
+ if (part.type !== "tool-result") {
15615
+ continue;
15616
+ }
15617
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15618
+ if (!isPowerTool(serverName)) {
15619
+ continue;
15620
+ }
15621
+ if (toolName !== POWER_TOOL_GLOB && toolName !== POWER_TOOL_GREP) {
15622
+ continue;
15623
+ }
15624
+ const hasLaterModification = fileModificationPositions.some((pos) => pos > i);
15625
+ if (hasLaterModification) {
15626
+ removeToolCallFromAssistant(result, part.toolCallId);
15627
+ removeToolResult(result, part.toolCallId);
15628
+ }
15629
+ }
14519
15630
  }
14520
- /**
14521
- * Revert a merge operation using the stored MergeState
14522
- * Restores the main branch to its pre-merge state while preserving uncommitted changes
14523
- */
14524
- async revertMerge(projectPath, taskId, worktreePath, mergeState, symlinkFolders = []) {
14525
- return await withLock(`git-revert-merge-${worktreePath}`, async () => {
14526
- const timestamp = Date.now();
14527
- const currentWorktreeStashId = `worktree-${taskId.length > 24 ? taskId.substring(24) : taskId}-revert-${timestamp}`;
14528
- const currentMainStashId = `main-${taskId.length > 24 ? taskId.substring(24) : taskId}-revert-${timestamp}`;
14529
- let worktreeRevertStashId = null;
14530
- let mainRevertStashId = null;
15631
+ return result;
15632
+ };
15633
+ const compactSemanticSearches = (messages, protectedMessageCount = 10) => {
15634
+ const result = cloneMessages(messages);
15635
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15636
+ const searchIndices = [];
15637
+ for (let i = 0; i < protectedStart; i++) {
15638
+ if (i >= result.length) {
15639
+ break;
15640
+ }
15641
+ const msg = result[i];
15642
+ if (msg.role !== "tool") {
15643
+ continue;
15644
+ }
15645
+ for (let j = 0; j < msg.content.length; j++) {
15646
+ const part = msg.content[j];
15647
+ if (part.type !== "tool-result") {
15648
+ continue;
15649
+ }
15650
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15651
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_SEMANTIC_SEARCH) {
15652
+ continue;
15653
+ }
15654
+ searchIndices.push({ messageIndex: i, partIndex: j, toolCallId: part.toolCallId });
15655
+ }
15656
+ }
15657
+ if (searchIndices.length <= 1) {
15658
+ return result;
15659
+ }
15660
+ const toRemove = searchIndices.slice(0, -1);
15661
+ const toKeep = searchIndices[searchIndices.length - 1];
15662
+ for (const search2 of toRemove) {
15663
+ removeToolCallFromAssistant(result, search2.toolCallId);
15664
+ removeToolResult(result, search2.toolCallId);
15665
+ }
15666
+ const keptToolIdx = result.findIndex((m) => m.role === "tool" && m.content.some((p) => p.type === "tool-result" && p.toolCallId === toKeep.toolCallId));
15667
+ if (keptToolIdx !== -1) {
15668
+ const keptMsg = result[keptToolIdx];
15669
+ const keptPartIdx = keptMsg.content.findIndex((p) => p.type === "tool-result" && p.toolCallId === toKeep.toolCallId);
15670
+ const keptPart = keptMsg.content[keptPartIdx];
15671
+ if (keptPart && keptPart.output.type === "text") {
15672
+ const lines = keptPart.output.value.split("\n");
15673
+ if (lines.length > 50) {
15674
+ keptPart.output = {
15675
+ type: "text",
15676
+ value: lines.slice(0, 50).join("\n") + "\n<truncated due to compaction, run again if full output is needed>"
15677
+ };
15678
+ }
15679
+ }
15680
+ }
15681
+ return result;
15682
+ };
15683
+ const deduplicateBash = (messages, protectedMessageCount = 10) => {
15684
+ const result = cloneMessages(messages);
15685
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15686
+ const bashCommands = /* @__PURE__ */ new Map();
15687
+ for (let i = 0; i < protectedStart; i++) {
15688
+ const msg = result[i];
15689
+ if (msg.role !== "tool") {
15690
+ continue;
15691
+ }
15692
+ for (const part of msg.content) {
15693
+ if (part.type !== "tool-result") {
15694
+ continue;
15695
+ }
15696
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15697
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_BASH) {
15698
+ continue;
15699
+ }
15700
+ const callInfo = findAssistantToolCall(result, part.toolCallId);
15701
+ if (!callInfo) {
15702
+ continue;
15703
+ }
15704
+ const command = typeof callInfo.input?.command === "string" ? callInfo.input.command.trim() : null;
15705
+ if (!command) {
15706
+ continue;
15707
+ }
15708
+ if (!bashCommands.has(command)) {
15709
+ bashCommands.set(command, []);
15710
+ }
15711
+ bashCommands.get(command).push({
15712
+ messageIndex: i,
15713
+ toolCallId: part.toolCallId
15714
+ });
15715
+ }
15716
+ }
15717
+ for (const [, occurrences] of bashCommands) {
15718
+ if (occurrences.length <= 1) {
15719
+ continue;
15720
+ }
15721
+ const toRemove = occurrences.slice(0, -1);
15722
+ for (const occ of toRemove) {
15723
+ removeToolCallFromAssistant(result, occ.toolCallId);
15724
+ removeToolResult(result, occ.toolCallId);
15725
+ }
15726
+ }
15727
+ for (let i = 0; i < protectedStart; i++) {
15728
+ if (i >= result.length) {
15729
+ break;
15730
+ }
15731
+ const msg = result[i];
15732
+ if (msg.role !== "tool") {
15733
+ continue;
15734
+ }
15735
+ for (let j = 0; j < msg.content.length; j++) {
15736
+ const part = msg.content[j];
15737
+ if (part.type !== "tool-result") {
15738
+ continue;
15739
+ }
15740
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15741
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_BASH) {
15742
+ continue;
15743
+ }
15744
+ if (part.output.type !== "text") {
15745
+ continue;
15746
+ }
14531
15747
  try {
14532
- logger.info("Starting merge revert operation", { mergeState });
14533
- worktreeRevertStashId = await this.stashUncommittedChanges(
14534
- currentWorktreeStashId,
14535
- worktreePath,
14536
- "Current uncommitted changes before revert",
14537
- symlinkFolders
14538
- );
14539
- mainRevertStashId = await this.stashUncommittedChanges(currentMainStashId, projectPath, "Current uncommitted changes before revert", []);
14540
- const targetBranch = mergeState.targetBranch || await this.getProjectMainBranch(projectPath);
14541
- await execWithShellPath(`git checkout ${targetBranch}`, {
14542
- cwd: projectPath
14543
- });
14544
- logger.info(`Resetting ${targetBranch} branch to ${mergeState.beforeMergeCommitHash}`);
14545
- await execWithShellPath(`git reset --hard ${mergeState.beforeMergeCommitHash}`, { cwd: projectPath });
14546
- logger.info(`Resetting worktree branch to ${mergeState.worktreeBranchCommitHash}`);
14547
- await execWithShellPath(`git reset --hard ${mergeState.worktreeBranchCommitHash}`, { cwd: worktreePath });
14548
- if (mergeState.mainOriginalStashId) {
14549
- logger.info("Restoring main branch original uncommitted changes");
14550
- await this.applyStash(projectPath, mergeState.mainOriginalStashId);
14551
- await this.dropStash(projectPath, mergeState.mainOriginalStashId);
14552
- }
14553
- if (worktreeRevertStashId) {
14554
- logger.info("Restoring uncommitted changes in worktree");
14555
- await this.applyStash(worktreePath, currentWorktreeStashId);
14556
- await this.dropStash(worktreePath, currentWorktreeStashId);
14557
- }
14558
- if (mainRevertStashId) {
14559
- await this.dropStash(projectPath, currentMainStashId);
14560
- }
14561
- logger.info("Merge revert completed successfully");
14562
- } catch (error) {
14563
- logger.error("Failed to revert merge:", error);
14564
- if (worktreeRevertStashId) {
14565
- try {
14566
- await this.applyStash(worktreePath, currentWorktreeStashId);
14567
- await this.dropStash(worktreePath, currentWorktreeStashId);
14568
- } catch (recoveryError) {
14569
- logger.error("Failed to recover worktree stash:", recoveryError);
15748
+ const parsed = JSON.parse(part.output.value);
15749
+ if (typeof parsed === "object" && parsed !== null) {
15750
+ const stdout = typeof parsed.stdout === "string" ? parsed.stdout : "";
15751
+ const stderr = typeof parsed.stderr === "string" ? parsed.stderr : "";
15752
+ const redactionMessage = "<output redacted due to compaction, run again if output is needed>";
15753
+ let modified = false;
15754
+ if (stdout.length > 30) {
15755
+ parsed.stdout = redactionMessage;
15756
+ modified = true;
14570
15757
  }
14571
- }
14572
- if (mainRevertStashId) {
14573
- try {
14574
- await this.applyStash(projectPath, currentMainStashId);
14575
- await this.dropStash(projectPath, currentMainStashId);
14576
- } catch (recoveryError) {
14577
- logger.error("Failed to recover main branch stash:", recoveryError);
15758
+ if (stderr.length > 30) {
15759
+ parsed.stderr = redactionMessage;
15760
+ modified = true;
15761
+ }
15762
+ if (modified) {
15763
+ part.output = {
15764
+ type: "text",
15765
+ value: JSON.stringify(parsed)
15766
+ };
14578
15767
  }
14579
15768
  }
14580
- throw new Error(`Failed to revert merge: ${error instanceof Error ? error.message : String(error)}`);
15769
+ } catch {
14581
15770
  }
14582
- });
15771
+ }
14583
15772
  }
14584
- async pruneDeleted(projectDir) {
14585
- logger.info("Pruning deleted worktrees", {
14586
- projectDir
14587
- });
14588
- const worktrees = await this.listWorktrees(projectDir);
14589
- logger.debug("Found worktrees", {
14590
- worktrees
14591
- });
14592
- for (const worktree of worktrees) {
14593
- if (worktree.path.startsWith(path.join(projectDir, AIDER_DESK_TASKS_DIR)) && worktree.prunable) {
14594
- try {
14595
- logger.debug(`Pruning deleted worktree: ${worktree.path}`);
14596
- await execWithShellPath(`git worktree remove ${worktree.path}`, {
14597
- cwd: projectDir
14598
- });
14599
- } catch (error) {
14600
- logger.warn("Failed to prune worktree:", {
14601
- worktree,
14602
- error: error instanceof Error ? error.message : String(error)
14603
- });
14604
- await fs.rm(worktree.path, { recursive: true, force: true });
14605
- }
15773
+ return result;
15774
+ };
15775
+ const redactFetchOutputs = (messages, protectedMessageCount = 10) => {
15776
+ const result = cloneMessages(messages);
15777
+ const protectedStart = getProtectedStartIndex(result, protectedMessageCount);
15778
+ for (let i = 0; i < protectedStart; i++) {
15779
+ const msg = result[i];
15780
+ if (msg.role !== "tool") {
15781
+ continue;
15782
+ }
15783
+ for (let j = 0; j < msg.content.length; j++) {
15784
+ const part = msg.content[j];
15785
+ if (part.type !== "tool-result") {
15786
+ continue;
15787
+ }
15788
+ const [serverName, toolName] = extractServerNameToolName(part.toolName);
15789
+ if (!isPowerTool(serverName) || toolName !== POWER_TOOL_FETCH) {
15790
+ continue;
15791
+ }
15792
+ const outputText = getToolOutputText(part.output);
15793
+ if (outputText.length > 0) {
15794
+ part.output = {
15795
+ type: "text",
15796
+ value: "<content redacted due to compaction, fetch again if content is needed>"
15797
+ };
14606
15798
  }
14607
15799
  }
14608
15800
  }
14609
- async close(projectDir) {
14610
- logger.info("Closing worktree manager");
14611
- await this.pruneDeleted(projectDir);
14612
- }
14613
- }
15801
+ return result;
15802
+ };
14614
15803
  const INTERNAL_TASK_ID = "internal";
14615
15804
  const RESPONSE_CHUNK_FLUSH_INTERVAL_MS = 10;
14616
15805
  const EMPTY_TASK_DATA = {
@@ -14622,7 +15811,6 @@ const EMPTY_TASK_DATA = {
14622
15811
  agentTotalCost: 0,
14623
15812
  mainModel: "",
14624
15813
  currentMode: "agent",
14625
- contextCompactingThreshold: 0,
14626
15814
  weakModelLocked: false,
14627
15815
  parentId: null,
14628
15816
  lastAgentProviderMetadata: void 0
@@ -15117,7 +16305,7 @@ class Task {
15117
16305
  isPromptRunning() {
15118
16306
  return !!this.currentPromptContext || this.agent.isRunning() || this.isCompacting;
15119
16307
  }
15120
- async runPrompt(prompt, mode = this.task.currentMode || "agent", addToInputHistory = true, userMessageId = uuid.v4(), sendNotification = true) {
16308
+ async runPrompt(prompt, mode = this.task.currentMode || "agent", addToInputHistory = true, userMessageId = uuid.v4(), sendNotification = true, images) {
15121
16309
  if (this.currentQuestion) {
15122
16310
  if (await this.answerQuestion("n", prompt)) {
15123
16311
  logger.debug("Processed by the answerQuestion function.");
@@ -15155,7 +16343,7 @@ class Task {
15155
16343
  if (addToInputHistory) {
15156
16344
  await this.project.addToInputHistory(prompt);
15157
16345
  }
15158
- this.addUserMessage(userMessageId, prompt);
16346
+ this.addUserMessage(userMessageId, prompt, promptContext, images);
15159
16347
  this.addLogMessage("loading");
15160
16348
  this.telemetryManager.captureRunPrompt(mode);
15161
16349
  let responses = [];
@@ -15165,7 +16353,7 @@ class Task {
15165
16353
  if (!profile) {
15166
16354
  throw new Error("No active Agent profile found");
15167
16355
  }
15168
- responses = await this.runPromptInAgent(profile, mode, prompt, promptContext, void 0, void 0, void 0, true, sendNotification);
16356
+ responses = await this.runPromptInAgent(profile, mode, prompt, promptContext, void 0, void 0, void 0, true, sendNotification, images);
15169
16357
  } else {
15170
16358
  responses = await this.runPromptInAider(mode, prompt, promptContext, sendNotification);
15171
16359
  }
@@ -15277,7 +16465,7 @@ class Task {
15277
16465
  }
15278
16466
  return responses;
15279
16467
  }
15280
- async runPromptInAgent(profile, mode, prompt, promptContext = { id: uuid.v4() }, contextMessages, contextFiles, systemPrompt, waitForCurrentAgentToFinish = true, sendNotification = true) {
16468
+ async runPromptInAgent(profile, mode, prompt, promptContext = { id: uuid.v4() }, contextMessages, contextFiles, systemPrompt, waitForCurrentAgentToFinish = true, sendNotification = true, images) {
15281
16469
  if (waitForCurrentAgentToFinish) {
15282
16470
  await this.waitForCurrentAgentToFinish();
15283
16471
  }
@@ -15288,7 +16476,19 @@ class Task {
15288
16476
  provider: this.task.provider || profile.provider,
15289
16477
  model: this.task.model || profile.model
15290
16478
  });
15291
- const agentMessages = await this.agent.runAgent(this, profile, prompt, mode, promptContext, contextMessages, contextFiles, systemPrompt);
16479
+ const agentMessages = await this.agent.runAgent(
16480
+ this,
16481
+ profile,
16482
+ prompt,
16483
+ mode,
16484
+ promptContext,
16485
+ contextMessages,
16486
+ contextFiles,
16487
+ systemPrompt,
16488
+ true,
16489
+ void 0,
16490
+ images
16491
+ );
15292
16492
  if (agentMessages.length > 0) {
15293
16493
  this.contextManager.toConnectorMessages(agentMessages).forEach((message) => {
15294
16494
  this.sendAddMessage(message.role, message.content, false);
@@ -16325,7 +17525,8 @@ ${contentText}</agent-response>`;
16325
17525
  sendUserMessage(data) {
16326
17526
  logger.debug("Sending user message:", {
16327
17527
  baseDir: this.project.baseDir,
16328
- content: data.content.substring(0, 100)
17528
+ content: data.content?.substring(0, 100),
17529
+ hasImages: (data.images?.length ?? 0) > 0
16329
17530
  });
16330
17531
  this.eventManager.sendUserMessage(data);
16331
17532
  }
@@ -16342,18 +17543,20 @@ ${contentText}</agent-response>`;
16342
17543
  });
16343
17544
  this.eventManager.sendTool(data);
16344
17545
  }
16345
- async clearContext(addToHistory = false, updateContextInfo = true) {
17546
+ async clearContext(addToHistory = false, updateContextInfo = true, updateTaskState = true, createSnapshot = true) {
16346
17547
  logger.info("Clearing context:", {
16347
17548
  baseDir: this.project.baseDir,
16348
17549
  addToHistory,
16349
17550
  updateContextInfo
16350
17551
  });
16351
- await this.updateTask({
16352
- state: DefaultTaskState.Todo,
16353
- metadata: void 0,
16354
- lastAgentProviderMetadata: null
16355
- });
16356
- this.contextManager.clearMessages();
17552
+ if (updateTaskState) {
17553
+ await this.updateTask({
17554
+ state: DefaultTaskState.Todo,
17555
+ metadata: void 0,
17556
+ lastAgentProviderMetadata: null
17557
+ });
17558
+ }
17559
+ this.contextManager.clearMessages(true, createSnapshot);
16357
17560
  await this.runCommand("clear", addToHistory);
16358
17561
  this.eventManager.sendClearTask(this.project.baseDir, this.taskId, true, false);
16359
17562
  if (updateContextInfo) {
@@ -16369,6 +17572,29 @@ ${contentText}</agent-response>`;
16369
17572
  this.contextManager.clearMessages();
16370
17573
  await this.contextManager.save();
16371
17574
  }
17575
+ sendContextInfoUpdated() {
17576
+ this.eventManager.sendContextInfoUpdated({
17577
+ baseDir: this.project.baseDir,
17578
+ taskId: this.taskId,
17579
+ canUndoContextChange: this.contextManager.hasUndoSnapshot()
17580
+ });
17581
+ }
17582
+ async undoContextChange() {
17583
+ const snapshot = this.contextManager.undoContextChange();
17584
+ if (!snapshot) {
17585
+ logger.debug("No undo snapshot available", { taskId: this.taskId });
17586
+ return false;
17587
+ }
17588
+ logger.info("Undoing context change", {
17589
+ baseDir: this.project.baseDir,
17590
+ taskId: this.taskId,
17591
+ messagesCount: snapshot.length
17592
+ });
17593
+ await this.contextManager.loadMessages(snapshot);
17594
+ await this.updateContextInfo();
17595
+ this.sendContextInfoUpdated();
17596
+ return true;
17597
+ }
16372
17598
  /**
16373
17599
  * Load context messages into the task context and send them to the UI.
16374
17600
  * This is used for loading pre-authored context (e.g., from extensions).
@@ -16382,6 +17608,12 @@ ${contentText}</agent-response>`;
16382
17608
  await this.contextManager.loadMessages(messages);
16383
17609
  }
16384
17610
  async interruptResponse(interruptId) {
17611
+ const extensionResult = await this.extensionManager.dispatchEvent("onInterrupted", { interruptId }, this.project, this);
17612
+ if (extensionResult.blocked) {
17613
+ logger.debug("Interrupt blocked by extension", { interruptId });
17614
+ return;
17615
+ }
17616
+ interruptId = extensionResult.interruptId;
16385
17617
  if (interruptId) {
16386
17618
  const subagentAbortController = this.subagentAbortControllers[interruptId];
16387
17619
  if (subagentAbortController) {
@@ -16554,7 +17786,7 @@ ${contentText}</agent-response>`;
16554
17786
  this.eventManager.sendTaskUpdated(this.task);
16555
17787
  }
16556
17788
  }
16557
- addUserMessage(id, content, promptContext) {
17789
+ addUserMessage(id, content, promptContext, images) {
16558
17790
  logger.info("Adding user message:", {
16559
17791
  baseDir: this.project.baseDir,
16560
17792
  content: content.substring(0, 100)
@@ -16565,6 +17797,7 @@ ${contentText}</agent-response>`;
16565
17797
  baseDir: this.project.baseDir,
16566
17798
  taskId: this.taskId,
16567
17799
  content,
17800
+ images,
16568
17801
  promptContext
16569
17802
  };
16570
17803
  this.eventManager.sendUserMessage(data);
@@ -16597,12 +17830,13 @@ ${contentText}</agent-response>`;
16597
17830
  this.eventManager.sendTaskMessageRemoved(this.project.baseDir, this.taskId, messageIds);
16598
17831
  }
16599
17832
  }
16600
- async redoUserPrompt(messageId, mode, updatedPrompt) {
17833
+ async redoUserPrompt(messageId, mode, updatedPrompt, updatedImages) {
16601
17834
  logger.info("Redoing user prompt:", {
16602
17835
  baseDir: this.project.baseDir,
16603
17836
  messageId,
16604
17837
  mode,
16605
- hasUpdatedPrompt: !!updatedPrompt
17838
+ hasUpdatedPrompt: !!updatedPrompt,
17839
+ hasUpdatedImages: !!updatedImages
16606
17840
  });
16607
17841
  const removedMessages = this.contextManager.removeMessagesUpToUserMessage(messageId);
16608
17842
  const originalUserMessage = removedMessages[0];
@@ -16610,14 +17844,16 @@ ${contentText}</agent-response>`;
16610
17844
  logger.warn("Could not find the specified user message to redo.", { messageId });
16611
17845
  return;
16612
17846
  }
16613
- const promptToRun = updatedPrompt ?? originalUserMessage.content;
17847
+ const originalText = extractTextContent(originalUserMessage.content);
17848
+ const promptToRun = updatedPrompt ?? originalText;
17849
+ const imagesToRun = updatedImages ?? (updatedPrompt !== void 0 ? void 0 : extractImagesFromContent(originalUserMessage.content));
16614
17850
  if (promptToRun) {
16615
17851
  logger.info("Found message content to run, reloading and re-running prompt.", {
16616
17852
  remainingMessagesCount: (await this.contextManager.getContextMessages()).length
16617
17853
  });
16618
17854
  this.sendTaskMessageRemoved(removedMessages.slice(1).map((msg) => msg.id));
16619
17855
  await this.updateContextInfo();
16620
- void this.runPrompt(promptToRun, mode, false, originalUserMessage.id);
17856
+ void this.runPrompt(promptToRun, mode, false, originalUserMessage.id, true, imagesToRun);
16621
17857
  } else {
16622
17858
  logger.warn("Could not find a previous user message to redo or an updated prompt to run.");
16623
17859
  }
@@ -16713,6 +17949,22 @@ ${contentText}</agent-response>`;
16713
17949
  }
16714
17950
  return skillMessages;
16715
17951
  }
17952
+ async smartCompactConversation(contextMessages, infoMessage = "Conversation smart-compacted.") {
17953
+ if (!contextMessages) {
17954
+ contextMessages = await this.contextManager.getContextMessages();
17955
+ }
17956
+ if (contextMessages.length === 0) {
17957
+ this.addLogMessage("warning", "No conversation to compact.");
17958
+ return [];
17959
+ }
17960
+ await this.contextManager.backupContext();
17961
+ const compactedMessages = smartCompactMessages(contextMessages);
17962
+ this.contextManager.setContextMessages(compactedMessages);
17963
+ await this.contextManager.loadMessages(compactedMessages, false);
17964
+ await this.updateContextInfo();
17965
+ this.addLogMessage("info", infoMessage, false, void 0, ["undoContextChange"]);
17966
+ return compactedMessages;
17967
+ }
16716
17968
  async compactConversation(mode, customInstructions, profile = null, contextMessages, promptContext, abortSignal, waitForAgentCompletion = true, loadingMessage = "Compacting conversation...") {
16717
17969
  if (!profile) {
16718
17970
  profile = await this.getTaskAgentProfile();
@@ -16801,7 +18053,7 @@ ${contentText}</agent-response>`;
16801
18053
  await this.contextManager.loadMessages(await this.contextManager.getContextMessages());
16802
18054
  }
16803
18055
  await this.updateContextInfo();
16804
- this.addLogMessage("info", "Conversation compacted.");
18056
+ this.addLogMessage("info", "Conversation compacted.", false, void 0, ["undoContextChange"]);
16805
18057
  } catch (error) {
16806
18058
  logger.error("Failed to compact conversation", {
16807
18059
  baseDir: this.project.baseDir,
@@ -16899,6 +18151,7 @@ ${contentText}</agent-response>`;
16899
18151
  }
16900
18152
  async updateContextInfo(checkContextFilesIncluded = false, checkRepoMapIncluded = false) {
16901
18153
  void this.debouncedUpdateContextInfo(checkContextFilesIncluded, checkRepoMapIncluded);
18154
+ void this.sendSkillsUpdated();
16902
18155
  }
16903
18156
  debouncedUpdateContextInfo = debounce(async (checkContextFilesIncluded = false, checkRepoMapIncluded = false) => {
16904
18157
  void this.sendRequestContextInfo();
@@ -17172,7 +18425,6 @@ ${error.stderr}`,
17172
18425
  id: uuid.v4()
17173
18426
  };
17174
18427
  this.addUserMessage(promptContext.id, prompt);
17175
- this.addLogMessage("loading", "Executing custom command...");
17176
18428
  try {
17177
18429
  if (!AIDER_MODES.includes(mode)) {
17178
18430
  const profile = await this.getTaskAgentProfile();
@@ -17180,11 +18432,22 @@ ${error.stderr}`,
17180
18432
  this.addLogMessage("error", "No active Agent profile found");
17181
18433
  return;
17182
18434
  }
18435
+ if (command.skills?.length) {
18436
+ for (const skillName of command.skills) {
18437
+ try {
18438
+ await this.activateSkill(skillName);
18439
+ } catch (error) {
18440
+ logger.warn(`Failed to activate skill '${skillName}' for command '${commandName}': ${error instanceof Error ? error.message : String(error)}`);
18441
+ }
18442
+ }
18443
+ }
17183
18444
  const systemPrompt = await this.promptsManager.getSystemPrompt(this.store.getSettings(), this, profile, command.autoApprove ?? this.task.autoApprove);
17184
18445
  const messages = command.includeContext === false ? [] : void 0;
17185
18446
  const contextFiles = command.includeContext === false ? [] : void 0;
18447
+ this.addLogMessage("loading", "Executing custom command...");
17186
18448
  await this.runPromptInAgent(profile, mode, prompt, promptContext, messages, contextFiles, systemPrompt);
17187
18449
  } else {
18450
+ this.addLogMessage("loading", "Executing custom command...");
17188
18451
  await this.runPromptInAider(mode, prompt, promptContext);
17189
18452
  }
17190
18453
  } finally {
@@ -18106,6 +19369,9 @@ class Project {
18106
19369
  if (!parentTask) {
18107
19370
  throw new Error(`Parent task with id ${normalizedParams.parentId} not found`);
18108
19371
  }
19372
+ if (parentTask.task.parentId) {
19373
+ normalizedParams.parentId = parentTask.task.parentId;
19374
+ }
18109
19375
  if (!parentTask.task.createdAt) {
18110
19376
  await parentTask.saveTask();
18111
19377
  }
@@ -18143,9 +19409,7 @@ class Project {
18143
19409
  };
18144
19410
  }
18145
19411
  const projectSettings = this.getProjectSettings();
18146
- const taskSettings = this.store.getSettings().taskSettings;
18147
19412
  const taskData = {
18148
- contextCompactingThreshold: taskSettings.contextCompactingThreshold,
18149
19413
  autoApprove: projectSettings.autoApproveLocked ? true : initialTaskData.autoApprove,
18150
19414
  ...initialTaskData
18151
19415
  };
@@ -18164,7 +19428,14 @@ class Project {
18164
19428
  }
18165
19429
  return task.task;
18166
19430
  }
18167
- async prepareTask(taskId = uuid.v4(), initialTaskData) {
19431
+ generateShortTaskId() {
19432
+ let id;
19433
+ do {
19434
+ id = uuid.v4().substring(0, 8);
19435
+ } while (this.tasks.has(id));
19436
+ return id;
19437
+ }
19438
+ async prepareTask(taskId = this.generateShortTaskId(), initialTaskData) {
18168
19439
  const task = new Task(
18169
19440
  this,
18170
19441
  taskId,
@@ -18333,6 +19604,9 @@ class Project {
18333
19604
  getAgentProfiles() {
18334
19605
  return this.agentProfileManager.getProjectProfiles(this);
18335
19606
  }
19607
+ resolveAgentProfile(id) {
19608
+ return this.agentProfileManager.resolveAgentProfile(id);
19609
+ }
18336
19610
  /**
18337
19611
  * Checks if any other task (excluding the specified taskId) uses the given worktree path.
18338
19612
  */
@@ -18588,6 +19862,10 @@ class EventManager {
18588
19862
  this.sendToWindows("clear-task", data);
18589
19863
  this.broadcastToEventConnectors("clear-task", data);
18590
19864
  }
19865
+ sendContextInfoUpdated(data) {
19866
+ this.sendToWindows("context-info-updated", data);
19867
+ this.broadcastToEventConnectors("context-info-updated", data);
19868
+ }
18591
19869
  // File management events
18592
19870
  sendFileAdded(baseDir, taskId, file) {
18593
19871
  const data = {
@@ -20979,7 +22257,8 @@ const createLmStudioLlm = (profile, model, settings, projectDir) => {
20979
22257
  const lmStudioProvider = openaiCompatible.createOpenAICompatible({
20980
22258
  name: "lmstudio",
20981
22259
  baseURL: baseUrl,
20982
- headers: profile.headers
22260
+ headers: profile.headers,
22261
+ includeUsage: true
20983
22262
  });
20984
22263
  return lmStudioProvider(model.id);
20985
22264
  };
@@ -21652,12 +22931,18 @@ const loadOpenaiCompatibleModels = async (profile, settings) => {
21652
22931
  return { models: [], success: false, error: errorMsg };
21653
22932
  }
21654
22933
  const data = await response.json();
21655
- const models = data.data?.map((model) => {
21656
- return {
21657
- id: model.id,
21658
- providerId: profile.id
21659
- };
21660
- }) || [];
22934
+ const models = data.data?.map(
22935
+ (model) => {
22936
+ const maxInputTokens = model.max_model_len ?? model.context_length ?? model.num_ctx ?? model.context_window;
22937
+ const maxOutputTokensLimit = model.max_completion_tokens ?? model.max_tokens;
22938
+ return {
22939
+ id: model.id,
22940
+ providerId: profile.id,
22941
+ ...maxInputTokens != null && { maxInputTokens },
22942
+ ...maxOutputTokensLimit != null && { maxOutputTokensLimit }
22943
+ };
22944
+ }
22945
+ ) || [];
21661
22946
  logger.info(`Loaded ${models.length} OpenAI-compatible models for profile ${profile.id}`);
21662
22947
  return { models, success: true };
21663
22948
  } catch (error) {
@@ -21706,11 +22991,14 @@ const createOpenAiCompatibleLlm = (profile, model, settings, projectDir) => {
21706
22991
  if (!baseUrl) {
21707
22992
  throw new Error(`Base URL is required for ${provider.name} provider. Set it in Providers settings or via the OPENAI_API_BASE environment variable.`);
21708
22993
  }
22994
+ const providerOverrides = model.providerOverrides;
22995
+ const trackTokenUsage = providerOverrides?.trackTokenUsage ?? provider.trackTokenUsage;
21709
22996
  const compatibleProvider = openaiCompatible.createOpenAICompatible({
21710
22997
  name: provider.name,
21711
22998
  apiKey,
21712
22999
  baseURL: baseUrl,
21713
- headers: profile.headers
23000
+ headers: profile.headers,
23001
+ includeUsage: trackTokenUsage !== false
21714
23002
  });
21715
23003
  return compatibleProvider(model.id);
21716
23004
  };
@@ -22192,7 +23480,7 @@ const getRequestyUsageReport = (task, provider, model, usage, providerMetadata)
22192
23480
  const cacheWriteTokens = requesty?.usage?.cachingTokens ?? 0;
22193
23481
  const cacheReadTokens = requesty?.usage?.cachedTokens ?? 0;
22194
23482
  const sentTokens = totalSentTokens - cacheReadTokens;
22195
- const messageCost = calculateRequestyCost(model, sentTokens, receivedTokens, cacheWriteTokens, cacheReadTokens);
23483
+ const messageCost = requesty?.usage?.cost ?? calculateRequestyCost(model, sentTokens, receivedTokens, cacheWriteTokens, cacheReadTokens);
22196
23484
  return {
22197
23485
  model: `${provider.id}/${model.id}`,
22198
23486
  sentTokens,
@@ -23102,6 +24390,7 @@ class ModelManager {
23102
24390
  getModelSettings(providerId, modelId, useModelInfoFallback = false) {
23103
24391
  let model;
23104
24392
  const providerModels = this.providerModels[providerId];
24393
+ logger.debug(`getModelSettings providerModels for provider: ${providerId}`, { providerModels });
23105
24394
  if (providerModels) {
23106
24395
  model = providerModels.find((m) => m.id === modelId);
23107
24396
  }
@@ -23115,6 +24404,11 @@ class ModelManager {
23115
24404
  };
23116
24405
  }
23117
24406
  }
24407
+ logger.debug("getModelSettings model", {
24408
+ providerId,
24409
+ modelId,
24410
+ model
24411
+ });
23118
24412
  return model;
23119
24413
  }
23120
24414
  createLlm(provider, model, settings, projectDir, toolSet, systemPrompt, providerMetadata) {
@@ -23680,7 +24974,7 @@ class VersionsManager {
23680
24974
  ...this.versionsInfo
23681
24975
  };
23682
24976
  }
23683
- logger.info("Checking for version updates...");
24977
+ logger.debug("Checking for version updates...");
23684
24978
  const aiderDeskCurrentVersion = app.getVersion();
23685
24979
  const autoUpdater = require("electron-updater").autoUpdater;
23686
24980
  if (this.pythonInstaller) {
@@ -25036,6 +26330,41 @@ class ExtensionFetcher {
25036
26330
  const extensions = [];
25037
26331
  try {
25038
26332
  const entries = await fs.readdir(extensionsPath, { withFileTypes: true });
26333
+ const rootIndexTs = path.join(extensionsPath, "index.ts");
26334
+ const rootIndexJs = path.join(extensionsPath, "index.js");
26335
+ const rootPackageJson = path.join(extensionsPath, "package.json");
26336
+ let rootIndexFile = null;
26337
+ if (await this.fileExists(rootIndexTs)) {
26338
+ rootIndexFile = rootIndexTs;
26339
+ } else if (await this.fileExists(rootIndexJs)) {
26340
+ rootIndexFile = rootIndexJs;
26341
+ }
26342
+ const hasRootPackageJson = await this.fileExists(rootPackageJson);
26343
+ if (rootIndexFile && hasRootPackageJson) {
26344
+ const metadata = await this.extractMetadataFromLocalFile(rootIndexFile);
26345
+ if (metadata) {
26346
+ const repoName = this.getRepoName(repoUrl);
26347
+ let readmeContent;
26348
+ const readmePath = path.join(extensionsPath, "README.md");
26349
+ try {
26350
+ const content = await fs.readFile(readmePath, "utf-8");
26351
+ if (content.trim()) {
26352
+ readmeContent = content;
26353
+ }
26354
+ } catch {
26355
+ }
26356
+ extensions.push({
26357
+ ...metadata,
26358
+ id: repoName,
26359
+ type: "folder",
26360
+ folder: repoName,
26361
+ repositoryUrl: repoUrl,
26362
+ hasDependencies: true,
26363
+ readmeContent
26364
+ });
26365
+ return extensions;
26366
+ }
26367
+ }
25039
26368
  for (const entry of entries) {
25040
26369
  if (entry.isDirectory()) {
25041
26370
  const indexPathTs = path.join(extensionsPath, entry.name, "index.ts");
@@ -25089,6 +26418,17 @@ class ExtensionFetcher {
25089
26418
  }
25090
26419
  return extensions;
25091
26420
  }
26421
+ getRepoName(repoUrl) {
26422
+ try {
26423
+ const url = new URL(repoUrl);
26424
+ const pathParts = url.pathname.split("/").filter(Boolean);
26425
+ if (pathParts.length >= 2) {
26426
+ return pathParts[1].replace(/\.git$/, "");
26427
+ }
26428
+ } catch {
26429
+ }
26430
+ return repoUrl.replace(/[^a-zA-Z0-9]/g, "-");
26431
+ }
25092
26432
  async fileExists(filePath) {
25093
26433
  try {
25094
26434
  await fs.access(filePath);
@@ -26009,7 +27349,7 @@ class ExtensionManager {
26009
27349
  }
26010
27350
  collectedSkills.push({
26011
27351
  ...skill,
26012
- location: "extension"
27352
+ location: skill.location || "extension"
26013
27353
  });
26014
27354
  }
26015
27355
  } catch (error) {
@@ -26350,12 +27690,19 @@ class ExtensionManager {
26350
27690
  } else if (extension.type === "folder" && extension.folder) {
26351
27691
  const repoDir = await this.fetcher.ensureRepoCloned(repositoryUrl);
26352
27692
  const extensionsPath = this.fetcher.getExtensionsPath(repositoryUrl, repoDir);
26353
- const sourcePath = path.join(extensionsPath, extension.folder);
26354
27693
  const targetPath = path.join(targetDir, extension.folder);
27694
+ let sourcePath = path.join(extensionsPath, extension.folder);
27695
+ if (!await this.fileExists(sourcePath)) {
27696
+ sourcePath = extensionsPath;
27697
+ }
26355
27698
  if (!await this.fileExists(sourcePath)) {
26356
27699
  throw new Error(`Extension folder not found in repository: ${extension.folder}`);
26357
27700
  }
26358
27701
  await fs.cp(sourcePath, targetPath, { recursive: true });
27702
+ const gitDir = path.join(targetPath, ".git");
27703
+ if (await this.fileExists(gitDir)) {
27704
+ await fs.rm(gitDir, { recursive: true, force: true });
27705
+ }
26359
27706
  if (extension.hasDependencies) {
26360
27707
  logger.debug(`[Extensions] Installing dependencies for ${extension.name}...`);
26361
27708
  await this.installDependencies(targetPath);
@@ -26480,12 +27827,19 @@ class ExtensionManager {
26480
27827
  } else if (extension.type === "folder" && extension.folder) {
26481
27828
  const repoDir = await this.fetcher.ensureRepoCloned(repositoryUrl);
26482
27829
  const extensionsPath = this.fetcher.getExtensionsPath(repositoryUrl, repoDir);
26483
- const sourcePath = path.join(extensionsPath, extension.folder);
26484
27830
  const targetPath = path.join(targetDir, extension.folder);
27831
+ let sourcePath = path.join(extensionsPath, extension.folder);
27832
+ if (!await this.fileExists(sourcePath)) {
27833
+ sourcePath = extensionsPath;
27834
+ }
26485
27835
  if (!await this.fileExists(sourcePath)) {
26486
27836
  throw new Error(`Extension folder not found in repository: ${extension.folder}`);
26487
27837
  }
26488
27838
  await fs.cp(sourcePath, targetPath, { recursive: true });
27839
+ const gitDir = path.join(targetPath, ".git");
27840
+ if (await this.fileExists(gitDir)) {
27841
+ await fs.rm(gitDir, { recursive: true, force: true });
27842
+ }
26489
27843
  const packageJsonPath = path.join(targetPath, "package.json");
26490
27844
  if (await this.fileExists(packageJsonPath)) {
26491
27845
  logger.debug(`[Extensions] Installing dependencies for ${extension.name}...`);
@@ -26535,7 +27889,7 @@ class ExtensionManager {
26535
27889
  if (result && typeof result === "object") {
26536
27890
  const partialEvent = result;
26537
27891
  currentEvent = { ...currentEvent, ...partialEvent };
26538
- if ("blocked" in currentEvent && currentEvent.blocked === true) {
27892
+ if ("blocked" in currentEvent && currentEvent.blocked) {
26539
27893
  logger.debug(`[Extensions] Event '${String(eventName)}' blocked by extension '${metadata.name}'`);
26540
27894
  break;
26541
27895
  }
@@ -26704,8 +28058,8 @@ class EventsHandler {
26704
28058
  const removedIds = await this.projectManager.getProject(baseDir).getTask(taskId)?.removeMessagesUpTo(messageId) ?? [];
26705
28059
  this.eventManager.sendTaskMessageRemoved(baseDir, taskId, removedIds);
26706
28060
  }
26707
- async redoUserPrompt(baseDir, taskId, messageId, mode, updatedPrompt) {
26708
- void this.projectManager.getProject(baseDir).getTask(taskId)?.redoUserPrompt(messageId, mode, updatedPrompt);
28061
+ async redoUserPrompt(baseDir, taskId, messageId, mode, updatedPrompt, updatedImages) {
28062
+ void this.projectManager.getProject(baseDir).getTask(taskId)?.redoUserPrompt(messageId, mode, updatedPrompt, updatedImages);
26709
28063
  }
26710
28064
  async resumeTask(baseDir, taskId) {
26711
28065
  void this.projectManager.getProject(baseDir).getTask(taskId)?.resumeTask();
@@ -26716,6 +28070,19 @@ class EventsHandler {
26716
28070
  await task.compactConversation(mode, customInstructions);
26717
28071
  }
26718
28072
  }
28073
+ async smartCompactConversation(baseDir, taskId) {
28074
+ const task = this.projectManager.getProject(baseDir).getTask(taskId);
28075
+ if (task) {
28076
+ await task.smartCompactConversation();
28077
+ }
28078
+ }
28079
+ async undoContextChange(baseDir, taskId) {
28080
+ const task = this.projectManager.getProject(baseDir).getTask(taskId);
28081
+ if (task) {
28082
+ return task.undoContextChange();
28083
+ }
28084
+ return false;
28085
+ }
26719
28086
  async handoffConversation(baseDir, taskId, focus) {
26720
28087
  const task = this.projectManager.getProject(baseDir).getTask(taskId);
26721
28088
  if (!task) {
@@ -26800,8 +28167,8 @@ class EventsHandler {
26800
28167
  applyEdits(baseDir, taskId, edits) {
26801
28168
  this.projectManager.getProject(baseDir).getTask(taskId)?.applyEdits(edits);
26802
28169
  }
26803
- async runPrompt(baseDir, taskId, prompt, mode) {
26804
- return this.projectManager.getProject(baseDir).getTask(taskId)?.runPrompt(prompt, mode) || [];
28170
+ async runPrompt(baseDir, taskId, prompt, mode, images) {
28171
+ return this.projectManager.getProject(baseDir).getTask(taskId)?.runPrompt(prompt, mode, true, void 0, true, images) || [];
26805
28172
  }
26806
28173
  async savePrompt(baseDir, taskId, prompt) {
26807
28174
  return this.projectManager.getProject(baseDir).getTask(taskId)?.savePromptOnly(prompt);
@@ -28760,6 +30127,16 @@ const migrateSettingsV18toV19 = (settings) => {
28760
30127
  }
28761
30128
  };
28762
30129
  };
30130
+ const migrateSettingsV19toV20 = (settings) => {
30131
+ const oldThreshold = settings.taskSettings?.contextCompactingThreshold;
30132
+ return {
30133
+ ...settings,
30134
+ taskSettings: {
30135
+ ...settings.taskSettings,
30136
+ contextCompactingThreshold: typeof oldThreshold === "number" ? { percentage: oldThreshold, tokens: 1e5 } : oldThreshold || { percentage: 90, tokens: 1e5 }
30137
+ }
30138
+ };
30139
+ };
28763
30140
  const DEFAULT_SETTINGS = {
28764
30141
  language: "en",
28765
30142
  startupMode: ProjectStartMode.Empty,
@@ -28820,7 +30197,7 @@ const DEFAULT_SETTINGS = {
28820
30197
  autoGenerateTaskName: true,
28821
30198
  showTaskStateActions: true,
28822
30199
  worktreeSymlinkFolders: ["node_modules", "vendor", "__pycache__", ".venv", "venv"],
28823
- contextCompactingThreshold: 0,
30200
+ contextCompactingThreshold: { percentage: 90, tokens: 1e5 },
28824
30201
  contextCompactionType: ContextCompactionType.Compact,
28825
30202
  defaultWorkingMode: "local",
28826
30203
  worktreeBranchPrefix: "aider-desk/task/",
@@ -28838,7 +30215,7 @@ const DEFAULT_SETTINGS = {
28838
30215
  const compareBaseDirs = (baseDir1, baseDir2) => {
28839
30216
  return normalizeBaseDir(baseDir1) === normalizeBaseDir(baseDir2);
28840
30217
  };
28841
- const CURRENT_SETTINGS_VERSION = 19;
30218
+ const CURRENT_SETTINGS_VERSION = 20;
28842
30219
  class Store {
28843
30220
  // @ts-expect-error expected to be initialized
28844
30221
  store;
@@ -28989,6 +30366,10 @@ class Store {
28989
30366
  settings = migrateSettingsV18toV19(settings);
28990
30367
  settingsVersion = 19;
28991
30368
  }
30369
+ if (settingsVersion === 19) {
30370
+ settings = migrateSettingsV19toV20(settings);
30371
+ settingsVersion = 20;
30372
+ }
28992
30373
  this.store.set("settings", settings);
28993
30374
  this.store.set("openProjects", openProjects || []);
28994
30375
  this.store.set("providers", providers || []);
@@ -29182,4 +30563,5 @@ if (!process.env.VITEST) {
29182
30563
  }
29183
30564
  exports.addProjectsFromEnv = addProjectsFromEnv;
29184
30565
  exports.initializeLangfuseExporter = initializeLangfuseExporter;
30566
+ exports.initializePostHogExporter = initializePostHogExporter;
29185
30567
  exports.logger = logger;