@aiderdesk/aiderdesk 0.62.0 → 0.64.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 (58) hide show
  1. package/out/renderer/assets/{_baseUniq-B7TxjYgy.js → _baseUniq-C6Q8LpuQ.js} +1 -1
  2. package/out/renderer/assets/{arc-TuVjX2tH.js → arc-DoIK-bD2.js} +1 -1
  3. package/out/renderer/assets/{architectureDiagram-Q4EWVU46-CnAcyRvO.js → architectureDiagram-Q4EWVU46-B8_dgBXp.js} +5 -5
  4. package/out/renderer/assets/{blockDiagram-DXYQGD6D-DBoJhGAM.js → blockDiagram-DXYQGD6D-BDOvGPDN.js} +6 -6
  5. package/out/renderer/assets/{c4Diagram-AHTNJAMY-BzW0gKwX.js → c4Diagram-AHTNJAMY-1ABZnJ2v.js} +2 -2
  6. package/out/renderer/assets/{channel-CqR4FAVX.js → channel-Cr_H2zdE.js} +1 -1
  7. package/out/renderer/assets/{chunk-4BX2VUAB-DQjg_Naa.js → chunk-4BX2VUAB-d88VZY9C.js} +1 -1
  8. package/out/renderer/assets/{chunk-4TB4RGXK-DkcI1yaW.js → chunk-4TB4RGXK-DLcMuHVI.js} +5 -5
  9. package/out/renderer/assets/{chunk-55IACEB6-pFPEMTmI.js → chunk-55IACEB6-BO1oJBQV.js} +1 -1
  10. package/out/renderer/assets/{chunk-EDXVE4YY-DRk838hn.js → chunk-EDXVE4YY-Dt80V_EG.js} +1 -1
  11. package/out/renderer/assets/{chunk-FMBD7UC4-BagQdd5u.js → chunk-FMBD7UC4-D5MNbIWZ.js} +1 -1
  12. package/out/renderer/assets/{chunk-OYMX7WX6-DOGJ-XHp.js → chunk-OYMX7WX6-DBFhtMcs.js} +3 -3
  13. package/out/renderer/assets/{chunk-QZHKN3VN-CdzjbbBk.js → chunk-QZHKN3VN-Bxwt_pyh.js} +1 -1
  14. package/out/renderer/assets/{chunk-YZCP3GAM-BKZRTlUj.js → chunk-YZCP3GAM-gAcMGuhT.js} +1 -1
  15. package/out/renderer/assets/{classDiagram-6PBFFD2Q-CjVhmlxb.js → classDiagram-6PBFFD2Q-B7lgamMP.js} +6 -6
  16. package/out/renderer/assets/{classDiagram-v2-HSJHXN6E-CjVhmlxb.js → classDiagram-v2-HSJHXN6E-B7lgamMP.js} +6 -6
  17. package/out/renderer/assets/{clone-B-kqpcr4.js → clone-DKkqtIT8.js} +1 -1
  18. package/out/renderer/assets/{cose-bilkent-S5V4N54A-CZLRecVB.js → cose-bilkent-S5V4N54A-BZNBIG2x.js} +1 -1
  19. package/out/renderer/assets/{dagre-KV5264BT-BUAzC7Um.js → dagre-KV5264BT-C3hXUNb-.js} +6 -6
  20. package/out/renderer/assets/{diagram-5BDNPKRD-DNQEDszc.js → diagram-5BDNPKRD-DNh45EqP.js} +6 -6
  21. package/out/renderer/assets/{diagram-G4DWMVQ6-D9YY6W2O.js → diagram-G4DWMVQ6-8lhqJfPk.js} +6 -6
  22. package/out/renderer/assets/{diagram-MMDJMWI5-B5H49OVD.js → diagram-MMDJMWI5-BcI1Ek4N.js} +5 -5
  23. package/out/renderer/assets/{diagram-TYMM5635-Bw7r467p.js → diagram-TYMM5635-DuHcW-s7.js} +5 -5
  24. package/out/renderer/assets/{erDiagram-SMLLAGMA-Bz4ALNLp.js → erDiagram-SMLLAGMA-I6Q9HYdF.js} +4 -4
  25. package/out/renderer/assets/{flowDiagram-DWJPFMVM-Uqg1ZeN2.js → flowDiagram-DWJPFMVM-BzRjtX5C.js} +6 -6
  26. package/out/renderer/assets/{ganttDiagram-T4ZO3ILL-Dm0tRLCH.js → ganttDiagram-T4ZO3ILL-DVkem_IA.js} +1 -1
  27. package/out/renderer/assets/{gitGraphDiagram-UUTBAWPF-R8imfIrj.js → gitGraphDiagram-UUTBAWPF-BYpvdMpK.js} +6 -6
  28. package/out/renderer/assets/{graph-B5YWEnt0.js → graph-CAtr5PoG.js} +2 -2
  29. package/out/renderer/assets/{index-D7Xi0GX5.js → index-CNL53LoL.js} +4230 -2874
  30. package/out/renderer/assets/{index-B6Zj03wk.css → index-Duw36zwk.css} +127 -0
  31. package/out/renderer/assets/{infoDiagram-42DDH7IO-CWPYNP1k.js → infoDiagram-42DDH7IO-BcmBthOY.js} +4 -4
  32. package/out/renderer/assets/{ishikawaDiagram-UXIWVN3A-CFTrc-S6.js → ishikawaDiagram-UXIWVN3A-moTWny-V.js} +1 -1
  33. package/out/renderer/assets/{journeyDiagram-VCZTEJTY-Dfd-piIf.js → journeyDiagram-VCZTEJTY-DOW8zaZt.js} +4 -4
  34. package/out/renderer/assets/{kanban-definition-6JOO6SKY-BocrfAtb.js → kanban-definition-6JOO6SKY-DpJjTob4.js} +2 -2
  35. package/out/renderer/assets/{layout-DHzrXVin.js → layout-BvH51Ui9.js} +4 -4
  36. package/out/renderer/assets/{min-D6g96v7R.js → min-CowxrbD6.js} +2 -2
  37. package/out/renderer/assets/{mindmap-definition-QFDTVHPH-D9cyK1Gb.js → mindmap-definition-QFDTVHPH-DggFFNHq.js} +3 -3
  38. package/out/renderer/assets/{pieDiagram-DEJITSTG-LUWwVJA5.js → pieDiagram-DEJITSTG-BED4dnMF.js} +6 -6
  39. package/out/renderer/assets/{quadrantDiagram-34T5L4WZ-DdWTi9yW.js → quadrantDiagram-34T5L4WZ-RpQ3qNU5.js} +1 -1
  40. package/out/renderer/assets/{requirementDiagram-MS252O5E-BoLT3tay.js → requirementDiagram-MS252O5E-VQt4zBMB.js} +3 -3
  41. package/out/renderer/assets/{sankeyDiagram-XADWPNL6-B4gY_QI-.js → sankeyDiagram-XADWPNL6-DywR7qAk.js} +1 -1
  42. package/out/renderer/assets/{sequenceDiagram-FGHM5R23-B-JJvxQW.js → sequenceDiagram-FGHM5R23-CVPfZD4e.js} +3 -3
  43. package/out/renderer/assets/{stateDiagram-FHFEXIEX-BntwOBUs.js → stateDiagram-FHFEXIEX-BrH8Q8ZG.js} +8 -8
  44. package/out/renderer/assets/{stateDiagram-v2-QKLJ7IA2-C__eWVIe.js → stateDiagram-v2-QKLJ7IA2-BTWk2K0H.js} +4 -4
  45. package/out/renderer/assets/{timeline-definition-GMOUNBTQ-C7ch2INk.js → timeline-definition-GMOUNBTQ-DwDUCrTb.js} +2 -2
  46. package/out/renderer/assets/{vennDiagram-DHZGUBPP-CQ__NSue.js → vennDiagram-DHZGUBPP-Bjvr7yGM.js} +1 -1
  47. package/out/renderer/assets/{wardley-RL74JXVD-Kh6mNiRV.js → wardley-RL74JXVD-Bo-sW7uQ.js} +3 -3
  48. package/out/renderer/assets/{wardleyDiagram-NUSXRM2D-CfRYzJMU.js → wardleyDiagram-NUSXRM2D-DRW_1PCJ.js} +5 -5
  49. package/out/renderer/assets/{xychartDiagram-5P7HB3ND-BFq2zIne.js → xychartDiagram-5P7HB3ND-Ds-qS4nC.js} +1 -1
  50. package/out/renderer/index.html +2 -2
  51. package/out/resources/connector/connector.py +9 -6
  52. package/out/resources/skills/extension-creator/references/config-components.md +6 -6
  53. package/out/runner.js +1142 -246
  54. package/package.json +2 -2
  55. package/patches/@ai-sdk+deepseek+1.0.37.patch +150 -0
  56. package/out/resources/linux/probe +0 -0
  57. package/out/resources/linux-x64/probe +0 -0
  58. /package/patches/{ai+5.0.172.patch → ai+5.0.179.patch} +0 -0
package/out/runner.js CHANGED
@@ -47,7 +47,6 @@ const filenamifyImport = require("filenamify");
47
47
  const slugify = require("slugify");
48
48
  const Turndown = require("turndown");
49
49
  const cheerio = require("cheerio");
50
- const yamlFrontMatter = require("yaml-front-matter");
51
50
  const uuid = require("uuid");
52
51
  const index_js = require("@modelcontextprotocol/sdk/client/index.js");
53
52
  const stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
@@ -59,6 +58,7 @@ const express = require("express");
59
58
  const cors = require("cors");
60
59
  const cloudflared = require("cloudflared");
61
60
  const socket_io = require("socket.io");
61
+ const yamlFrontMatter = require("yaml-front-matter");
62
62
  const debounce = require("lodash/debounce.js");
63
63
  const crypto = require("crypto");
64
64
  const undici = require("undici");
@@ -243,6 +243,7 @@ const WorktreeSchema = zod.z.object({
243
243
  path: zod.z.string(),
244
244
  baseBranch: zod.z.string().optional(),
245
245
  baseCommit: zod.z.string().optional(),
246
+ branch: zod.z.string().optional(),
246
247
  prunable: zod.z.boolean().optional()
247
248
  });
248
249
  const MergeStateSchema = zod.z.object({
@@ -299,7 +300,10 @@ const ProjectSettingsSchema = zod.z.object({
299
300
  contextCompactingThreshold: zod.z.number().optional(),
300
301
  weakModelLocked: zod.z.boolean().optional(),
301
302
  autoApproveLocked: zod.z.boolean().optional(),
302
- updatedFilesGroupMode: zod.z.enum(["grouped", "flat"]).default("flat")
303
+ updatedFilesGroupMode: zod.z.enum(["grouped", "flat"]).default("flat"),
304
+ disabledRuleFiles: zod.z.array(zod.z.string()).default([]),
305
+ contextSidebarSectionsOrder: zod.z.array(zod.z.string()).default([]),
306
+ contextSidebarSectionsHidden: zod.z.array(zod.z.string()).default([])
303
307
  });
304
308
  var ToolApprovalState = /* @__PURE__ */ ((ToolApprovalState2) => {
305
309
  ToolApprovalState2["Always"] = "always";
@@ -483,19 +487,22 @@ const parseUsageReport = (model, report) => {
483
487
  };
484
488
  const normalizeBaseDir = (baseDir, os2 = process.platform === "win32" ? OS.Windows : process.platform === "darwin" ? OS.MacOS : OS.Linux) => {
485
489
  if (os2 === OS.Windows) {
486
- return baseDir.toLowerCase();
490
+ return baseDir.toLowerCase().replace(/\\+$/, "");
487
491
  } else {
488
492
  const wslPrefix = "\\\\wsl.localhost\\";
489
493
  if (baseDir.startsWith(wslPrefix)) {
490
494
  const thirdBackslashIndex = baseDir.indexOf("\\", wslPrefix.length);
491
495
  if (thirdBackslashIndex !== -1) {
492
496
  const actualPath = baseDir.substring(thirdBackslashIndex + 1);
493
- return "/" + actualPath.replace(/\\/g, "/");
497
+ return ("/" + actualPath.replace(/\\/g, "/")).replace(/\/+$/, "");
494
498
  }
495
499
  }
496
- return baseDir;
500
+ return baseDir.replace(/\/+$/, "");
497
501
  }
498
502
  };
503
+ const compareBaseDirs$1 = (baseDir1, baseDir2, os2) => {
504
+ return normalizeBaseDir(baseDir1, os2) === normalizeBaseDir(baseDir2, os2);
505
+ };
499
506
  const fileExists = async (fileName) => {
500
507
  return await fs.stat(fileName).catch(() => null) !== null;
501
508
  };
@@ -812,6 +819,7 @@ const POSTHOG_HOST = "https://eu.i.posthog.com";
812
819
  process.env.AIDER_DESK_HEADLESS === "true";
813
820
  const AUTH_USERNAME = process.env.AIDER_DESK_USERNAME;
814
821
  const AUTH_PASSWORD = process.env.AIDER_DESK_PASSWORD;
822
+ const CORS_ALLOWED_ORIGINS = process.env.AIDER_DESK_CORS_ALLOWED_ORIGINS;
815
823
  const PROBE_BINARY_PATH = path.join(
816
824
  RESOURCES_DIR,
817
825
  process.platform === "win32" ? "win" : process.platform === "darwin" ? "macos" : "linux",
@@ -1434,6 +1442,7 @@ const search = async (options) => {
1434
1442
  args.push(`${flag} ${String(value)}`);
1435
1443
  }
1436
1444
  }
1445
+ args.push("--");
1437
1446
  args.push(`"${options.query}"`);
1438
1447
  if (options.path) {
1439
1448
  args.push(`"${options.path}"`);
@@ -1518,11 +1527,11 @@ const DEFAULT_PROVIDER_MODELS = {
1518
1527
  bedrock: "global.anthropic.claude-sonnet-4-6",
1519
1528
  cerebras: "qwen-3-235b-a22b-instruct-2507",
1520
1529
  "claude-agent-sdk": "sonnet",
1521
- deepseek: "deepseek-chat",
1530
+ deepseek: "deepseek-v4-pro",
1522
1531
  gemini: "gemini-pro-latest",
1523
1532
  "gemini-cli": "gemini-2.5-pro",
1524
1533
  groq: "moonshotai/kimi-k2-instruct-0905",
1525
- "kimi-plan": "k2p5",
1534
+ "kimi-plan": "k2p6",
1526
1535
  openai: "gpt-5.4",
1527
1536
  openrouter: "anthropic/claude-sonnet-4.6",
1528
1537
  opencode: "claude-sonnet-4-6",
@@ -1842,7 +1851,8 @@ const getDefaultProviderParams = (providerName) => {
1842
1851
  case "deepseek":
1843
1852
  provider = {
1844
1853
  name: "deepseek",
1845
- apiKey: ""
1854
+ apiKey: "",
1855
+ thinkingEnabled: true
1846
1856
  };
1847
1857
  break;
1848
1858
  case "bedrock":
@@ -2361,7 +2371,10 @@ const getDefaultProjectSettings = (store, providerModels, baseDir, defaultAgentP
2361
2371
  currentMode: "agent",
2362
2372
  agentProfileId: defaultAgentProfileId,
2363
2373
  autoApproveLocked: false,
2364
- updatedFilesGroupMode: "flat"
2374
+ updatedFilesGroupMode: "flat",
2375
+ disabledRuleFiles: [],
2376
+ contextSidebarSectionsOrder: [],
2377
+ contextSidebarSectionsHidden: []
2365
2378
  };
2366
2379
  };
2367
2380
  const filenamify = filenamifyImport.default;
@@ -3048,19 +3061,16 @@ const scrapeWeb = async (url, timeout = 6e4, abortSignal, format = "markdown") =
3048
3061
  const scraper = new WebScraper();
3049
3062
  return await scraper.scrape(url, timeout, abortSignal, format);
3050
3063
  };
3051
- const fileLocks = /* @__PURE__ */ new Map();
3052
- const withFileLock = (filePath, operation) => {
3053
- logger.debug("Acquiring file lock:", { filePath });
3054
- const currentLock = fileLocks.get(filePath) || Promise.resolve();
3055
- const newLock = currentLock.then(operation, operation);
3056
- fileLocks.set(filePath, newLock);
3057
- newLock.finally(() => {
3058
- if (fileLocks.get(filePath) === newLock) {
3059
- logger.debug("Releasing file lock:", { filePath });
3060
- fileLocks.delete(filePath);
3061
- }
3062
- });
3063
- return newLock;
3064
+ const THINKING_RESPONSE_STAR_TAG = "---\n► **THINKING**\n";
3065
+ const ANSWER_RESPONSE_START_TAG = "---\n► **ANSWER**\n";
3066
+ const extractPromptContextFromToolResult = (toolResult) => {
3067
+ if (toolResult && typeof toolResult === "object" && "promptContext" in toolResult) {
3068
+ return toolResult.promptContext;
3069
+ }
3070
+ return void 0;
3071
+ };
3072
+ const findLastUserMessage = (messages) => {
3073
+ return [...messages].reverse().find((msg) => msg.role === MessageRole.User);
3064
3074
  };
3065
3075
  const expandTilde = (filePath) => {
3066
3076
  if (filePath.startsWith("~/") || filePath === "~") {
@@ -3068,11 +3078,25 @@ const expandTilde = (filePath) => {
3068
3078
  }
3069
3079
  return filePath;
3070
3080
  };
3071
- const readFileContent = async (absolutePath, withLines = false, lineOffset = 0, lineLimit = 1e3) => {
3081
+ const readFileContent = async (absolutePath, withLines = false, lineOffset = 0, lineLimit = 1e3, sizeLimit = 0.05 * lineLimit) => {
3072
3082
  const fileContentBuffer = await fs.readFile(absolutePath);
3073
3083
  if (istextorbinary.isBinary(absolutePath, fileContentBuffer)) {
3074
3084
  throw new Error("Binary files cannot be read.");
3075
3085
  }
3086
+ const fileSizeKB = fileContentBuffer.length / 1024;
3087
+ if (fileSizeKB > sizeLimit) {
3088
+ const truncatedBytes = fileContentBuffer.subarray(0, Math.floor(sizeLimit * 1024));
3089
+ const truncatedContent = truncatedBytes.toString("utf8");
3090
+ const truncatedLines = truncatedContent.split("\n");
3091
+ if (withLines) {
3092
+ return truncatedLines.map((line, index) => `${index + 1}|${line}`).join("\n") + `
3093
+
3094
+ File size limit (${sizeLimit.toFixed(1)} KB) exceeded. Use shell commands (e.g., head, tail, grep) to read specific parts.`;
3095
+ }
3096
+ return truncatedContent + `
3097
+
3098
+ File size limit (${sizeLimit.toFixed(1)} KB) exceeded. Use shell commands (e.g., head, tail, grep) to read specific parts.`;
3099
+ }
3076
3100
  const fileContent = fileContentBuffer.toString("utf8");
3077
3101
  const lines = fileContent.split("\n");
3078
3102
  const totalLines = lines.length;
@@ -3088,6 +3112,42 @@ const readFileContent = async (absolutePath, withLines = false, lineOffset = 0,
3088
3112
  }
3089
3113
  return limitedLines.join("\n");
3090
3114
  };
3115
+ const truncateToolResult = async (content, maxLines = 1e3, maxSizeKB = 50) => {
3116
+ const lines = content.split("\n");
3117
+ const sizeBytes = Buffer.byteLength(content, "utf8");
3118
+ const sizeKB = sizeBytes / 1024;
3119
+ if (lines.length <= maxLines && sizeKB <= maxSizeKB) {
3120
+ return content;
3121
+ }
3122
+ const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
3123
+ const tmpFileName = `aider-desk-tool-result-${id}.txt`;
3124
+ const tmpFilePath = path.join(os.tmpdir(), tmpFileName);
3125
+ await fs.writeFile(tmpFilePath, content, "utf8");
3126
+ const previewLines = lines.slice(0, maxLines);
3127
+ const reasons = [];
3128
+ if (lines.length > maxLines) {
3129
+ reasons.push(`${lines.length} lines exceeded limit of ${maxLines}`);
3130
+ }
3131
+ if (sizeKB > maxSizeKB) {
3132
+ reasons.push(`${sizeKB.toFixed(1)} KB exceeded limit of ${maxSizeKB} KB`);
3133
+ }
3134
+ return previewLines.join("\n") + `
3135
+ ... Content truncated (${reasons.join(", ")}). Full content saved to ${tmpFilePath}.`;
3136
+ };
3137
+ const fileLocks = /* @__PURE__ */ new Map();
3138
+ const withFileLock = (filePath, operation) => {
3139
+ logger.debug("Acquiring file lock:", { filePath });
3140
+ const currentLock = fileLocks.get(filePath) || Promise.resolve();
3141
+ const newLock = currentLock.then(operation, operation);
3142
+ fileLocks.set(filePath, newLock);
3143
+ newLock.finally(() => {
3144
+ if (fileLocks.get(filePath) === newLock) {
3145
+ logger.debug("Releasing file lock:", { filePath });
3146
+ fileLocks.delete(filePath);
3147
+ }
3148
+ });
3149
+ return newLock;
3150
+ };
3091
3151
  const createPowerToolset = (task, profile, promptContext, abortSignal) => {
3092
3152
  const approvalManager = new ApprovalManager(task, profile);
3093
3153
  const fileEditTool = ai.tool({
@@ -3406,10 +3466,10 @@ Do not use escape characters \\ in the string like \\n or \\" and others. Do not
3406
3466
  encoding: "utf8",
3407
3467
  signal: abortSignal
3408
3468
  });
3409
- const lines = fileContent.split("\n");
3469
+ const lines2 = fileContent.split("\n");
3410
3470
  const relativeFilePath = path.relative(task.getTaskDir(), absoluteFilePath);
3411
- for (let index = 0; index < lines.length; index++) {
3412
- const line = lines[index];
3471
+ for (let index = 0; index < lines2.length; index++) {
3472
+ const line = lines2[index];
3413
3473
  if (searchRegex.test(line)) {
3414
3474
  if (results.length >= maxResults) {
3415
3475
  break;
@@ -3421,8 +3481,8 @@ Do not use escape characters \\ in the string like \\n or \\" and others. Do not
3421
3481
  };
3422
3482
  if (contextLines > 0) {
3423
3483
  const start = Math.max(0, index - contextLines);
3424
- const end = Math.min(lines.length - 1, index + contextLines);
3425
- matchResult.context = lines.slice(start, end + 1);
3484
+ const end = Math.min(lines2.length - 1, index + contextLines);
3485
+ matchResult.context = lines2.slice(start, end + 1);
3426
3486
  }
3427
3487
  results.push(matchResult);
3428
3488
  }
@@ -3431,7 +3491,40 @@ Do not use escape characters \\ in the string like \\n or \\" and others. Do not
3431
3491
  if (results.length === 0) {
3432
3492
  return `No matches found for pattern '${searchTerm}' in files matching '${filePattern}'.`;
3433
3493
  }
3434
- return results;
3494
+ const grouped = {};
3495
+ for (const r of results) {
3496
+ if (!grouped[r.filePath]) {
3497
+ grouped[r.filePath] = [];
3498
+ }
3499
+ grouped[r.filePath].push(r);
3500
+ }
3501
+ const lines = [];
3502
+ lines.push(`## Grep Results: \`${searchTerm}\` in \`${filePattern}\` (${results.length} matches)`);
3503
+ lines.push("");
3504
+ for (const [filePath, matches] of Object.entries(grouped)) {
3505
+ lines.push(`### ${filePath} (${matches.length} ${matches.length === 1 ? "match" : "matches"})`);
3506
+ for (const match of matches) {
3507
+ const escapedContent = match.lineContent.replace(/`/g, "\\`");
3508
+ lines.push(`- **L${match.lineNumber}:** \`${escapedContent}\``);
3509
+ if (match.context && match.context.length > 0) {
3510
+ lines.push(" ```");
3511
+ for (const ctxLine of match.context) {
3512
+ lines.push(` ${ctxLine}`);
3513
+ }
3514
+ lines.push(" ```");
3515
+ }
3516
+ }
3517
+ lines.push("");
3518
+ }
3519
+ const notices = [];
3520
+ if (results.length >= maxResults) {
3521
+ notices.push(`${maxResults} matches limit reached. Use maxResults=${maxResults * 2} for more, or refine pattern`);
3522
+ }
3523
+ if (notices.length > 0) {
3524
+ lines.push("---");
3525
+ lines.push(`[${notices.join(". ")}]`);
3526
+ }
3527
+ return lines.join("\n");
3435
3528
  } catch (error) {
3436
3529
  if (isAbortError(error)) {
3437
3530
  return "Operation was cancelled by user.";
@@ -3505,24 +3598,26 @@ Timeout: ${timeout}ms`;
3505
3598
  abortSignal?.removeEventListener("abort", abortListener);
3506
3599
  }
3507
3600
  };
3508
- const resolveWithResult = () => {
3601
+ const resolveWithResult = async () => {
3509
3602
  if (isResolved) {
3510
3603
  return;
3511
3604
  }
3512
3605
  isResolved = true;
3513
3606
  cleanup();
3514
- resolve({ stdout, stderr, exitCode });
3607
+ const truncatedStdout = await truncateToolResult(stdout);
3608
+ const truncatedStderr = await truncateToolResult(stderr);
3609
+ resolve({ stdout: truncatedStdout, stderr: truncatedStderr, exitCode });
3515
3610
  };
3516
3611
  abortListener = () => {
3517
3612
  if (isResolved) {
3518
3613
  return;
3519
3614
  }
3520
3615
  if (childProcess?.pid) {
3521
- treeKill(childProcess.pid, "SIGTERM");
3616
+ treeKill(childProcess.pid, "SIGKILL");
3522
3617
  }
3523
3618
  stderr = "Operation was cancelled by user.";
3524
3619
  exitCode = 130;
3525
- resolveWithResult();
3620
+ void resolveWithResult();
3526
3621
  };
3527
3622
  abortSignal?.addEventListener("abort", abortListener);
3528
3623
  try {
@@ -3539,13 +3634,16 @@ Timeout: ${timeout}ms`;
3539
3634
  return;
3540
3635
  }
3541
3636
  if (childProcess?.pid) {
3542
- treeKill(childProcess.pid, "SIGTERM");
3637
+ treeKill(childProcess.pid, "SIGKILL");
3543
3638
  }
3544
3639
  stderr = `Error: Command timed out after ${timeout}ms. Consider increasing the timeout parameter.`;
3545
3640
  exitCode = 124;
3546
- resolveWithResult();
3641
+ void resolveWithResult();
3547
3642
  }, timeout);
3548
3643
  childProcess.stdout?.on("data", (data) => {
3644
+ if (isResolved) {
3645
+ return;
3646
+ }
3549
3647
  const chunk = data.toString("utf-8");
3550
3648
  stdout += chunk;
3551
3649
  task.addToolMessage(
@@ -3562,6 +3660,9 @@ Timeout: ${timeout}ms`;
3562
3660
  );
3563
3661
  });
3564
3662
  childProcess.stderr?.on("data", (data) => {
3663
+ if (isResolved) {
3664
+ return;
3665
+ }
3565
3666
  const chunk = data.toString("utf-8");
3566
3667
  stderr += chunk;
3567
3668
  task.addToolMessage(
@@ -3581,7 +3682,7 @@ Timeout: ${timeout}ms`;
3581
3682
  if (!isResolved) {
3582
3683
  stderr = error.message;
3583
3684
  exitCode = 1;
3584
- resolveWithResult();
3685
+ void resolveWithResult();
3585
3686
  }
3586
3687
  });
3587
3688
  childProcess.on("exit", (code, signal) => {
@@ -3593,14 +3694,14 @@ Timeout: ${timeout}ms`;
3593
3694
  } else {
3594
3695
  exitCode = 1;
3595
3696
  }
3596
- resolveWithResult();
3697
+ void resolveWithResult();
3597
3698
  }
3598
3699
  });
3599
3700
  } catch (error) {
3600
3701
  if (!isResolved) {
3601
3702
  stderr = error instanceof Error ? error.message : String(error);
3602
3703
  exitCode = 1;
3603
- resolveWithResult();
3704
+ void resolveWithResult();
3604
3705
  }
3605
3706
  }
3606
3707
  });
@@ -4073,7 +4174,8 @@ Parent Task ID: ${parentTaskId || "none (top-level task)"}` : ""}`;
4073
4174
  const newTask = await task.getProject().createNewTask({
4074
4175
  parentId: parentTaskId || null,
4075
4176
  name: name || "",
4076
- autoApprove
4177
+ autoApprove,
4178
+ workingMode: worktree ? "worktree" : "local"
4077
4179
  });
4078
4180
  const updates = {};
4079
4181
  if (agentProfileId) {
@@ -4085,9 +4187,6 @@ Parent Task ID: ${parentTaskId || "none (top-level task)"}` : ""}`;
4085
4187
  updates.model = modelParts.join("/");
4086
4188
  updates.mainModel = modelId;
4087
4189
  }
4088
- if (worktree) {
4089
- updates.workingMode = "worktree";
4090
- }
4091
4190
  const taskInstance = task.getProject().getTask(newTask.id);
4092
4191
  if (!taskInstance) {
4093
4192
  throw new Error(`Failed to get task instance for newly created task ${newTask.id}`);
@@ -4699,64 +4798,6 @@ New content: "${content}"`;
4699
4798
  }
4700
4799
  return filteredTools;
4701
4800
  };
4702
- const SKILLS_DIR_NAME = "skills";
4703
- const SKILL_MARKDOWN_FILE = "SKILL.md";
4704
- const parseSkillFrontMatter = (markdown) => {
4705
- const parsed = yamlFrontMatter.loadFront(markdown);
4706
- const name = typeof parsed.name === "string" ? parsed.name : void 0;
4707
- const description = typeof parsed.description === "string" ? parsed.description : void 0;
4708
- if (!name || !description) {
4709
- return null;
4710
- }
4711
- return { name, description };
4712
- };
4713
- const safeReadDir = async (dirPath) => {
4714
- try {
4715
- return await fs.readdir(dirPath);
4716
- } catch {
4717
- return [];
4718
- }
4719
- };
4720
- const safeStat = async (filePath) => {
4721
- try {
4722
- return await fs.stat(filePath);
4723
- } catch {
4724
- return null;
4725
- }
4726
- };
4727
- const loadSkillsFromDir = async (skillsRootDir, location) => {
4728
- const entries = await safeReadDir(skillsRootDir);
4729
- const skills = [];
4730
- for (const entry of entries) {
4731
- const dirPath = path.join(skillsRootDir, entry);
4732
- const stat = await safeStat(dirPath);
4733
- if (!stat?.isDirectory()) {
4734
- continue;
4735
- }
4736
- const skillMdPath = path.join(dirPath, SKILL_MARKDOWN_FILE);
4737
- const skillMdStat = await safeStat(skillMdPath);
4738
- if (!skillMdStat?.isFile()) {
4739
- continue;
4740
- }
4741
- let markdown;
4742
- try {
4743
- markdown = await fs.readFile(skillMdPath, "utf8");
4744
- } catch {
4745
- continue;
4746
- }
4747
- const parsed = parseSkillFrontMatter(markdown);
4748
- if (!parsed) {
4749
- continue;
4750
- }
4751
- skills.push({
4752
- name: parsed.name,
4753
- description: parsed.description,
4754
- location,
4755
- dirPath
4756
- });
4757
- }
4758
- return skills;
4759
- };
4760
4801
  const getActivateSkillDescription = (skills) => {
4761
4802
  const instructions = 'Execute a skill within the main conversation\n\n<skills_instructions>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to invoke:\n- Use this tool with the skill name only (no arguments)\n- Example: {"skill": "pdf"}\n\nImportant:\n- When a skill is relevant, you must invoke this tool IMMEDIATELY as your first action\n- NEVER just announce or mention a skill in your text response without actually calling this tool\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already running\n</skills_instructions>';
4762
4803
  const availableSkills = skills.map((skill) => {
@@ -4780,15 +4821,10 @@ ${availableSkills}
4780
4821
  };
4781
4822
  const createSkillsToolset = async (task, profile, promptContext) => {
4782
4823
  const approvalManager = new ApprovalManager(task, profile);
4824
+ const skillManager = task.getSkillManager();
4783
4825
  const generateActivateSkillDescription = async () => {
4784
- const globalSkillsDir = path.join(os.homedir(), AIDER_DESK_DIR, SKILLS_DIR_NAME);
4785
- const projectSkillsDir = path.join(task.getProjectDir(), AIDER_DESK_DIR, SKILLS_DIR_NAME);
4786
- const [globalSkills, projectSkills, builtinSkills] = await Promise.all([
4787
- loadSkillsFromDir(globalSkillsDir, "global"),
4788
- loadSkillsFromDir(projectSkillsDir, "project"),
4789
- loadSkillsFromDir(AIDER_DESK_BUILTIN_SKILLS_DIR, "builtin")
4790
- ]);
4791
- return getActivateSkillDescription([...projectSkills, ...globalSkills, ...builtinSkills]);
4826
+ const skills = await skillManager.loadAllSkills();
4827
+ return getActivateSkillDescription(skills);
4792
4828
  };
4793
4829
  const activateSkillTool = ai.tool({
4794
4830
  description: await generateActivateSkillDescription(),
@@ -4806,30 +4842,21 @@ const createSkillsToolset = async (task, profile, promptContext) => {
4806
4842
  if (!isApproved) {
4807
4843
  return `Activating skill denied by user. Reason: ${userInput}`;
4808
4844
  }
4809
- const globalSkillsDir = path.join(os.homedir(), AIDER_DESK_DIR, SKILLS_DIR_NAME);
4810
- const projectSkillsDir = path.join(task.getProjectDir(), AIDER_DESK_DIR, SKILLS_DIR_NAME);
4811
- const [globalSkills, projectSkills, builtinSkills] = await Promise.all([
4812
- loadSkillsFromDir(globalSkillsDir, "global"),
4813
- loadSkillsFromDir(projectSkillsDir, "project"),
4814
- loadSkillsFromDir(AIDER_DESK_BUILTIN_SKILLS_DIR, "builtin")
4815
- ]);
4816
- const allSkills = [...projectSkills, ...globalSkills, ...builtinSkills];
4845
+ const allSkills = await skillManager.loadAllSkills();
4817
4846
  const requested = allSkills.find((s) => s.name === skill);
4818
4847
  if (!requested) {
4819
4848
  const available = allSkills.map((s) => s.name).join(", ");
4820
4849
  return `Skill '${skill}' not found. Available skills: ${available || "(none)"}.`;
4821
4850
  }
4822
- const skillMdPath = path.join(requested.dirPath, SKILL_MARKDOWN_FILE);
4823
- let content;
4824
- try {
4825
- content = await fs.readFile(skillMdPath, "utf8");
4826
- } catch {
4827
- return `Failed to read skill content from ${skillMdPath}.`;
4851
+ const content = await skillManager.getSkillContent(skill);
4852
+ if (!content) {
4853
+ return `Skill '${requested.name}' has no content or dirPath.`;
4828
4854
  }
4855
+ const dirInfo = requested.dirPath ? `
4856
+ Skill directory is ${requested.dirPath} - use it as parent directory for relative paths mentioned in the skill description.` : "";
4829
4857
  return `${content}
4830
4858
 
4831
- Skill '${requested.name}' activated.
4832
- Skill directory is ${requested.dirPath} - use it as parent directory for relative paths mentioned in the skill description.`;
4859
+ Skill '${requested.name}' activated.${dirInfo}`;
4833
4860
  }
4834
4861
  });
4835
4862
  const allTools = {
@@ -5266,17 +5293,6 @@ class McpManager {
5266
5293
  }
5267
5294
  }
5268
5295
  }
5269
- const THINKING_RESPONSE_STAR_TAG = "---\n► **THINKING**\n";
5270
- const ANSWER_RESPONSE_START_TAG = "---\n► **ANSWER**\n";
5271
- const extractPromptContextFromToolResult = (toolResult) => {
5272
- if (toolResult && typeof toolResult === "object" && "promptContext" in toolResult) {
5273
- return toolResult.promptContext;
5274
- }
5275
- return void 0;
5276
- };
5277
- const findLastUserMessage = (messages) => {
5278
- return [...messages].reverse().find((msg) => msg.role === MessageRole.User);
5279
- };
5280
5296
  const extractReasoningMiddleware = function extractReasoningMiddleware2({
5281
5297
  tagName,
5282
5298
  separator = "\n"
@@ -6246,6 +6262,9 @@ ${fileList}`
6246
6262
  options.abortSignal = abortSignal;
6247
6263
  }
6248
6264
  const result = await toolDef.execute(effectiveInput, options);
6265
+ if (options.abortSignal?.aborted) {
6266
+ return result;
6267
+ }
6249
6268
  const toolFinishedExtensionResult = await this.extensionManager.dispatchEvent(
6250
6269
  "onToolFinished",
6251
6270
  { toolName, input: effectiveInput, output: result },
@@ -6295,6 +6314,14 @@ ${fileList}`
6295
6314
  }
6296
6315
  );
6297
6316
  logger.debug(`Tool ${toolDef.name} returned response`, { response });
6317
+ if (response && typeof response === "object" && "content" in response && Array.isArray(response.content)) {
6318
+ for (let i = 0; i < response.content.length; i++) {
6319
+ const part = response.content[i];
6320
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
6321
+ part.text = await truncateToolResult(part.text);
6322
+ }
6323
+ }
6324
+ }
6298
6325
  this.lastToolCallTime = Date.now();
6299
6326
  return response;
6300
6327
  } catch (error) {
@@ -6475,7 +6502,7 @@ ${fileList}`
6475
6502
  this.abortControllers.set(controllerId, newController);
6476
6503
  }
6477
6504
  const effectiveAbortSignal = abortSignal || (controllerId ? this.abortControllers.get(controllerId)?.signal : void 0);
6478
- const cacheControl = this.modelManager.getCacheControl(profile, provider.provider);
6505
+ const cacheControl = this.modelManager.getCacheControl(provider, modelName);
6479
6506
  const providerOptions = this.modelManager.getProviderOptions(provider, modelName);
6480
6507
  const providerParameters = this.modelManager.getProviderParameters(provider, modelName);
6481
6508
  const firstUserMessage = contextMessages.length > 0 ? contextMessages[0] : null;
@@ -7178,6 +7205,7 @@ ${fileList}`
7178
7205
  }
7179
7206
  const settings = this.store.getSettings();
7180
7207
  const model = await this.modelManager.createLlm(provider, modelName, settings, projectDir, void 0, systemPrompt, void 0);
7208
+ const cacheControl = this.modelManager.getCacheControl(provider, modelName);
7181
7209
  const providerOptions = this.modelManager.getProviderOptions(provider, modelName);
7182
7210
  const providerParameters = this.modelManager.getProviderParameters(provider, modelName);
7183
7211
  const controllerId = uuid.v4();
@@ -7202,7 +7230,7 @@ ${fileList}`
7202
7230
  const result = await ai.generateText({
7203
7231
  model,
7204
7232
  system: systemPrompt,
7205
- messages: await optimizeMessages(messages),
7233
+ messages: await optimizeMessages(messages, cacheControl),
7206
7234
  abortSignal: effectiveAbortSignal,
7207
7235
  providerOptions,
7208
7236
  ...providerParameters
@@ -7233,7 +7261,7 @@ ${fileList}`
7233
7261
  const messages = await this.prepareMessages(task, profile, await task.getContextMessages(), await task.getContextFiles());
7234
7262
  const toolSet = await this.getAvailableTools(task, "agent", profile, provider, profile.model);
7235
7263
  const systemPrompt = await this.promptsManager.getSystemPrompt(this.store.getSettings(), task, profile);
7236
- const cacheControl = this.modelManager.getCacheControl(profile, provider.provider);
7264
+ const cacheControl = this.modelManager.getCacheControl(provider, profile.model);
7237
7265
  const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
7238
7266
  const userRequestMessageIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
7239
7267
  const optimizedMessages = await optimizeMessages(
@@ -8467,9 +8495,10 @@ const UpdateOpenProjectsOrderSchema = zod.z.object({
8467
8495
  const LoadInputHistorySchema = zod.z.object({
8468
8496
  projectDir: zod.z.string().min(1, "Project directory is required")
8469
8497
  });
8470
- const RedoLastUserPromptSchema = zod.z.object({
8498
+ const RedoUserPromptSchema = zod.z.object({
8471
8499
  projectDir: zod.z.string().min(1, "Project directory is required"),
8472
8500
  taskId: zod.z.string().min(1, "Task id is required"),
8501
+ messageId: zod.z.string().min(1, "Message id is required"),
8473
8502
  mode: zod.z.string().min(1, "Mode is required"),
8474
8503
  updatedPrompt: zod.z.string().optional()
8475
8504
  });
@@ -8602,6 +8631,11 @@ const MergeWorktreeToMainSchema = zod.z.object({
8602
8631
  targetBranch: zod.z.string().optional(),
8603
8632
  commitMessage: zod.z.string().optional()
8604
8633
  });
8634
+ const MergeAndSwitchToLocalSchema = zod.z.object({
8635
+ projectDir: zod.z.string().min(1, "Project directory is required"),
8636
+ taskId: zod.z.string().min(1, "Task id is required"),
8637
+ targetBranch: zod.z.string().optional()
8638
+ });
8605
8639
  const ApplyUncommittedChangesSchema = zod.z.object({
8606
8640
  projectDir: zod.z.string().min(1, "Project directory is required"),
8607
8641
  taskId: zod.z.string().min(1, "Task id is required"),
@@ -8656,6 +8690,11 @@ const ResolveWorktreeConflictsWithAgentSchema = zod.z.object({
8656
8690
  projectDir: zod.z.string().min(1, "Project directory is required"),
8657
8691
  taskId: zod.z.string().min(1, "Task id is required")
8658
8692
  });
8693
+ const RenameWorktreeBranchSchema = zod.z.object({
8694
+ projectDir: zod.z.string().min(1, "Project directory is required"),
8695
+ taskId: zod.z.string().min(1, "Task id is required"),
8696
+ newBranchName: zod.z.string().min(1, "New branch name is required")
8697
+ });
8659
8698
  class ProjectApi extends BaseApi {
8660
8699
  constructor(eventsHandler) {
8661
8700
  super();
@@ -8684,13 +8723,13 @@ class ProjectApi extends BaseApi {
8684
8723
  router.post(
8685
8724
  "/project/redo-prompt",
8686
8725
  this.handleRequest(async (req, res) => {
8687
- const parsed = this.validateRequest(RedoLastUserPromptSchema, req.body, res);
8726
+ const parsed = this.validateRequest(RedoUserPromptSchema, req.body, res);
8688
8727
  if (!parsed) {
8689
8728
  return;
8690
8729
  }
8691
- const { projectDir, taskId, mode, updatedPrompt } = parsed;
8692
- await this.eventsHandler.redoLastUserPrompt(projectDir, taskId, mode, updatedPrompt);
8693
- res.status(200).json({ message: "Redo last user prompt initiated" });
8730
+ const { projectDir, taskId, messageId, mode, updatedPrompt } = parsed;
8731
+ await this.eventsHandler.redoUserPrompt(projectDir, taskId, messageId, mode, updatedPrompt);
8732
+ res.status(200).json({ message: "Redo user prompt initiated" });
8694
8733
  })
8695
8734
  );
8696
8735
  router.post(
@@ -9022,6 +9061,18 @@ class ProjectApi extends BaseApi {
9022
9061
  res.status(200).json({ message: "Worktree merged" });
9023
9062
  })
9024
9063
  );
9064
+ router.post(
9065
+ "/project/worktree/merge-and-switch-to-local",
9066
+ this.handleRequest(async (req, res) => {
9067
+ const parsed = this.validateRequest(MergeAndSwitchToLocalSchema, req.body, res);
9068
+ if (!parsed) {
9069
+ return;
9070
+ }
9071
+ const { projectDir, taskId, targetBranch } = parsed;
9072
+ await this.eventsHandler.mergeAndSwitchToLocal(projectDir, taskId, targetBranch);
9073
+ res.status(200).json({ message: "Worktree merged and switched to local" });
9074
+ })
9075
+ );
9025
9076
  router.post(
9026
9077
  "/project/worktree/apply-uncommitted",
9027
9078
  this.handleRequest(async (req, res) => {
@@ -9166,6 +9217,18 @@ class ProjectApi extends BaseApi {
9166
9217
  res.status(200).json({ message: "Conflicts resolved" });
9167
9218
  })
9168
9219
  );
9220
+ router.post(
9221
+ "/project/worktree/rename-branch",
9222
+ this.handleRequest(async (req, res) => {
9223
+ const parsed = this.validateRequest(RenameWorktreeBranchSchema, req.body, res);
9224
+ if (!parsed) {
9225
+ return;
9226
+ }
9227
+ const { projectDir, taskId, newBranchName } = parsed;
9228
+ await this.eventsHandler.renameWorktreeBranch(projectDir, taskId, newBranchName);
9229
+ res.status(200).json({ message: "Branch renamed" });
9230
+ })
9231
+ );
9169
9232
  router.post(
9170
9233
  "/project/update-order",
9171
9234
  this.handleRequest(async (req, res) => {
@@ -10119,6 +10182,80 @@ class ExtensionsApi extends BaseApi {
10119
10182
  );
10120
10183
  }
10121
10184
  }
10185
+ const GetSkillsSchema = zod.z.object({
10186
+ projectDir: zod.z.string().min(1, "Project directory is required"),
10187
+ taskId: zod.z.string().min(1, "Task ID is required")
10188
+ });
10189
+ const ActivateSkillSchema = zod.z.object({
10190
+ projectDir: zod.z.string().min(1, "Project directory is required"),
10191
+ taskId: zod.z.string().min(1, "Task ID is required"),
10192
+ skillName: zod.z.string().min(1, "Skill name is required")
10193
+ });
10194
+ class SkillsApi extends BaseApi {
10195
+ constructor(eventsHandler) {
10196
+ super();
10197
+ this.eventsHandler = eventsHandler;
10198
+ }
10199
+ registerRoutes(router) {
10200
+ router.get(
10201
+ "/skills",
10202
+ this.handleRequest(async (req, res) => {
10203
+ const parsed = this.validateRequest(GetSkillsSchema, req.query, res);
10204
+ if (!parsed) {
10205
+ return;
10206
+ }
10207
+ const { projectDir, taskId } = parsed;
10208
+ const skills = await this.eventsHandler.getSkills(projectDir, taskId);
10209
+ res.status(200).json(skills);
10210
+ })
10211
+ );
10212
+ router.post(
10213
+ "/skills/activate",
10214
+ this.handleRequest(async (req, res) => {
10215
+ const parsed = this.validateRequest(ActivateSkillSchema, req.body, res);
10216
+ if (!parsed) {
10217
+ return;
10218
+ }
10219
+ const { projectDir, taskId, skillName } = parsed;
10220
+ await this.eventsHandler.activateSkill(projectDir, taskId, skillName);
10221
+ res.status(200).json({ success: true });
10222
+ })
10223
+ );
10224
+ router.post(
10225
+ "/skills/deactivate",
10226
+ this.handleRequest(async (req, res) => {
10227
+ const parsed = this.validateRequest(ActivateSkillSchema, req.body, res);
10228
+ if (!parsed) {
10229
+ return;
10230
+ }
10231
+ const { projectDir, taskId, skillName } = parsed;
10232
+ await this.eventsHandler.deactivateSkill(projectDir, taskId, skillName);
10233
+ res.status(200).json({ success: true });
10234
+ })
10235
+ );
10236
+ }
10237
+ }
10238
+ const createCorsOriginValidator = (store) => (origin, callback) => {
10239
+ if (CORS_ALLOWED_ORIGINS) {
10240
+ const origins = CORS_ALLOWED_ORIGINS.split(",").map((o) => o.trim()).filter(Boolean);
10241
+ if (!origin || origins.includes(origin)) {
10242
+ callback(null, true);
10243
+ } else {
10244
+ callback(null, false);
10245
+ }
10246
+ return;
10247
+ }
10248
+ const corsSettings = store.getSettings().server.cors;
10249
+ if (!corsSettings.enabled || corsSettings.origins.length === 0) {
10250
+ callback(null, false);
10251
+ return;
10252
+ }
10253
+ if (!origin || corsSettings.origins.includes(origin)) {
10254
+ callback(null, true);
10255
+ } else {
10256
+ callback(null, false);
10257
+ }
10258
+ };
10122
10259
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
10123
10260
  class ServerController {
10124
10261
  constructor(server, projectManager, eventsHandler, store, pythonInstaller) {
@@ -10191,11 +10328,15 @@ class ServerController {
10191
10328
  new VoiceApi(this.eventsHandler).registerRoutes(apiRouter);
10192
10329
  new TerminalApi(this.eventsHandler).registerRoutes(apiRouter);
10193
10330
  new ExtensionsApi(this.eventsHandler).registerRoutes(apiRouter);
10331
+ new SkillsApi(this.eventsHandler).registerRoutes(apiRouter);
10194
10332
  this.app.use("/api", apiRouter);
10195
10333
  }
10334
+ setupCors() {
10335
+ this.app.use(cors({ origin: createCorsOriginValidator(this.store) }));
10336
+ }
10196
10337
  init() {
10197
10338
  this.app.use(express.json({ limit: "50mb" }));
10198
- this.app.use(cors());
10339
+ this.setupCors();
10199
10340
  this.app.use(this.serverGuardMiddleware.bind(this));
10200
10341
  this.app.use(this.timeoutMiddleware.bind(this));
10201
10342
  this.app.use(this.basicAuthMiddleware.bind(this));
@@ -10462,9 +10603,10 @@ const isUnsubscribeEventsMessage = (message) => {
10462
10603
  return message.action === "unsubscribe-events";
10463
10604
  };
10464
10605
  class ConnectorManager {
10465
- constructor(httpServer, projectManager, eventManager) {
10606
+ constructor(httpServer, projectManager, eventManager, store) {
10466
10607
  this.projectManager = projectManager;
10467
10608
  this.eventManager = eventManager;
10609
+ this.store = store;
10468
10610
  this.init(httpServer);
10469
10611
  }
10470
10612
  io = null;
@@ -10472,7 +10614,7 @@ class ConnectorManager {
10472
10614
  init(httpServer) {
10473
10615
  this.io = new socket_io.Server(httpServer, {
10474
10616
  cors: {
10475
- origin: "*",
10617
+ origin: createCorsOriginValidator(this.store),
10476
10618
  methods: ["GET", "POST"]
10477
10619
  },
10478
10620
  pingTimeout: 6e5,
@@ -11306,6 +11448,15 @@ class ContextManager {
11306
11448
  }
11307
11449
  return this.removeByToolCallId(messageId);
11308
11450
  }
11451
+ removeMessagesByIds(ids) {
11452
+ for (const id of ids) {
11453
+ const index = this.messages.findIndex((msg) => msg.id === id);
11454
+ if (index !== -1) {
11455
+ this.messages.splice(index, 1);
11456
+ }
11457
+ }
11458
+ this.autosave();
11459
+ }
11309
11460
  removeMessageByIndex(index, messageId) {
11310
11461
  const removedIds = [];
11311
11462
  removedIds.push(messageId);
@@ -11435,16 +11586,17 @@ class ContextManager {
11435
11586
  }
11436
11587
  this.autosave();
11437
11588
  }
11438
- removeMessagesUpToLastUserMessage() {
11439
- const lastUserMessageIndex = this.messages.findLastIndex((msg) => msg.role === MessageRole.User);
11440
- if (lastUserMessageIndex === -1) {
11441
- logger.warn("No user message found to remove up to.", {
11442
- taskId: this.taskId
11589
+ removeMessagesUpToUserMessage(messageId) {
11590
+ const userMessageIndex = this.messages.findIndex((msg) => msg.id === messageId && msg.role === MessageRole.User);
11591
+ if (userMessageIndex === -1) {
11592
+ logger.warn("No user message found with the given ID to remove up to.", {
11593
+ taskId: this.taskId,
11594
+ messageId
11443
11595
  });
11444
11596
  return [];
11445
11597
  }
11446
- const removedMessages = this.messages.splice(lastUserMessageIndex);
11447
- logger.debug(`Task ${this.taskId}: Removed ${removedMessages.length} messages up to last user message. Total messages: ${this.messages.length}`);
11598
+ const removedMessages = this.messages.splice(userMessageIndex);
11599
+ logger.debug(`Task ${this.taskId}: Removed ${removedMessages.length} messages up to user message ${messageId}. Total messages: ${this.messages.length}`);
11448
11600
  this.autosave();
11449
11601
  return removedMessages;
11450
11602
  }
@@ -12584,6 +12736,156 @@ class AiderManager {
12584
12736
  connectors.filter((connector) => connector.listenTo.includes("update-env-vars")).forEach((connector) => connector.sendUpdateEnvVarsMessage(environmentVariables));
12585
12737
  }
12586
12738
  }
12739
+ const SKILLS_DIR_NAME = "skills";
12740
+ const SKILL_MARKDOWN_FILE = "SKILL.md";
12741
+ const TOOL_NAME = `${SKILLS_TOOL_GROUP_NAME}${TOOL_GROUP_NAME_SEPARATOR}${SKILLS_TOOL_ACTIVATE_SKILL}`;
12742
+ const parseSkillFrontMatter = (markdown) => {
12743
+ const parsed = yamlFrontMatter.loadFront(markdown);
12744
+ const name = typeof parsed.name === "string" ? parsed.name : void 0;
12745
+ const description = typeof parsed.description === "string" ? parsed.description : void 0;
12746
+ if (!name || !description) {
12747
+ return null;
12748
+ }
12749
+ return { name, description };
12750
+ };
12751
+ const safeReadDir = async (dirPath) => {
12752
+ try {
12753
+ return await fs.readdir(dirPath);
12754
+ } catch {
12755
+ return [];
12756
+ }
12757
+ };
12758
+ const safeStat = async (filePath) => {
12759
+ try {
12760
+ return await fs.stat(filePath);
12761
+ } catch {
12762
+ return null;
12763
+ }
12764
+ };
12765
+ const loadSkillsFromDir = async (skillsRootDir, location) => {
12766
+ const entries = await safeReadDir(skillsRootDir);
12767
+ const skills = [];
12768
+ for (const entry of entries) {
12769
+ const dirPath = path.join(skillsRootDir, entry);
12770
+ const stat = await safeStat(dirPath);
12771
+ if (!stat?.isDirectory()) {
12772
+ continue;
12773
+ }
12774
+ const skillMdPath = path.join(dirPath, SKILL_MARKDOWN_FILE);
12775
+ const skillMdStat = await safeStat(skillMdPath);
12776
+ if (!skillMdStat?.isFile()) {
12777
+ continue;
12778
+ }
12779
+ let markdown;
12780
+ try {
12781
+ markdown = await fs.readFile(skillMdPath, "utf8");
12782
+ } catch {
12783
+ continue;
12784
+ }
12785
+ const parsed = parseSkillFrontMatter(markdown);
12786
+ if (!parsed) {
12787
+ continue;
12788
+ }
12789
+ skills.push({
12790
+ name: parsed.name,
12791
+ description: parsed.description,
12792
+ location,
12793
+ dirPath
12794
+ });
12795
+ }
12796
+ return skills;
12797
+ };
12798
+ class SkillManager {
12799
+ extensionManager;
12800
+ projectDir;
12801
+ constructor(projectDir, extensionManager) {
12802
+ this.projectDir = projectDir;
12803
+ this.extensionManager = extensionManager;
12804
+ }
12805
+ async loadAllSkills() {
12806
+ const globalSkillsDir = path.join(os.homedir(), AIDER_DESK_DIR, SKILLS_DIR_NAME);
12807
+ const projectSkillsDir = path.join(this.projectDir, AIDER_DESK_DIR, SKILLS_DIR_NAME);
12808
+ const extensionSkills = this.extensionManager ? this.extensionManager.getSkills({ baseDir: this.projectDir }, { getTaskDir: () => this.projectDir }) : [];
12809
+ const [globalSkills, projectSkills, builtinSkills] = await Promise.all([
12810
+ loadSkillsFromDir(globalSkillsDir, "global"),
12811
+ loadSkillsFromDir(projectSkillsDir, "project"),
12812
+ loadSkillsFromDir(AIDER_DESK_BUILTIN_SKILLS_DIR, "builtin")
12813
+ ]);
12814
+ return [...projectSkills, ...globalSkills, ...builtinSkills, ...extensionSkills];
12815
+ }
12816
+ async getSkills(contextMessages) {
12817
+ const skills = await this.loadAllSkills();
12818
+ const activatedSkillNames = contextMessages ? this.getActivatedSkillNames(contextMessages) : /* @__PURE__ */ new Set();
12819
+ return skills.map((skill) => ({
12820
+ ...skill,
12821
+ activated: activatedSkillNames.has(skill.name)
12822
+ }));
12823
+ }
12824
+ async getSkillContent(skillName) {
12825
+ const skills = await this.loadAllSkills();
12826
+ const skill = skills.find((s) => s.name === skillName);
12827
+ if (!skill) {
12828
+ return null;
12829
+ }
12830
+ if (skill.content) {
12831
+ return skill.content;
12832
+ }
12833
+ if (skill.dirPath) {
12834
+ const skillMdPath = path.join(skill.dirPath, SKILL_MARKDOWN_FILE);
12835
+ try {
12836
+ return await fs.readFile(skillMdPath, "utf8");
12837
+ } catch {
12838
+ return null;
12839
+ }
12840
+ }
12841
+ return null;
12842
+ }
12843
+ getActivatedSkillNames(contextMessages) {
12844
+ const activatedNames = /* @__PURE__ */ new Set();
12845
+ for (const message of contextMessages) {
12846
+ if (message.role === "assistant" && Array.isArray(message.content)) {
12847
+ for (const part of message.content) {
12848
+ if (part.type === "tool-call" && part.toolName === TOOL_NAME && part.input?.skill) {
12849
+ activatedNames.add(part.input.skill);
12850
+ }
12851
+ }
12852
+ }
12853
+ }
12854
+ return activatedNames;
12855
+ }
12856
+ buildActivateSkillMessages(skillName, content) {
12857
+ const toolCallId = uuid.v4();
12858
+ const assistantMessage = {
12859
+ id: uuid.v4(),
12860
+ role: "assistant",
12861
+ content: [
12862
+ {
12863
+ type: "text",
12864
+ text: "User requested the skill activation."
12865
+ },
12866
+ {
12867
+ type: "tool-call",
12868
+ toolCallId,
12869
+ toolName: TOOL_NAME,
12870
+ input: { skill: skillName }
12871
+ }
12872
+ ]
12873
+ };
12874
+ const toolMessage = {
12875
+ id: uuid.v4(),
12876
+ role: "tool",
12877
+ content: [
12878
+ {
12879
+ type: "tool-result",
12880
+ toolCallId,
12881
+ toolName: TOOL_NAME,
12882
+ output: { type: "text", value: content }
12883
+ }
12884
+ ]
12885
+ };
12886
+ return [assistantMessage, toolMessage];
12887
+ }
12888
+ }
12587
12889
  class GitError extends Error {
12588
12890
  name = "GitError";
12589
12891
  gitCommands;
@@ -12661,7 +12963,7 @@ class WorktreeManager {
12661
12963
  await execWithShellPath('git commit -m "Initial commit" --allow-empty', { cwd: projectPath });
12662
12964
  }
12663
12965
  let baseCommit;
12664
- let actualBaseBranch;
12966
+ let newBranchName;
12665
12967
  const baseRef = baseBranch;
12666
12968
  if (branch) {
12667
12969
  let branchExists = false;
@@ -12675,7 +12977,7 @@ class WorktreeManager {
12675
12977
  baseCommit = (await execWithShellPath(`git rev-parse ${branch}`, {
12676
12978
  cwd: projectPath
12677
12979
  })).stdout.trim();
12678
- actualBaseBranch = branch;
12980
+ newBranchName = branch;
12679
12981
  } else {
12680
12982
  if (baseBranch !== "HEAD") {
12681
12983
  try {
@@ -12688,7 +12990,7 @@ class WorktreeManager {
12688
12990
  cwd: projectPath
12689
12991
  })).stdout.trim();
12690
12992
  await execWithShellPath(`git worktree add -b ${branch} "${worktreePath}" ${baseRef}`, { cwd: projectPath });
12691
- actualBaseBranch = branch;
12993
+ newBranchName = branch;
12692
12994
  }
12693
12995
  } else {
12694
12996
  await execWithShellPath(`git worktree add "${worktreePath}" ${baseRef}`, { cwd: projectPath });
@@ -12697,25 +12999,38 @@ class WorktreeManager {
12697
12999
  })).stdout.trim();
12698
13000
  if (baseRef === "HEAD") {
12699
13001
  try {
12700
- actualBaseBranch = (await execWithShellPath("git rev-parse --abbrev-ref HEAD", {
13002
+ newBranchName = (await execWithShellPath("git rev-parse --abbrev-ref HEAD", {
12701
13003
  cwd: projectPath
12702
13004
  })).stdout.trim();
12703
- if (actualBaseBranch === "HEAD") {
12704
- actualBaseBranch = "DETACHED HEAD";
13005
+ if (newBranchName === "HEAD") {
13006
+ newBranchName = "DETACHED HEAD";
12705
13007
  }
12706
13008
  } catch {
12707
- actualBaseBranch = "DETACHED HEAD";
13009
+ newBranchName = "DETACHED HEAD";
12708
13010
  }
12709
13011
  } else {
12710
- actualBaseBranch = `${baseRef} (DETACHED)`;
13012
+ newBranchName = `${baseRef} (DETACHED)`;
12711
13013
  }
12712
13014
  logger.info(`Worktree created in DETACHED HEAD mode from commit: ${baseCommit}`);
12713
13015
  }
13016
+ let resolvedBaseBranch;
13017
+ if (baseRef === "HEAD") {
13018
+ try {
13019
+ const result = await execWithShellPath("git rev-parse --abbrev-ref HEAD", { cwd: projectPath });
13020
+ const headBranch = result.stdout.trim();
13021
+ resolvedBaseBranch = headBranch !== "HEAD" ? headBranch : void 0;
13022
+ } catch {
13023
+ resolvedBaseBranch = void 0;
13024
+ }
13025
+ } else {
13026
+ resolvedBaseBranch = baseRef;
13027
+ }
12714
13028
  logger.info(`Worktree created successfully at: ${worktreePath}`);
12715
13029
  return {
12716
13030
  path: worktreePath,
12717
13031
  baseCommit,
12718
- baseBranch: actualBaseBranch
13032
+ baseBranch: resolvedBaseBranch,
13033
+ branch: newBranchName
12719
13034
  };
12720
13035
  } catch (error) {
12721
13036
  logger.error("Failed to create worktree:", error);
@@ -12723,6 +13038,29 @@ class WorktreeManager {
12723
13038
  }
12724
13039
  });
12725
13040
  }
13041
+ async renameBranch(projectPath, oldBranch, newBranch) {
13042
+ try {
13043
+ const finalBranchName = await this.findUniqueBranchName(projectPath, newBranch);
13044
+ await execWithShellPath(`git branch -m ${oldBranch} ${finalBranchName}`, { cwd: projectPath });
13045
+ logger.info(`Renamed branch: ${oldBranch} -> ${finalBranchName}`);
13046
+ return finalBranchName;
13047
+ } catch (error) {
13048
+ logger.warn(`Failed to rename branch ${oldBranch} to ${newBranch}:`, error);
13049
+ return newBranch;
13050
+ }
13051
+ }
13052
+ async findUniqueBranchName(projectPath, baseName) {
13053
+ const existingBranches = await this.listBranches(projectPath);
13054
+ const branchNames = new Set(existingBranches.map((b) => b.name));
13055
+ if (!branchNames.has(baseName)) {
13056
+ return baseName;
13057
+ }
13058
+ let suffix = 2;
13059
+ while (branchNames.has(`${baseName}-${suffix}`)) {
13060
+ suffix++;
13061
+ }
13062
+ return `${baseName}-${suffix}`;
13063
+ }
12726
13064
  async createSymlinks(projectPath, worktreePath, folderNames) {
12727
13065
  if (folderNames.length === 0) {
12728
13066
  logger.debug("No symlink folders configured, skipping symlink creation");
@@ -12776,14 +13114,14 @@ class WorktreeManager {
12776
13114
  return await withLock(`worktree-remove-${projectDir}-${worktree.path}`, async () => {
12777
13115
  try {
12778
13116
  await execWithShellPath(`git worktree remove "${worktree.path}" --force`, { cwd: projectDir });
12779
- if (worktree.baseBranch) {
13117
+ if (worktree.branch) {
12780
13118
  try {
12781
- await execWithShellPath(`git branch -D ${worktree.baseBranch}`, {
13119
+ await execWithShellPath(`git branch -D ${worktree.branch}`, {
12782
13120
  cwd: projectDir
12783
13121
  });
12784
- logger.info(`Deleted task branch: ${worktree.baseBranch}`);
13122
+ logger.info(`Deleted task branch: ${worktree.branch}`);
12785
13123
  } catch (error) {
12786
- logger.debug(`Could not delete branch ${worktree.baseBranch}:`, error);
13124
+ logger.debug(`Could not delete branch ${worktree.branch}:`, error);
12787
13125
  }
12788
13126
  }
12789
13127
  } catch (error) {
@@ -12810,12 +13148,12 @@ class WorktreeManager {
12810
13148
  ...currentWorktree
12811
13149
  });
12812
13150
  }
12813
- currentWorktree = { path: line.substring(9), baseBranch: "" };
13151
+ currentWorktree = { path: line.substring(9), branch: "" };
12814
13152
  } else if (line.startsWith("branch ")) {
12815
13153
  currentWorktree = {
12816
13154
  ...currentWorktree || {},
12817
13155
  path: currentWorktree ? currentWorktree.path : "",
12818
- baseBranch: line.substring(7).replace("refs/heads/", "")
13156
+ branch: line.substring(7).replace("refs/heads/", "")
12819
13157
  };
12820
13158
  } else if (line.startsWith("HEAD ")) {
12821
13159
  currentWorktree = {
@@ -12827,7 +13165,7 @@ class WorktreeManager {
12827
13165
  currentWorktree = {
12828
13166
  ...currentWorktree || {},
12829
13167
  path: currentWorktree ? currentWorktree.path : "",
12830
- baseBranch: void 0
13168
+ branch: void 0
12831
13169
  };
12832
13170
  } else if (line.startsWith("prunable")) {
12833
13171
  currentWorktree = {
@@ -12856,7 +13194,7 @@ class WorktreeManager {
12856
13194
  cwd: projectPath
12857
13195
  });
12858
13196
  const worktrees = await this.listWorktrees(projectPath);
12859
- const worktreeBranches = new Set(worktrees.map((w) => w.baseBranch));
13197
+ const worktreeBranches = new Set(worktrees.map((w) => w.branch));
12860
13198
  const branches = [];
12861
13199
  const lines = branchOutput.split("\n").filter((line) => line.trim());
12862
13200
  for (const line of lines) {
@@ -12910,6 +13248,22 @@ class WorktreeManager {
12910
13248
  logger.warn("getEffectiveMainBranch is deprecated, use getProjectMainBranch instead");
12911
13249
  return await this.getProjectMainBranch(project.path);
12912
13250
  }
13251
+ async getHeadCommit(worktreePath) {
13252
+ try {
13253
+ const { stdout } = await execWithShellPath("git rev-parse HEAD", { cwd: worktreePath });
13254
+ return stdout.trim();
13255
+ } catch {
13256
+ return void 0;
13257
+ }
13258
+ }
13259
+ async getBranchesContainingCommit(projectPath, commit) {
13260
+ try {
13261
+ const { stdout } = await execWithShellPath(`git branch --contains ${commit} --format='%(refname:short)'`, { cwd: projectPath });
13262
+ return stdout.trim().split("\n").map((b) => b.trim()).filter((b) => b);
13263
+ } catch {
13264
+ return [];
13265
+ }
13266
+ }
12913
13267
  async hasChangesToRebase(worktreePath, mainBranch) {
12914
13268
  try {
12915
13269
  let stdout = "0";
@@ -12991,7 +13345,7 @@ class WorktreeManager {
12991
13345
  };
12992
13346
  }
12993
13347
  }
12994
- async rebaseMainIntoWorktree(worktreePath, mainBranch) {
13348
+ async rebaseMainIntoWorktree(worktreePath, mainBranch, baseCommit) {
12995
13349
  return await withLock(`git-rebase-${worktreePath}`, async () => {
12996
13350
  const executedCommands = [];
12997
13351
  let lastOutput = "";
@@ -13010,7 +13364,7 @@ class WorktreeManager {
13010
13364
  });
13011
13365
  logger.info("Created temporary commit for uncommitted changes");
13012
13366
  }
13013
- const command = `git rebase ${mainBranch}`;
13367
+ const command = baseCommit ? `git rebase --onto ${mainBranch} ${baseCommit}` : `git rebase ${mainBranch}`;
13014
13368
  executedCommands.push(`${command} (in ${worktreePath})`);
13015
13369
  const rebaseResult = await execWithShellPath(command, {
13016
13370
  cwd: worktreePath
@@ -13085,7 +13439,7 @@ class WorktreeManager {
13085
13439
  }
13086
13440
  });
13087
13441
  }
13088
- async squashAndMergeWorktreeToMain(projectPath, worktreePath, mainBranch, commitMessage) {
13442
+ async squashAndMergeWorktreeToMain(projectPath, worktreePath, mainBranch, commitMessage, baseCommit) {
13089
13443
  const executedCommands = [];
13090
13444
  let lastOutput = "";
13091
13445
  try {
@@ -13101,8 +13455,8 @@ class WorktreeManager {
13101
13455
  if (!commits.trim()) {
13102
13456
  return;
13103
13457
  }
13104
- command = `git rebase ${mainBranch}`;
13105
- executedCommands.push(`git rebase ${mainBranch} (in ${worktreePath})`);
13458
+ command = baseCommit ? `git rebase --onto ${mainBranch} ${baseCommit}` : `git rebase ${mainBranch}`;
13459
+ executedCommands.push(`${command} (in ${worktreePath})`);
13106
13460
  try {
13107
13461
  const rebaseWorktreeResult = await execWithShellPath(command, {
13108
13462
  cwd: worktreePath
@@ -13411,10 +13765,12 @@ Git output: ${err.stderr || err.stdout || err.message}`
13411
13765
  */
13412
13766
  async hasUncommittedChanges(path2) {
13413
13767
  try {
13414
- const { stdout } = await execWithShellPath("git status --porcelain=v1", {
13768
+ const { stdout } = await execWithShellPath("git status --porcelain=v1 -z", {
13415
13769
  cwd: path2
13416
13770
  });
13417
- return stdout.trim().length > 0;
13771
+ const filePaths = this.parseGitStatusEntries(stdout);
13772
+ const realChanges = filePaths.filter((filePath) => !this.isSymlinkPath(path2, filePath));
13773
+ return realChanges.length > 0;
13418
13774
  } catch (error) {
13419
13775
  logger.error("Failed to check for uncommitted changes:", error);
13420
13776
  return false;
@@ -13506,7 +13862,7 @@ Git output: ${err.stderr || err.stdout || err.message}`
13506
13862
  * Merge worktree to main branch with uncommitted changes support
13507
13863
  * Returns MergeState for potential revert
13508
13864
  */
13509
- async mergeWorktreeToMainWithUncommitted(projectPath, taskId, worktreePath, squash, commitMessage, targetBranch, symlinkFolders = []) {
13865
+ async mergeWorktreeToMainWithUncommitted(projectPath, taskId, worktreePath, squash, commitMessage, targetBranch, symlinkFolders = [], baseCommit) {
13510
13866
  return await withLock(`git-merge-worktree-${worktreePath}`, async () => {
13511
13867
  const timestamp = Date.now();
13512
13868
  const worktreeStashId = `worktree-${taskId.length > 24 ? taskId.substring(24) : taskId}-merge-${timestamp}`;
@@ -13534,7 +13890,7 @@ Git output: ${err.stderr || err.stdout || err.message}`
13534
13890
  if (!commitMessage) {
13535
13891
  throw new Error("Commit message is required for squash merge");
13536
13892
  }
13537
- await this.squashAndMergeWorktreeToMain(projectPath, worktreePath, mainBranch, commitMessage);
13893
+ await this.squashAndMergeWorktreeToMain(projectPath, worktreePath, mainBranch, commitMessage, baseCommit);
13538
13894
  } else {
13539
13895
  await this.mergeWorktreeToMain(projectPath, worktreePath, mainBranch);
13540
13896
  }
@@ -13588,10 +13944,15 @@ Git output: ${err.stderr || err.stdout || err.message}`
13588
13944
  * Check if worktree has uncommitted changes or unmerged commits
13589
13945
  * Returns information about unsaved work in the worktree
13590
13946
  */
13591
- async checkWorktreeForUnmergedWork(projectPath, worktreePath, targetBranch) {
13947
+ async checkWorktreeForUnmergedWork(projectPath, worktreePath, targetBranch, symlinkFolders = []) {
13592
13948
  try {
13593
- const hasUncommittedChanges = await this.hasUncommittedChanges(worktreePath);
13594
13949
  const { files: uncommittedFiles } = await this.getUncommittedFiles(worktreePath);
13950
+ const filteredUncommittedFiles = uncommittedFiles.filter((file) => {
13951
+ const pathPart = file.replace(/^..\s*/, "").trim();
13952
+ const normalizedPath = pathPart.replace(/\\/g, "/");
13953
+ return !symlinkFolders.some((folder) => normalizedPath.startsWith(`${folder}/`) || normalizedPath === folder);
13954
+ });
13955
+ const actualHasUncommittedChanges = filteredUncommittedFiles.length > 0;
13595
13956
  const effectiveTargetBranch = targetBranch || await this.getProjectMainBranch(projectPath);
13596
13957
  let unmergedCommitCount = 0;
13597
13958
  let unmergedCommits = [];
@@ -13605,11 +13966,11 @@ Git output: ${err.stderr || err.stdout || err.message}`
13605
13966
  }
13606
13967
  const hasUnmergedCommits = unmergedCommitCount > 0;
13607
13968
  return {
13608
- hasUncommittedChanges,
13969
+ hasUncommittedChanges: actualHasUncommittedChanges,
13609
13970
  hasUnmergedCommits,
13610
13971
  unmergedCommitCount,
13611
13972
  unmergedCommits,
13612
- uncommittedFiles
13973
+ uncommittedFiles: filteredUncommittedFiles
13613
13974
  };
13614
13975
  } catch (error) {
13615
13976
  logger.error("Failed to check worktree for unmerged work:", error);
@@ -13634,12 +13995,25 @@ Git output: ${err.stderr || err.stdout || err.message}`
13634
13995
  const { stdout } = await execWithShellPath("git status --porcelain=v1 -z", {
13635
13996
  cwd: worktreePath
13636
13997
  });
13637
- const files = stdout.split("\0").map((l) => l.trim()).filter((l) => l.length > 0);
13998
+ const filePaths = this.parseGitStatusEntries(stdout);
13999
+ const files = filePaths.filter((filePath) => !this.isSymlinkPath(worktreePath, filePath));
13638
14000
  return {
13639
14001
  count: files.length,
13640
14002
  files: Array.from(new Set(files))
13641
14003
  };
13642
14004
  }
14005
+ parseGitStatusEntries(stdout) {
14006
+ return stdout.split("\0").map((entry) => entry.substring(3).trim()).filter((filePath) => filePath.length > 0);
14007
+ }
14008
+ isSymlinkPath(cwd, filePath) {
14009
+ try {
14010
+ const fullPath = path.join(cwd, filePath);
14011
+ const stat = fs$1.lstatSync(fullPath);
14012
+ return stat.isSymbolicLink();
14013
+ } catch {
14014
+ return false;
14015
+ }
14016
+ }
13643
14017
  /**
13644
14018
  * Get updated files with per-commit and uncommitted diffs.
13645
14019
  *
@@ -14282,6 +14656,7 @@ class Task {
14282
14656
  };
14283
14657
  this.taskDataPath = path.join(this.project.baseDir, AIDER_DESK_TASKS_DIR, this.taskId, "settings.json");
14284
14658
  this.contextManager = new ContextManager(this, this.taskId);
14659
+ this.skillManager = new SkillManager(project.baseDir, extensionManager);
14285
14660
  this.agent = new Agent(
14286
14661
  this.store,
14287
14662
  this.agentProfileManager,
@@ -14327,6 +14702,7 @@ class Task {
14327
14702
  contextManager;
14328
14703
  agent;
14329
14704
  aiderManager;
14705
+ skillManager;
14330
14706
  task;
14331
14707
  async getTaskAgentProfile() {
14332
14708
  let agentProfileId = this.task.agentProfileId;
@@ -14380,10 +14756,56 @@ class Task {
14380
14756
  * Generate a branch name from task name (first 7 words, separated by '-')
14381
14757
  */
14382
14758
  generateBranchName() {
14759
+ const settings = this.store.getSettings();
14760
+ const branchPrefix = settings.taskSettings.worktreeBranchPrefix || WORKTREE_BRANCH_PREFIX;
14383
14761
  const words = this.task.name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((word) => word.length > 0).slice(0, 7);
14384
14762
  const branchName = words.join("-");
14385
14763
  const cleanBranchName = branchName.replace(/^[.-]+/, "").replace(/-+/g, "-").replace(/-$/, "");
14386
- return `${WORKTREE_BRANCH_PREFIX}${cleanBranchName || this.taskId}`;
14764
+ const fallbackId = /^[0-9a-f]{8}-/i.test(this.taskId) ? this.taskId.split("-")[0] : this.taskId;
14765
+ return `${branchPrefix}${cleanBranchName || fallbackId}`;
14766
+ }
14767
+ async renameWorktreeBranchIfNeeded() {
14768
+ if (this.task.workingMode !== "worktree" || !this.task.worktree?.branch) {
14769
+ return;
14770
+ }
14771
+ const settings = this.store.getSettings();
14772
+ if (!settings.taskSettings.renameBranchOnNameGeneration) {
14773
+ return;
14774
+ }
14775
+ const oldBranch = this.task.worktree.branch;
14776
+ const newBranch = this.generateBranchName();
14777
+ if (oldBranch === newBranch) {
14778
+ return;
14779
+ }
14780
+ await this.renameWorktreeBranch(newBranch);
14781
+ }
14782
+ /**
14783
+ * @deprecated This migration ensures older task data has the `branch` field
14784
+ * and `baseBranch` stores the branch the worktree was created from.
14785
+ * Can be removed once all users have migrated past v0.64.0.
14786
+ */
14787
+ async migrateWorktreeData() {
14788
+ if (!this.task.worktree || this.task.worktree.branch) {
14789
+ return;
14790
+ }
14791
+ const currentBranch = this.task.worktree.baseBranch;
14792
+ this.task.worktree.branch = currentBranch;
14793
+ let resolvedBase = "";
14794
+ if (this.task.worktree.baseCommit) {
14795
+ const branches = await this.worktreeManager.getBranchesContainingCommit(this.project.baseDir, this.task.worktree.baseCommit);
14796
+ if (branches.length === 1) {
14797
+ resolvedBase = branches[0];
14798
+ }
14799
+ }
14800
+ if (!resolvedBase) {
14801
+ try {
14802
+ resolvedBase = await this.worktreeManager.getProjectMainBranch(this.project.baseDir);
14803
+ } catch {
14804
+ resolvedBase = "";
14805
+ }
14806
+ }
14807
+ this.task.worktree.baseBranch = resolvedBase || void 0;
14808
+ await this.saveTask({ worktree: this.task.worktree });
14387
14809
  }
14388
14810
  isInternal() {
14389
14811
  return this.taskId === INTERNAL_TASK_ID;
@@ -14459,14 +14881,18 @@ class Task {
14459
14881
  worktreePath: this.task.worktree.path
14460
14882
  });
14461
14883
  } else {
14462
- const branchName = this.generateBranchName();
14463
- this.task.worktree = await this.worktreeManager.createWorktree(this.project.baseDir, this.taskId, branchName);
14884
+ await this.initWorktree();
14464
14885
  void this.sendUpdatedFilesUpdated();
14886
+ void this.sendWorktreeIntegrationStatusUpdated();
14465
14887
  }
14466
14888
  } else if (workingMode === "local") {
14467
14889
  if (existingWorktree) {
14468
- await this.worktreeManager.removeWorktree(this.project.baseDir, existingWorktree);
14890
+ const isShared = this.project.isWorktreeSharedWithOtherTasks(existingWorktree.path, this.taskId);
14891
+ if (!isShared) {
14892
+ await this.worktreeManager.removeWorktree(this.project.baseDir, existingWorktree);
14893
+ }
14469
14894
  void this.sendUpdatedFilesUpdated();
14895
+ void this.sendWorktreeIntegrationStatusUpdated();
14470
14896
  }
14471
14897
  } else {
14472
14898
  logger.debug("Empty workingMode, setting to local", {
@@ -14483,6 +14909,7 @@ class Task {
14483
14909
  this.task.workingMode = "local";
14484
14910
  }
14485
14911
  }
14912
+ await this.migrateWorktreeData();
14486
14913
  if (await fileExists(this.getTaskDir())) {
14487
14914
  this.git = simpleGit.simpleGit(this.getTaskDir());
14488
14915
  }
@@ -14857,7 +15284,9 @@ class Task {
14857
15284
  await this.saveTask({
14858
15285
  name: this.task.name || this.getTaskNameFromPrompt(prompt || ""),
14859
15286
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14860
- state: DefaultTaskState.InProgress
15287
+ state: DefaultTaskState.InProgress,
15288
+ provider: this.task.provider || profile.provider,
15289
+ model: this.task.model || profile.model
14861
15290
  });
14862
15291
  const agentMessages = await this.agent.runAgent(this, profile, prompt, mode, promptContext, contextMessages, contextFiles, systemPrompt);
14863
15292
  if (agentMessages.length > 0) {
@@ -14868,6 +15297,7 @@ class Task {
14868
15297
  void this.sendRequestContextInfo();
14869
15298
  void this.sendWorktreeIntegrationStatusUpdated();
14870
15299
  void this.sendUpdatedFilesUpdated();
15300
+ void this.sendSkillsUpdated();
14871
15301
  this.resolveAgentRunPromises();
14872
15302
  if (waitForCurrentAgentToFinish) {
14873
15303
  await this.runNextQueuedPrompt();
@@ -14898,14 +15328,11 @@ class Task {
14898
15328
  const settings = this.store.getSettings();
14899
15329
  if (settings.taskSettings.autoGenerateTaskName) {
14900
15330
  this.generateTaskNameInBackground(prompt).then((taskName) => {
14901
- if (taskName) {
14902
- void this.saveTask({ name: taskName });
14903
- } else {
14904
- void this.saveTask({ name: fallbackName });
14905
- }
15331
+ const newName = taskName || fallbackName;
15332
+ void this.saveTask({ name: newName }).then(() => this.renameWorktreeBranchIfNeeded());
14906
15333
  }).catch((error) => {
14907
15334
  logger.warn("Failed to generate task name:", error);
14908
- void this.saveTask({ name: fallbackName });
15335
+ void this.saveTask({ name: fallbackName }).then(() => this.renameWorktreeBranchIfNeeded());
14909
15336
  });
14910
15337
  return "<<generating>>";
14911
15338
  } else {
@@ -15681,10 +16108,10 @@ ${contentText}</agent-response>`;
15681
16108
  return contextFiles;
15682
16109
  }
15683
16110
  const profile = await this.getTaskAgentProfile();
15684
- const ruleFiles = await this.getRuleFilesAsContextFiles(profile || void 0);
16111
+ const ruleFiles = await this.getRuleFilesAsContextFiles(profile || void 0, true);
15685
16112
  return [...contextFiles, ...ruleFiles];
15686
16113
  }
15687
- async getRuleFilesAsContextFiles(profile) {
16114
+ async getRuleFilesAsContextFiles(profile, includeDisabled = false) {
15688
16115
  const ruleFiles = [];
15689
16116
  const homeDir = os.homedir();
15690
16117
  if (await fileExists(AIDER_DESK_GLOBAL_RULES_DIR)) {
@@ -15755,7 +16182,8 @@ ${contentText}</agent-response>`;
15755
16182
  }
15756
16183
  }
15757
16184
  const extensionResult = await this.extensionManager.dispatchEvent("onRuleFilesRetrieved", { files: ruleFiles }, this.project, this);
15758
- return extensionResult.files;
16185
+ const disabledRuleFiles = this.project.getProjectSettings().disabledRuleFiles ?? [];
16186
+ return includeDisabled ? extensionResult.files : extensionResult.files.filter((f) => !disabledRuleFiles.includes(f.path));
15759
16187
  }
15760
16188
  getRepoMap() {
15761
16189
  return this.aiderManager.getRepoMap();
@@ -15784,6 +16212,92 @@ ${contentText}</agent-response>`;
15784
16212
  getContextMessages() {
15785
16213
  return this.contextManager.getContextMessages();
15786
16214
  }
16215
+ getSkillManager() {
16216
+ return this.skillManager;
16217
+ }
16218
+ async getSkills() {
16219
+ const contextMessages = await this.contextManager.getContextMessages();
16220
+ return this.skillManager.getSkills(contextMessages);
16221
+ }
16222
+ async activateSkill(skillName) {
16223
+ const contextMessages = await this.contextManager.getContextMessages();
16224
+ const activatedNames = this.skillManager.getActivatedSkillNames(contextMessages);
16225
+ if (activatedNames.has(skillName)) {
16226
+ logger.debug("Skill already activated, skipping", { skillName });
16227
+ return;
16228
+ }
16229
+ const content = await this.skillManager.getSkillContent(skillName);
16230
+ if (!content) {
16231
+ throw new Error(`Skill '${skillName}' not found`);
16232
+ }
16233
+ const [assistantMessage, toolMessage] = this.skillManager.buildActivateSkillMessages(skillName, content);
16234
+ this.contextManager.addContextMessage(assistantMessage);
16235
+ this.contextManager.addContextMessage(toolMessage);
16236
+ await this.processResponseMessage(
16237
+ {
16238
+ id: assistantMessage.id,
16239
+ action: "response",
16240
+ content: "User requested the skill activation.",
16241
+ finished: true
16242
+ },
16243
+ false
16244
+ );
16245
+ const toolCallId = toolMessage.content[0].toolCallId;
16246
+ this.addToolMessage(
16247
+ toolCallId,
16248
+ SKILLS_TOOL_GROUP_NAME,
16249
+ SKILLS_TOOL_ACTIVATE_SKILL,
16250
+ { skill: skillName },
16251
+ JSON.stringify(content),
16252
+ void 0,
16253
+ void 0,
16254
+ false,
16255
+ true
16256
+ );
16257
+ await this.updateContextInfo();
16258
+ }
16259
+ async deactivateSkill(skillName) {
16260
+ const contextMessages = await this.contextManager.getContextMessages();
16261
+ const toolName = `${SKILLS_TOOL_GROUP_NAME}${TOOL_GROUP_NAME_SEPARATOR}${SKILLS_TOOL_ACTIVATE_SKILL}`;
16262
+ for (let i = contextMessages.length - 1; i >= 0; i--) {
16263
+ const message = contextMessages[i];
16264
+ if (message.role === "assistant" && Array.isArray(message.content)) {
16265
+ const toolCallPart = message.content.find(
16266
+ (part) => part.type === "tool-call" && part.toolName === toolName && part.input?.skill === skillName
16267
+ );
16268
+ if (!toolCallPart || toolCallPart.type !== "tool-call") {
16269
+ continue;
16270
+ }
16271
+ const toolCallId = toolCallPart.toolCallId;
16272
+ const toolMessage = contextMessages.find(
16273
+ (msg) => msg.role === "tool" && Array.isArray(msg.content) && msg.content.some((part) => part.type === "tool-result" && part.toolCallId === toolCallId)
16274
+ );
16275
+ const removedIds = [];
16276
+ const idsToRemoveFromContext = [];
16277
+ if (toolMessage) {
16278
+ removedIds.push(toolMessage.id);
16279
+ removedIds.push(toolCallId);
16280
+ idsToRemoveFromContext.push(toolMessage.id);
16281
+ }
16282
+ const otherToolCalls = message.content.filter((part) => part.type === "tool-call" && part !== toolCallPart);
16283
+ if (otherToolCalls.length > 0) {
16284
+ this.contextManager.removeMessageById(toolCallId);
16285
+ } else {
16286
+ removedIds.push(message.id);
16287
+ idsToRemoveFromContext.push(message.id);
16288
+ this.contextManager.removeMessagesByIds(idsToRemoveFromContext);
16289
+ }
16290
+ if (removedIds.length > 0) {
16291
+ await this.reloadConnectorMessages();
16292
+ await this.updateContextInfo();
16293
+ void this.sendSkillsUpdated();
16294
+ }
16295
+ return removedIds;
16296
+ }
16297
+ }
16298
+ logger.debug(`No activation found for skill '${skillName}' to deactivate.`);
16299
+ return [];
16300
+ }
15787
16301
  async addRoleContextMessage(role, content, usageReport) {
15788
16302
  logger.debug("Adding role message to session:", {
15789
16303
  baseDir: this.project.baseDir,
@@ -16064,12 +16578,18 @@ ${contentText}</agent-response>`;
16064
16578
  const removedIds = this.contextManager.removeMessageById(messageId);
16065
16579
  await this.reloadConnectorMessages();
16066
16580
  await this.updateContextInfo();
16581
+ if (removedIds.length > 0) {
16582
+ void this.sendSkillsUpdated();
16583
+ }
16067
16584
  return removedIds;
16068
16585
  }
16069
16586
  async removeMessagesUpTo(messageId) {
16070
16587
  const removedIds = this.contextManager.removeMessagesAfter(messageId);
16071
16588
  await this.reloadConnectorMessages();
16072
16589
  await this.updateContextInfo();
16590
+ if (removedIds.length > 0) {
16591
+ void this.sendSkillsUpdated();
16592
+ }
16073
16593
  return removedIds;
16074
16594
  }
16075
16595
  sendTaskMessageRemoved(messageIds) {
@@ -16077,26 +16597,27 @@ ${contentText}</agent-response>`;
16077
16597
  this.eventManager.sendTaskMessageRemoved(this.project.baseDir, this.taskId, messageIds);
16078
16598
  }
16079
16599
  }
16080
- async redoLastUserPrompt(mode, updatedPrompt) {
16081
- logger.info("Redoing last user prompt:", {
16600
+ async redoUserPrompt(messageId, mode, updatedPrompt) {
16601
+ logger.info("Redoing user prompt:", {
16082
16602
  baseDir: this.project.baseDir,
16603
+ messageId,
16083
16604
  mode,
16084
16605
  hasUpdatedPrompt: !!updatedPrompt
16085
16606
  });
16086
- const removedMessages = this.contextManager.removeMessagesUpToLastUserMessage();
16087
- const originalLastUserMessage = removedMessages.findLast((msg) => msg.role === MessageRole.User);
16088
- if (!originalLastUserMessage) {
16089
- logger.warn("Could not find original last user message content to redo.");
16607
+ const removedMessages = this.contextManager.removeMessagesUpToUserMessage(messageId);
16608
+ const originalUserMessage = removedMessages[0];
16609
+ if (!originalUserMessage || originalUserMessage.role !== MessageRole.User) {
16610
+ logger.warn("Could not find the specified user message to redo.", { messageId });
16090
16611
  return;
16091
16612
  }
16092
- const promptToRun = updatedPrompt ?? originalLastUserMessage.content;
16613
+ const promptToRun = updatedPrompt ?? originalUserMessage.content;
16093
16614
  if (promptToRun) {
16094
16615
  logger.info("Found message content to run, reloading and re-running prompt.", {
16095
16616
  remainingMessagesCount: (await this.contextManager.getContextMessages()).length
16096
16617
  });
16097
- this.sendTaskMessageRemoved(removedMessages.slice(0, -1).map((msg) => msg.id));
16618
+ this.sendTaskMessageRemoved(removedMessages.slice(1).map((msg) => msg.id));
16098
16619
  await this.updateContextInfo();
16099
- void this.runPrompt(promptToRun, mode, false, originalLastUserMessage.id);
16620
+ void this.runPrompt(promptToRun, mode, false, originalUserMessage.id);
16100
16621
  } else {
16101
16622
  logger.warn("Could not find a previous user message to redo or an updated prompt to run.");
16102
16623
  }
@@ -16123,7 +16644,7 @@ ${contentText}</agent-response>`;
16123
16644
  if (lastMessage && lastMessage.role === MessageRole.User) {
16124
16645
  logger.info("Last message is from user, redoing prompt");
16125
16646
  this.addLogMessage("loading", "Resuming task...");
16126
- void this.redoLastUserPrompt(mode);
16647
+ void this.redoUserPrompt(lastMessage.id, mode);
16127
16648
  } else {
16128
16649
  logger.info("Last message is not from user, sending Continue prompt");
16129
16650
  void this.runPrompt("Continue", mode, false);
@@ -16455,9 +16976,13 @@ ${contentText}</agent-response>`;
16455
16976
  async projectSettingsChanged(oldSettings, newSettings) {
16456
16977
  const modeChanged = oldSettings.currentMode !== newSettings.currentMode;
16457
16978
  const agentProfileIdChanged = oldSettings.agentProfileId !== newSettings.agentProfileId;
16458
- if (agentProfileIdChanged || modeChanged) {
16979
+ const disabledRulesChanged = JSON.stringify(oldSettings.disabledRuleFiles) !== JSON.stringify(newSettings.disabledRuleFiles);
16980
+ if (agentProfileIdChanged || modeChanged || disabledRulesChanged) {
16459
16981
  void this.sendContextFilesUpdated();
16460
16982
  }
16983
+ if (disabledRulesChanged) {
16984
+ void this.updateAgentEstimatedTokens();
16985
+ }
16461
16986
  }
16462
16987
  sendUpdateEnvVars(environmentVariables) {
16463
16988
  this.aiderManager.sendUpdateEnvVars(environmentVariables);
@@ -16734,6 +17259,18 @@ ${error.stderr}`,
16734
17259
  });
16735
17260
  this.eventManager.sendUpdatedFilesUpdated(this.project.baseDir, this.taskId, updatedFiles);
16736
17261
  }
17262
+ async sendSkillsUpdated() {
17263
+ const skills = await this.getSkills();
17264
+ this.eventManager.sendSkillsUpdated(this.project.baseDir, this.taskId, skills);
17265
+ }
17266
+ async initWorktree() {
17267
+ const branchName = this.generateBranchName();
17268
+ this.task.worktree = await this.worktreeManager.createWorktree(this.project.baseDir, this.taskId, branchName);
17269
+ const settings = this.store.getSettings();
17270
+ if (settings.taskSettings.worktreeSymlinkFolders && settings.taskSettings.worktreeSymlinkFolders.length > 0) {
17271
+ await this.worktreeManager.createSymlinks(this.project.baseDir, this.task.worktree.path, settings.taskSettings.worktreeSymlinkFolders);
17272
+ }
17273
+ }
16737
17274
  async applyWorkingMode(mode) {
16738
17275
  logger.info("Applying workingMode configuration", {
16739
17276
  baseDir: this.project.baseDir,
@@ -16744,17 +17281,15 @@ ${error.stderr}`,
16744
17281
  const currentWorktree = await this.worktreeManager.getTaskWorktree(this.project.baseDir, this.taskId);
16745
17282
  if (mode === "worktree") {
16746
17283
  if (!currentWorktree) {
16747
- const branchName = this.generateBranchName();
16748
- this.task.worktree = await this.worktreeManager.createWorktree(this.project.baseDir, this.taskId, branchName);
16749
- const settings = this.store.getSettings();
16750
- if (settings.taskSettings.worktreeSymlinkFolders && settings.taskSettings.worktreeSymlinkFolders.length > 0) {
16751
- await this.worktreeManager.createSymlinks(this.project.baseDir, this.task.worktree.path, settings.taskSettings.worktreeSymlinkFolders);
16752
- }
17284
+ await this.initWorktree();
16753
17285
  }
16754
17286
  this.task.workingMode = mode;
16755
17287
  } else if (mode === "local") {
16756
17288
  if (currentWorktree) {
16757
- await this.worktreeManager.removeWorktree(this.project.baseDir, currentWorktree);
17289
+ const isShared = this.project.isWorktreeSharedWithOtherTasks(currentWorktree.path, this.taskId);
17290
+ if (!isShared) {
17291
+ await this.worktreeManager.removeWorktree(this.project.baseDir, currentWorktree);
17292
+ }
16758
17293
  }
16759
17294
  this.task.worktree = void 0;
16760
17295
  this.task.lastMergeState = void 0;
@@ -16826,7 +17361,8 @@ Only answer with the commit message, nothing else.`,
16826
17361
  squash,
16827
17362
  effectiveCommitMessage || this.task.name || `Task ${this.taskId} changes`,
16828
17363
  targetBranch,
16829
- symlinkFolders
17364
+ symlinkFolders,
17365
+ this.task.worktree.baseCommit
16830
17366
  );
16831
17367
  await this.saveTask({ lastMergeState: mergeState });
16832
17368
  this.addLogMessage(
@@ -16848,6 +17384,47 @@ Only answer with the commit message, nothing else.`,
16848
17384
  await this.sendUpdatedFilesUpdated();
16849
17385
  await this.sendWorktreeIntegrationStatusUpdated();
16850
17386
  }
17387
+ async mergeAndSwitchToLocal(targetBranch) {
17388
+ if (!this.task.worktree) {
17389
+ throw new Error("No worktree exists for this task");
17390
+ }
17391
+ logger.info("Merging worktree and switching to local mode", {
17392
+ baseDir: this.project.baseDir,
17393
+ taskId: this.taskId
17394
+ });
17395
+ await this.waitForCurrentPromptToFinish();
17396
+ try {
17397
+ const effectiveTargetBranch = targetBranch || await this.worktreeManager.getProjectMainBranch(this.project.baseDir);
17398
+ this.addLogMessage("loading", `Merging worktree to ${effectiveTargetBranch} branch and switching to local mode...`);
17399
+ const settings = this.store.getSettings();
17400
+ const symlinkFolders = settings.taskSettings.worktreeSymlinkFolders || [];
17401
+ const mergeState = await this.worktreeManager.mergeWorktreeToMainWithUncommitted(
17402
+ this.project.baseDir,
17403
+ this.task.id,
17404
+ this.task.worktree.path,
17405
+ false,
17406
+ this.task.name || `Task ${this.taskId} changes`,
17407
+ targetBranch,
17408
+ symlinkFolders
17409
+ );
17410
+ await this.saveTask({ lastMergeState: mergeState });
17411
+ this.addLogMessage("info", `Successfully merged worktree to ${effectiveTargetBranch} branch`, true);
17412
+ await this.updateTask({ workingMode: "local" });
17413
+ } catch (error) {
17414
+ logger.error("Failed to merge worktree and switch to local:", { error });
17415
+ const isConflict = error instanceof GitError && (error.gitOutput?.toLowerCase().includes("resolve all conflicts") || error.message?.toLowerCase().includes("conflicts must be resolved first") || error.gitOutput?.toLowerCase().includes("conflicts must be resolved first"));
17416
+ this.addLogMessage(
17417
+ "error",
17418
+ isConflict ? "worktree.mergeConflicts" : error instanceof GitError ? error.getErrorDetails() : `Failed to merge worktree: ${error instanceof Error ? error.message : String(error)}`,
17419
+ true,
17420
+ void 0,
17421
+ isConflict ? ["rebase-worktree"] : void 0
17422
+ );
17423
+ await this.sendUpdatedFilesUpdated();
17424
+ await this.sendWorktreeIntegrationStatusUpdated();
17425
+ throw error;
17426
+ }
17427
+ }
16851
17428
  async applyUncommittedChanges(targetBranch) {
16852
17429
  if (!this.task.worktree) {
16853
17430
  throw new Error("No worktree exists for this task");
@@ -16977,10 +17554,18 @@ ${diff}
16977
17554
  taskId: this.taskId,
16978
17555
  amend
16979
17556
  });
17557
+ const beforeResult = await this.extensionManager.dispatchEvent("onBeforeCommit", { message, amend }, this.project, this);
17558
+ if (beforeResult.blocked) {
17559
+ logger.debug("Commit blocked by extension");
17560
+ return;
17561
+ }
17562
+ message = beforeResult.message;
17563
+ amend = beforeResult.amend;
16980
17564
  const taskDir = this.getTaskDir();
16981
17565
  await this.worktreeManager.commitChanges(taskDir, message, amend);
16982
17566
  await this.sendUpdatedFilesUpdated();
16983
17567
  await this.sendWorktreeIntegrationStatusUpdated();
17568
+ await this.extensionManager.dispatchEvent("onAfterCommit", { message, amend }, this.project, this);
16984
17569
  }
16985
17570
  async getWorktreeIntegrationStatus(targetBranch) {
16986
17571
  if (!this.task.worktree) {
@@ -16988,12 +17573,16 @@ ${diff}
16988
17573
  }
16989
17574
  const effectiveTargetBranch = targetBranch || await this.worktreeManager.getProjectMainBranch(this.project.baseDir);
16990
17575
  const worktreePath = this.task.worktree.path;
17576
+ const settings = this.store.getSettings();
17577
+ const symlinkFolders = settings.taskSettings.worktreeSymlinkFolders || [];
16991
17578
  const [unmergedWork, predictedConflicts, rebaseState] = await Promise.all([
16992
- this.worktreeManager.checkWorktreeForUnmergedWork(this.project.baseDir, worktreePath, effectiveTargetBranch),
17579
+ this.worktreeManager.checkWorktreeForUnmergedWork(this.project.baseDir, worktreePath, effectiveTargetBranch, symlinkFolders),
16993
17580
  this.worktreeManager.checkForRebaseConflicts(worktreePath, effectiveTargetBranch),
16994
17581
  this.worktreeManager.getRebaseState(worktreePath)
16995
17582
  ]);
16996
17583
  return {
17584
+ currentBranch: this.task.worktree.branch || "",
17585
+ baseBranch: this.task.worktree.baseBranch || "",
16997
17586
  targetBranch: effectiveTargetBranch,
16998
17587
  aheadCommits: {
16999
17588
  count: unmergedWork.unmergedCommitCount,
@@ -17020,8 +17609,12 @@ ${diff}
17020
17609
  await this.waitForCurrentPromptToFinish();
17021
17610
  try {
17022
17611
  this.addLogMessage("loading", `Rebasing worktree from ${effectiveFromBranch}...`);
17023
- const { success, error } = await this.worktreeManager.rebaseMainIntoWorktree(this.task.worktree.path, effectiveFromBranch);
17612
+ const { success, error } = await this.worktreeManager.rebaseMainIntoWorktree(this.task.worktree.path, effectiveFromBranch, this.task.worktree.baseCommit);
17024
17613
  if (success) {
17614
+ const newHead = await this.worktreeManager.getHeadCommit(this.task.worktree.path);
17615
+ if (newHead) {
17616
+ await this.saveTask({ worktree: { ...this.task.worktree, baseCommit: newHead, baseBranch: effectiveFromBranch } });
17617
+ }
17025
17618
  this.addLogMessage("info", "Worktree rebased successfully", true);
17026
17619
  return;
17027
17620
  }
@@ -17062,6 +17655,19 @@ ${diff}
17062
17655
  await this.sendUpdatedFilesUpdated();
17063
17656
  await this.sendWorktreeIntegrationStatusUpdated();
17064
17657
  }
17658
+ async renameWorktreeBranch(newBranchName) {
17659
+ if (!this.task.worktree) {
17660
+ throw new Error("No worktree exists for this task");
17661
+ }
17662
+ const oldBranchName = this.task.worktree.branch;
17663
+ if (!oldBranchName) {
17664
+ throw new Error("Cannot determine current branch name");
17665
+ }
17666
+ const actualBranchName = await this.worktreeManager.renameBranch(this.project.baseDir, oldBranchName, newBranchName);
17667
+ this.task.worktree.branch = actualBranchName;
17668
+ await this.saveTask({ worktree: this.task.worktree });
17669
+ void this.sendWorktreeIntegrationStatusUpdated();
17670
+ }
17065
17671
  async executeConflictResolution(directoryPath, directoryName) {
17066
17672
  const activeProfile = await this.getTaskAgentProfile();
17067
17673
  if (!activeProfile) {
@@ -17727,6 +18333,17 @@ class Project {
17727
18333
  getAgentProfiles() {
17728
18334
  return this.agentProfileManager.getProjectProfiles(this);
17729
18335
  }
18336
+ /**
18337
+ * Checks if any other task (excluding the specified taskId) uses the given worktree path.
18338
+ */
18339
+ isWorktreeSharedWithOtherTasks(worktreePath, excludeTaskId) {
18340
+ for (const [id, task] of this.tasks.entries()) {
18341
+ if (id !== excludeTaskId && task.task.worktree?.path === worktreePath) {
18342
+ return true;
18343
+ }
18344
+ }
18345
+ return false;
18346
+ }
17730
18347
  async deleteTaskInternal(taskId) {
17731
18348
  const taskDir = path.join(this.baseDir, ".aider-desk", "tasks", taskId);
17732
18349
  const task = this.tasks.get(taskId);
@@ -17735,6 +18352,19 @@ class Project {
17735
18352
  this.tasks.delete(taskId);
17736
18353
  this.eventManager.sendTaskDeleted(task.task);
17737
18354
  }
18355
+ const taskData = task?.task;
18356
+ if (taskData?.worktree && !this.isWorktreeSharedWithOtherTasks(taskData.worktree.path, taskId)) {
18357
+ try {
18358
+ await this.worktreeManager.removeWorktree(this.baseDir, taskData.worktree);
18359
+ } catch (error) {
18360
+ logger.warn("Failed to remove worktree during task deletion", {
18361
+ baseDir: this.baseDir,
18362
+ taskId,
18363
+ worktreePath: taskData.worktree.path,
18364
+ error: error instanceof Error ? error.message : String(error)
18365
+ });
18366
+ }
18367
+ }
17738
18368
  await fs.rm(taskDir, { recursive: true, force: true });
17739
18369
  }
17740
18370
  async deleteTask(taskId) {
@@ -17769,8 +18399,12 @@ class Project {
17769
18399
  if (!sourceTask) {
17770
18400
  throw new Error(`Task with id ${taskId} not found`);
17771
18401
  }
18402
+ const hasWorktree = sourceTask.task.worktree && sourceTask.task.workingMode === "worktree";
17772
18403
  const newTask = await this.prepareTask(void 0, {
17773
18404
  ...sourceTask.task,
18405
+ // When the source task has a worktree, make the duplicate a subtask
18406
+ // so both tasks sharing the same worktree are clearly related
18407
+ ...hasWorktree ? { parentId: sourceTask.task.parentId || taskId } : {},
17774
18408
  state: sourceTask.task.state === DefaultTaskState.InProgress ? DefaultTaskState.Todo : sourceTask.task.state
17775
18409
  });
17776
18410
  await newTask.init();
@@ -17982,6 +18616,15 @@ class EventManager {
17982
18616
  this.sendToWindows("updated-files-updated", data);
17983
18617
  this.broadcastToEventConnectors("updated-files-updated", data);
17984
18618
  }
18619
+ sendSkillsUpdated(baseDir, taskId, skills) {
18620
+ const data = {
18621
+ baseDir,
18622
+ taskId,
18623
+ skills
18624
+ };
18625
+ this.sendToWindows("skills-updated", data);
18626
+ this.broadcastToEventConnectors("skills-updated", data);
18627
+ }
17985
18628
  // Response events
17986
18629
  sendResponseChunk(data) {
17987
18630
  this.sendToWindows("response-chunk", data);
@@ -19382,6 +20025,34 @@ const createDeepseekLlm = (profile, model, settings, projectDir) => {
19382
20025
  });
19383
20026
  return deepseekProvider(model.id);
19384
20027
  };
20028
+ const getDeepseekProviderOptions = (llmProvider, model) => {
20029
+ if (!isDeepseekProvider(llmProvider)) {
20030
+ return void 0;
20031
+ }
20032
+ const providerOverrides = model.providerOverrides;
20033
+ const thinkingEnabled = providerOverrides?.thinkingEnabled ?? llmProvider.thinkingEnabled ?? true;
20034
+ const reasoningEffort = providerOverrides?.reasoningEffort ?? llmProvider.reasoningEffort ?? "high";
20035
+ return {
20036
+ deepseek: {
20037
+ thinking: { type: thinkingEnabled ? "enabled" : "disabled" },
20038
+ ...thinkingEnabled && { reasoningEffort }
20039
+ }
20040
+ };
20041
+ };
20042
+ const getDeepseekProviderParameters = (llmProvider, model) => {
20043
+ if (!isDeepseekProvider(llmProvider)) {
20044
+ return {};
20045
+ }
20046
+ const providerOverrides = model.providerOverrides;
20047
+ const thinkingEnabled = providerOverrides?.thinkingEnabled ?? llmProvider.thinkingEnabled ?? true;
20048
+ if (thinkingEnabled) {
20049
+ return {
20050
+ temperature: void 0,
20051
+ topP: void 0
20052
+ };
20053
+ }
20054
+ return {};
20055
+ };
19385
20056
  const deepseekProviderStrategy = {
19386
20057
  // Core LLM functions
19387
20058
  createLlm: createDeepseekLlm,
@@ -19390,7 +20061,9 @@ const deepseekProviderStrategy = {
19390
20061
  loadModels: loadDeepseekModels,
19391
20062
  hasEnvVars: hasDeepseekEnvVars,
19392
20063
  getAiderMapping: getDeepseekAiderMapping,
19393
- getModelInfo: getDefaultModelInfo
20064
+ getModelInfo: getDefaultModelInfo,
20065
+ getProviderOptions: getDeepseekProviderOptions,
20066
+ getProviderParameters: getDeepseekProviderParameters
19394
20067
  };
19395
20068
  const loadGeminiModels = async (profile, settings) => {
19396
20069
  if (!isGeminiProvider(profile.provider)) {
@@ -20015,7 +20688,6 @@ const alibabaPlanProviderStrategy = {
20015
20688
  getProviderOptions: getAlibabaPlanProviderOptions
20016
20689
  };
20017
20690
  const KIMI_PLAN_BASE_URL = "https://api.kimi.com/coding/v1";
20018
- const KIMI_PLAN_MODEL_ID = "k2p5";
20019
20691
  const loadKimiPlanModels = async (profile, settings) => {
20020
20692
  if (!isKimiPlanProvider(profile.provider)) {
20021
20693
  return { models: [], success: false };
@@ -20029,7 +20701,19 @@ const loadKimiPlanModels = async (profile, settings) => {
20029
20701
  }
20030
20702
  const models = [
20031
20703
  {
20032
- id: KIMI_PLAN_MODEL_ID,
20704
+ id: "kimi-k2-thinking",
20705
+ providerId: profile.id,
20706
+ maxInputTokens: 262144,
20707
+ maxOutputTokensLimit: 32768
20708
+ },
20709
+ {
20710
+ id: "k2p5",
20711
+ providerId: profile.id,
20712
+ maxInputTokens: 262144,
20713
+ maxOutputTokensLimit: 32768
20714
+ },
20715
+ {
20716
+ id: "k2p6",
20033
20717
  providerId: profile.id,
20034
20718
  maxInputTokens: 262144,
20035
20719
  maxOutputTokensLimit: 32768
@@ -21346,9 +22030,9 @@ const getOpenRouterUsageReport = (task, provider, model, usage, providerMetadata
21346
22030
  agentTotalCost: task.task.agentTotalCost + messageCost
21347
22031
  };
21348
22032
  };
21349
- const getOpenRouterCacheControl = (profile, llmProvider) => {
22033
+ const getOpenRouterCacheControl = (llmProvider, model) => {
21350
22034
  if (isOpenRouterProvider(llmProvider)) {
21351
- if (profile.model?.startsWith("anthropic/")) {
22035
+ if (model.id?.startsWith("anthropic/")) {
21352
22036
  return {
21353
22037
  providerOptions: {
21354
22038
  openrouter: {
@@ -21537,9 +22221,9 @@ const normalizeRequestyMessages = (_provider, model, messages) => {
21537
22221
  }
21538
22222
  return messages;
21539
22223
  };
21540
- const getRequestyCacheControl = (profile, llmProvider) => {
22224
+ const getRequestyCacheControl = (llmProvider, model) => {
21541
22225
  if (isRequestyProvider(llmProvider) && !llmProvider.useAutoCache) {
21542
- if (profile.model?.startsWith("anthropic/")) {
22226
+ if (model.id?.startsWith("anthropic/")) {
21543
22227
  return {
21544
22228
  providerOptions: {
21545
22229
  requesty: {
@@ -21928,6 +22612,8 @@ const zaiPlanProviderStrategy = {
21928
22612
  };
21929
22613
  const MODELS_META_URL = "https://models.dev/api.json";
21930
22614
  const MODELS_FILE = path.join(AIDER_DESK_DATA_DIR, "models.json");
22615
+ const PROVIDER_MODELS_CACHE_FILE = path.join(AIDER_DESK_CACHE_DIR, "provider-models.json");
22616
+ const PROVIDER_MODELS_CACHE_VERSION = 1;
21931
22617
  class ModelManager {
21932
22618
  constructor(store, eventManager) {
21933
22619
  this.store = store;
@@ -21976,9 +22662,20 @@ class ModelManager {
21976
22662
  this.updateEnvVarsProviders();
21977
22663
  await this.loadModelsInfo();
21978
22664
  await this.loadModelOverrides();
21979
- await this.loadProviderModels(this.getProviders().filter((p) => !p.disabled));
22665
+ const cacheLoaded = await this.loadProviderModelsFromCache();
22666
+ if (cacheLoaded) {
22667
+ this.eventManager.sendProviderModelsUpdated({
22668
+ models: Object.values(this.providerModels).flat(),
22669
+ loading: true,
22670
+ errors: this.providerErrors
22671
+ });
22672
+ this.loadProviderModelsInBackground(this.getProviders().filter((p) => !p.disabled));
22673
+ } else {
22674
+ await this.loadProviderModels(this.getProviders().filter((p) => !p.disabled));
22675
+ }
21980
22676
  logger.info("ModelInfoManager initialized successfully.", {
21981
- modelCount: Object.keys(this.modelsInfo).length
22677
+ modelCount: Object.keys(this.modelsInfo).length,
22678
+ cacheLoaded
21982
22679
  });
21983
22680
  } catch (error) {
21984
22681
  logger.error("Error initializing ModelInfoManager:", error);
@@ -22025,6 +22722,44 @@ class ModelManager {
22025
22722
  await freshDataPromise;
22026
22723
  }
22027
22724
  }
22725
+ async loadProviderModelsFromCache() {
22726
+ try {
22727
+ const cacheData = await fs$1.promises.readFile(PROVIDER_MODELS_CACHE_FILE, "utf-8");
22728
+ const cache = JSON.parse(cacheData);
22729
+ if (cache.version !== PROVIDER_MODELS_CACHE_VERSION) {
22730
+ logger.info("Provider models cache version mismatch, ignoring cache");
22731
+ return false;
22732
+ }
22733
+ this.providerModels = cache.providerModels;
22734
+ this.providerErrors = cache.providerErrors;
22735
+ logger.info("Loaded provider models from cache", {
22736
+ providerCount: Object.keys(cache.providerModels).length
22737
+ });
22738
+ return true;
22739
+ } catch {
22740
+ logger.info("Provider models cache not found or invalid");
22741
+ return false;
22742
+ }
22743
+ }
22744
+ async saveProviderModelsToCache() {
22745
+ try {
22746
+ const cache = {
22747
+ version: PROVIDER_MODELS_CACHE_VERSION,
22748
+ providerModels: this.providerModels,
22749
+ providerErrors: this.providerErrors
22750
+ };
22751
+ await fs$1.promises.mkdir(AIDER_DESK_CACHE_DIR, { recursive: true });
22752
+ await fs$1.promises.writeFile(PROVIDER_MODELS_CACHE_FILE, JSON.stringify(cache));
22753
+ logger.info("Saved provider models to cache");
22754
+ } catch (error) {
22755
+ logger.error("Failed to save provider models to cache:", error);
22756
+ }
22757
+ }
22758
+ loadProviderModelsInBackground(providers) {
22759
+ this.loadProviderModels(providers).catch((error) => {
22760
+ logger.error("Background loading of provider models failed:", error);
22761
+ });
22762
+ }
22028
22763
  processModelsMeta(data) {
22029
22764
  for (const providerId in data) {
22030
22765
  const providerData = data[providerId];
@@ -22067,6 +22802,21 @@ class ModelManager {
22067
22802
  }
22068
22803
  return Array.from(changed);
22069
22804
  }
22805
+ async loadModelsWithRetry(strategy, profile, retryCount = 3) {
22806
+ let lastResponse;
22807
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
22808
+ if (attempt > 0) {
22809
+ const delayMs = Math.pow(2, attempt - 1) * 1e3;
22810
+ logger.info(`Retrying load models for provider profile ${profile.id} in ${delayMs}ms (attempt ${attempt + 1}/${retryCount + 1})`);
22811
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
22812
+ }
22813
+ lastResponse = await strategy.loadModels(profile, this.store.getSettings());
22814
+ if (lastResponse.success) {
22815
+ return lastResponse;
22816
+ }
22817
+ }
22818
+ return lastResponse;
22819
+ }
22070
22820
  async providersChanged(oldProviders, newProviders) {
22071
22821
  await this.initPromise;
22072
22822
  const removedProviders = oldProviders.filter((p) => !newProviders.find((np) => np.id === p.id));
@@ -22108,7 +22858,7 @@ class ModelManager {
22108
22858
  continue;
22109
22859
  }
22110
22860
  let providerModels = [];
22111
- const response = await strategy.loadModels(profile, this.store.getSettings());
22861
+ const response = await this.loadModelsWithRetry(strategy, profile);
22112
22862
  delete this.providerErrors[profile.id];
22113
22863
  if (response.success) {
22114
22864
  providerModels.push(...response.models);
@@ -22137,6 +22887,7 @@ class ModelManager {
22137
22887
  errors: this.providerErrors
22138
22888
  });
22139
22889
  this.eventManager.sendSettingsUpdated(this.store.getSettings());
22890
+ await this.saveProviderModelsToCache();
22140
22891
  }
22141
22892
  enrichWithModelInfo(models, profile, strategy) {
22142
22893
  const enrichedModels = [...models];
@@ -22404,12 +23155,22 @@ class ModelManager {
22404
23155
  }
22405
23156
  return strategy.getUsageReport(task, provider, modelObj, usage, providerMetadata);
22406
23157
  }
22407
- getCacheControl(profile, llmProvider) {
23158
+ getCacheControl(provider, modelId) {
23159
+ const llmProvider = provider.provider;
22408
23160
  const strategy = this.providerRegistry[llmProvider.name];
22409
23161
  if (!strategy?.getCacheControl) {
22410
23162
  return void 0;
22411
23163
  }
22412
- return strategy.getCacheControl(profile, llmProvider);
23164
+ const models = this.providerModels[provider.id] || [];
23165
+ const modelObj = models.find((m) => m.id === modelId);
23166
+ if (!modelObj) {
23167
+ const fallbackModel = {
23168
+ id: modelId,
23169
+ providerId: provider.id
23170
+ };
23171
+ return strategy.getCacheControl(llmProvider, fallbackModel);
23172
+ }
23173
+ return strategy.getCacheControl(llmProvider, modelObj);
22413
23174
  }
22414
23175
  isStreamingDisabled(provider, modelId) {
22415
23176
  const llmProvider = provider.provider;
@@ -22543,7 +23304,7 @@ class ModelManager {
22543
23304
  getProviderOptions: provider.strategy.getProviderOptions ? (_provider, model) => provider.strategy.getProviderOptions(model) : void 0,
22544
23305
  getProviderTools: provider.strategy.getProviderTools ? (_provider, model) => provider.strategy.getProviderTools(model) : void 0,
22545
23306
  getProviderParameters: provider.strategy.getProviderParameters ? (_provider, model) => provider.strategy.getProviderParameters(model) : void 0,
22546
- getCacheControl: provider.strategy.getCacheControl ? (profile2) => provider.strategy.getCacheControl(profile2) : void 0,
23307
+ getCacheControl: provider.strategy.getCacheControl ? (_provider, model) => provider.strategy.getCacheControl(model) : void 0,
22547
23308
  hasEnvVars: () => false,
22548
23309
  getAiderMapping: provider.strategy.getAiderMapping ? provider.strategy.getAiderMapping : (_provider, modelId) => ({
22549
23310
  modelName: modelId,
@@ -23802,8 +24563,16 @@ class TaskContextImpl {
23802
24563
  async loadContextMessages(messages) {
23803
24564
  await this.task.loadContextMessages(messages);
23804
24565
  }
24566
+ async redoUserPrompt(messageId, mode, updatedPrompt) {
24567
+ await this.task.redoUserPrompt(messageId, mode || "agent", updatedPrompt);
24568
+ }
23805
24569
  async redoLastUserPrompt(mode, updatedPrompt) {
23806
- await this.task.redoLastUserPrompt(mode || "agent", updatedPrompt);
24570
+ const messages = await this.task.getContextMessages();
24571
+ const lastUserMessage = messages.findLast((msg) => msg.role === "user");
24572
+ if (!lastUserMessage) {
24573
+ return;
24574
+ }
24575
+ await this.task.redoUserPrompt(lastUserMessage.id, mode || "agent", updatedPrompt);
23807
24576
  }
23808
24577
  async removeMessagesUpTo(messageId) {
23809
24578
  await this.task.removeMessagesUpTo(messageId);
@@ -24015,7 +24784,11 @@ class ExtensionContextImpl {
24015
24784
  this.memoryManager = memoryManager;
24016
24785
  this.project = project;
24017
24786
  this.task = task;
24787
+ this.taskContext = this.task ? new TaskContextImpl(this.task) : null;
24788
+ this.projectContext = this.project ? new ProjectContextImpl(this.project) : null;
24018
24789
  }
24790
+ taskContext;
24791
+ projectContext;
24019
24792
  log(message, type = "info") {
24020
24793
  const logFn = logger[type];
24021
24794
  logFn(`[Extension:${this.extensionName}] ${message}`);
@@ -24024,13 +24797,13 @@ class ExtensionContextImpl {
24024
24797
  return this.project?.baseDir ?? "";
24025
24798
  }
24026
24799
  getTaskContext() {
24027
- return this.task ? new TaskContextImpl(this.task) : null;
24800
+ return this.taskContext;
24028
24801
  }
24029
24802
  getProjectContext() {
24030
- if (!this.project) {
24803
+ if (!this.projectContext) {
24031
24804
  throw new Error("Project context not available");
24032
24805
  }
24033
- return new ProjectContextImpl(this.project);
24806
+ return this.projectContext;
24034
24807
  }
24035
24808
  async getModelConfigs() {
24036
24809
  if (!this.modelManager) {
@@ -24464,7 +25237,7 @@ class ExtensionManager {
24464
25237
  filterEnabledExtensions(extensions) {
24465
25238
  const settings = this.store.getSettings();
24466
25239
  const disabledExtensions = settings.extensions?.disabled || [];
24467
- return extensions.filter((ext) => !disabledExtensions.includes(ext.metadata.name));
25240
+ return extensions.filter((ext) => !disabledExtensions.includes(ext.filePath));
24468
25241
  }
24469
25242
  /**
24470
25243
  * Handle settings changes. Detects when extensions with UI components
@@ -24482,7 +25255,7 @@ class ExtensionManager {
24482
25255
  const changedExtensions = [...newlyDisabled, ...newlyEnabled];
24483
25256
  if (changedExtensions.length > 0) {
24484
25257
  const allExtensions = this.registry.getExtensions();
24485
- const hasUIComponentsChange = allExtensions.some((ext) => changedExtensions.includes(ext.metadata.name) && ext.instance.getUIComponents !== void 0);
25258
+ const hasUIComponentsChange = allExtensions.some((ext) => changedExtensions.includes(ext.filePath) && ext.instance.getUIComponents !== void 0);
24486
25259
  if (hasUIComponentsChange) {
24487
25260
  logger.debug("[Extensions] Extensions with UI components changed, triggering UI refresh");
24488
25261
  this.eventManager.sendExtensionUIRefresh({ reloadComponents: true });
@@ -24510,6 +25283,7 @@ class ExtensionManager {
24510
25283
  this.registry.clear();
24511
25284
  await this.loadExtensionsForDir(AIDER_DESK_GLOBAL_EXTENSIONS_DIR);
24512
25285
  this.initialized = true;
25286
+ this.migrateDisabledExtensions();
24513
25287
  await this.startHotReloadWatcher();
24514
25288
  this.captureExtensionsTelemetry();
24515
25289
  this.preloadAvailableExtensions().catch((error) => {
@@ -24640,13 +25414,51 @@ class ExtensionManager {
24640
25414
  isInitialized() {
24641
25415
  return this.initialized;
24642
25416
  }
25417
+ /**
25418
+ * @deprecated Migration helper: converts old name-based disabled list to filePath-based.
25419
+ * Will be removed in a future version.
25420
+ */
25421
+ migrateDisabledExtensions() {
25422
+ const settings = this.store.getSettings();
25423
+ const disabled = settings.extensions?.disabled;
25424
+ if (!disabled || disabled.length === 0) {
25425
+ return;
25426
+ }
25427
+ const allExtensions = this.registry.getExtensions();
25428
+ const filePaths = new Set(allExtensions.map((ext) => ext.filePath));
25429
+ const migrated = [];
25430
+ let changed = false;
25431
+ for (const item of disabled) {
25432
+ if (filePaths.has(item)) {
25433
+ migrated.push(item);
25434
+ } else {
25435
+ const match = allExtensions.find((ext) => ext.metadata.name === item);
25436
+ if (match) {
25437
+ migrated.push(match.filePath);
25438
+ changed = true;
25439
+ } else {
25440
+ migrated.push(item);
25441
+ }
25442
+ }
25443
+ }
25444
+ if (changed) {
25445
+ logger.info("[Extensions] Migrated disabled extensions from name-based to filePath-based identifiers");
25446
+ this.store.saveSettings({
25447
+ ...settings,
25448
+ extensions: {
25449
+ ...settings.extensions,
25450
+ disabled: migrated
25451
+ }
25452
+ });
25453
+ }
25454
+ }
24643
25455
  captureExtensionsTelemetry() {
24644
25456
  const allExtensions = this.registry.getExtensions();
24645
25457
  const settings = this.store.getSettings();
24646
25458
  const disabledExtensions = settings.extensions?.disabled || [];
24647
25459
  const globalExtensions = allExtensions.filter((ext) => !ext.projectDir).length;
24648
25460
  const projectExtensions = allExtensions.filter((ext) => ext.projectDir).length;
24649
- const enabledCount = allExtensions.filter((ext) => !disabledExtensions.includes(ext.metadata.name)).length;
25461
+ const enabledCount = allExtensions.filter((ext) => !disabledExtensions.includes(ext.filePath)).length;
24650
25462
  this.telemetryManager.captureExtensionsLoaded(allExtensions.length, globalExtensions, projectExtensions, enabledCount, disabledExtensions.length);
24651
25463
  }
24652
25464
  async dispose() {
@@ -25170,6 +25982,42 @@ class ExtensionManager {
25170
25982
  }
25171
25983
  return collectedProviders;
25172
25984
  }
25985
+ getSkills(project, task) {
25986
+ const collectedSkills = [];
25987
+ const allExtensions = this.registry.getExtensions(project.baseDir);
25988
+ const extensions = this.filterEnabledExtensions(allExtensions);
25989
+ for (const loaded of extensions) {
25990
+ const { instance, metadata } = loaded;
25991
+ if (!instance.getSkills) {
25992
+ continue;
25993
+ }
25994
+ try {
25995
+ const context = new ExtensionContextImpl(loaded.id, metadata.name, this.store, this.modelManager, this.eventManager, this.memoryManager, project, task);
25996
+ const skills = instance.getSkills(context);
25997
+ if (!Array.isArray(skills)) {
25998
+ logger.error(`[Extensions] Extension '${metadata.name}' getSkills() did not return an array`);
25999
+ continue;
26000
+ }
26001
+ for (const skill of skills) {
26002
+ if (!skill.name || !skill.description) {
26003
+ logger.error(`[Extensions] Invalid skill from extension '${metadata.name}': missing name or description`);
26004
+ continue;
26005
+ }
26006
+ if (!skill.dirPath && !skill.content) {
26007
+ logger.error(`[Extensions] Invalid skill from extension '${metadata.name}': must have dirPath or content`);
26008
+ continue;
26009
+ }
26010
+ collectedSkills.push({
26011
+ ...skill,
26012
+ location: "extension"
26013
+ });
26014
+ }
26015
+ } catch (error) {
26016
+ logger.error(`[Extensions] Failed to get skills from extension '${metadata.name}':`, error);
26017
+ }
26018
+ }
26019
+ return collectedSkills;
26020
+ }
25173
26021
  getUIComponents(project, task) {
25174
26022
  const collectedComponents = [];
25175
26023
  const allExtensions = this.registry.getExtensions(project?.baseDir);
@@ -25610,6 +26458,15 @@ class ExtensionManager {
25610
26458
  throw new Error("Invalid GitHub repository URL");
25611
26459
  }
25612
26460
  await this.unloadExtensionsForDir(targetDir);
26461
+ const parsedExistingPath = path.parse(existingExtension.filePath);
26462
+ const existingIsFolder = parsedExistingPath.name === "index";
26463
+ if (extension.type === "folder" && !existingIsFolder) {
26464
+ await fs.unlink(existingExtension.filePath);
26465
+ logger.debug(`[Extensions] Removed old single-file extension: ${existingExtension.filePath}`);
26466
+ } else if (extension.type === "single" && existingIsFolder) {
26467
+ await fs.rm(parsedExistingPath.dir, { recursive: true, force: true });
26468
+ logger.debug(`[Extensions] Removed old folder extension: ${parsedExistingPath.dir}`);
26469
+ }
25613
26470
  if (extension.type === "single" && extension.file) {
25614
26471
  const url = `${githubRawBase}/${extension.file}`;
25615
26472
  const response = await fetch(url);
@@ -25780,7 +26637,7 @@ class EventsHandler {
25780
26637
  }
25781
26638
  async addOpenProject(baseDir) {
25782
26639
  const projects = this.store.getOpenProjects();
25783
- const existingProject = projects.find((p) => normalizeBaseDir(p.baseDir) === normalizeBaseDir(baseDir));
26640
+ const existingProject = projects.find((p) => compareBaseDirs$1(p.baseDir, baseDir));
25784
26641
  if (!existingProject) {
25785
26642
  logger.info("EventsHandler: addOpenProject", { baseDir });
25786
26643
  const providerModels = await this.modelManager.getProviderModels();
@@ -25797,7 +26654,7 @@ class EventsHandler {
25797
26654
  }
25798
26655
  removeOpenProject(baseDir) {
25799
26656
  const projects = this.store.getOpenProjects();
25800
- const updatedProjects = projects.filter((project) => normalizeBaseDir(project.baseDir) !== normalizeBaseDir(baseDir));
26657
+ const updatedProjects = projects.filter((project) => !compareBaseDirs$1(project.baseDir, baseDir));
25801
26658
  if (updatedProjects.length > 0) {
25802
26659
  if (!updatedProjects.some((p) => p.active)) {
25803
26660
  updatedProjects[updatedProjects.length - 1].active = true;
@@ -25847,8 +26704,8 @@ class EventsHandler {
25847
26704
  const removedIds = await this.projectManager.getProject(baseDir).getTask(taskId)?.removeMessagesUpTo(messageId) ?? [];
25848
26705
  this.eventManager.sendTaskMessageRemoved(baseDir, taskId, removedIds);
25849
26706
  }
25850
- async redoLastUserPrompt(baseDir, taskId, mode, updatedPrompt) {
25851
- void this.projectManager.getProject(baseDir).getTask(taskId)?.redoLastUserPrompt(mode, updatedPrompt);
26707
+ async redoUserPrompt(baseDir, taskId, messageId, mode, updatedPrompt) {
26708
+ void this.projectManager.getProject(baseDir).getTask(taskId)?.redoUserPrompt(messageId, mode, updatedPrompt);
25852
26709
  }
25853
26710
  async resumeTask(baseDir, taskId) {
25854
26711
  void this.projectManager.getProject(baseDir).getTask(taskId)?.resumeTask();
@@ -26077,6 +26934,13 @@ class EventsHandler {
26077
26934
  }
26078
26935
  await task.mergeWorktreeToMain(squash, targetBranch, commitMessage);
26079
26936
  }
26937
+ async mergeAndSwitchToLocal(baseDir, taskId, targetBranch) {
26938
+ const task = this.projectManager.getProject(baseDir).getTask(taskId);
26939
+ if (!task) {
26940
+ throw new Error(`Task ${taskId} not found`);
26941
+ }
26942
+ await task.mergeAndSwitchToLocal(targetBranch);
26943
+ }
26080
26944
  async applyUncommittedChanges(baseDir, taskId, targetBranch) {
26081
26945
  const task = this.projectManager.getProject(baseDir).getTask(taskId);
26082
26946
  if (!task) {
@@ -26159,6 +27023,13 @@ class EventsHandler {
26159
27023
  }
26160
27024
  await task.resolveConflictsWithAgent();
26161
27025
  }
27026
+ async renameWorktreeBranch(baseDir, taskId, newBranchName) {
27027
+ const task = this.projectManager.getProject(baseDir).getTask(taskId);
27028
+ if (!task) {
27029
+ throw new Error(`Task ${taskId} not found`);
27030
+ }
27031
+ await task.renameWorktreeBranch(newBranchName);
27032
+ }
26162
27033
  async scrapeWeb(baseDir, taskId, url, filePath) {
26163
27034
  const content = await scrapeWeb(url);
26164
27035
  const project = this.projectManager.getProject(baseDir);
@@ -26437,6 +27308,17 @@ ${error instanceof Error ? error.message : String(error)}`);
26437
27308
  async initProjectRulesFile(baseDir, taskId, args) {
26438
27309
  return this.projectManager.getProject(baseDir).getTask(taskId)?.initProjectAgentsFile(args);
26439
27310
  }
27311
+ async getSkills(baseDir, taskId) {
27312
+ return await this.projectManager.getProject(baseDir).getTask(taskId)?.getSkills() || [];
27313
+ }
27314
+ async activateSkill(baseDir, taskId, skillName) {
27315
+ await this.projectManager.getProject(baseDir).getTask(taskId)?.activateSkill(skillName);
27316
+ void this.projectManager.getProject(baseDir).getTask(taskId)?.sendSkillsUpdated();
27317
+ }
27318
+ async deactivateSkill(baseDir, taskId, skillName) {
27319
+ const removedIds = await this.projectManager.getProject(baseDir).getTask(taskId)?.deactivateSkill(skillName) ?? [];
27320
+ this.eventManager.sendTaskMessageRemoved(baseDir, taskId, removedIds);
27321
+ }
26440
27322
  async enableServer(username, password) {
26441
27323
  const currentSettings = this.store.getSettings();
26442
27324
  const updatedSettings = {
@@ -26932,6 +27814,7 @@ class PromptsManager {
26932
27814
  };
26933
27815
  getRulesContent = async (task, agentProfile) => {
26934
27816
  const ruleFiles = await task.getRuleFilesAsContextFiles(agentProfile);
27817
+ logger.debug("Rule files for prompt content:", { ruleFiles });
26935
27818
  const ruleFilesContent = await Promise.all(
26936
27819
  ruleFiles.map(async (file) => {
26937
27820
  try {
@@ -27332,7 +28215,7 @@ const initManagers = async (store, windowManager) => {
27332
28215
  windowManager
27333
28216
  );
27334
28217
  const serverController = new ServerController(httpServer, projectManager, eventsHandler, store, pythonInstaller);
27335
- const connectorManager = new ConnectorManager(httpServer, projectManager, eventManager);
28218
+ const connectorManager = new ConnectorManager(httpServer, projectManager, eventManager, store);
27336
28219
  httpServer.listen(SERVER_PORT);
27337
28220
  logger.info(`AiderDesk headless server listening on http://localhost:${SERVER_PORT}`);
27338
28221
  let cleanedUp = false;
@@ -27920,6 +28803,10 @@ const DEFAULT_SETTINGS = {
27920
28803
  enabled: false,
27921
28804
  username: "",
27922
28805
  password: ""
28806
+ },
28807
+ cors: {
28808
+ enabled: false,
28809
+ origins: []
27923
28810
  }
27924
28811
  },
27925
28812
  memory: {
@@ -27935,7 +28822,9 @@ const DEFAULT_SETTINGS = {
27935
28822
  worktreeSymlinkFolders: ["node_modules", "vendor", "__pycache__", ".venv", "venv"],
27936
28823
  contextCompactingThreshold: 0,
27937
28824
  contextCompactionType: ContextCompactionType.Compact,
27938
- defaultWorkingMode: "local"
28825
+ defaultWorkingMode: "local",
28826
+ worktreeBranchPrefix: "aider-desk/task/",
28827
+ renameBranchOnNameGeneration: true
27939
28828
  },
27940
28829
  extensions: {
27941
28830
  repositories: [AIDER_DESK_EXTENSIONS_REPO_URL],
@@ -27999,7 +28888,14 @@ class Store {
27999
28888
  }
28000
28889
  },
28001
28890
  mcpServers: settings.mcpServers || DEFAULT_SETTINGS.mcpServers,
28002
- server: settings.server || DEFAULT_SETTINGS.server,
28891
+ server: {
28892
+ ...DEFAULT_SETTINGS.server,
28893
+ ...settings.server,
28894
+ cors: {
28895
+ ...DEFAULT_SETTINGS.server.cors,
28896
+ ...settings.server?.cors
28897
+ }
28898
+ },
28003
28899
  memory: {
28004
28900
  ...DEFAULT_SETTINGS.memory,
28005
28901
  ...settings?.memory