wave-agent-sdk 0.2.1 → 0.5.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 (194) hide show
  1. package/dist/agent.d.ts +66 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +156 -83
  4. package/dist/constants/prompts.d.ts +7 -2
  5. package/dist/constants/prompts.d.ts.map +1 -1
  6. package/dist/constants/prompts.js +41 -5
  7. package/dist/constants/tools.d.ts +2 -2
  8. package/dist/constants/tools.js +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  13. package/dist/managers/MemoryRuleManager.js +16 -2
  14. package/dist/managers/aiManager.d.ts +14 -4
  15. package/dist/managers/aiManager.d.ts.map +1 -1
  16. package/dist/managers/aiManager.js +61 -9
  17. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  18. package/dist/managers/backgroundBashManager.js +1 -0
  19. package/dist/managers/backgroundTaskManager.d.ts +35 -0
  20. package/dist/managers/backgroundTaskManager.d.ts.map +1 -0
  21. package/dist/managers/backgroundTaskManager.js +249 -0
  22. package/dist/managers/bashManager.d.ts.map +1 -1
  23. package/dist/managers/bashManager.js +0 -3
  24. package/dist/managers/foregroundTaskManager.d.ts +9 -0
  25. package/dist/managers/foregroundTaskManager.d.ts.map +1 -0
  26. package/dist/managers/foregroundTaskManager.js +20 -0
  27. package/dist/managers/liveConfigManager.d.ts +1 -1
  28. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  29. package/dist/managers/lspManager.d.ts.map +1 -1
  30. package/dist/managers/lspManager.js +3 -1
  31. package/dist/managers/messageManager.d.ts +34 -4
  32. package/dist/managers/messageManager.d.ts.map +1 -1
  33. package/dist/managers/messageManager.js +104 -13
  34. package/dist/managers/permissionManager.d.ts.map +1 -1
  35. package/dist/managers/permissionManager.js +11 -13
  36. package/dist/managers/pluginManager.d.ts.map +1 -1
  37. package/dist/managers/pluginManager.js +3 -2
  38. package/dist/managers/pluginScopeManager.d.ts +13 -2
  39. package/dist/managers/pluginScopeManager.d.ts.map +1 -1
  40. package/dist/managers/pluginScopeManager.js +38 -0
  41. package/dist/managers/reversionManager.d.ts +39 -0
  42. package/dist/managers/reversionManager.d.ts.map +1 -0
  43. package/dist/managers/reversionManager.js +118 -0
  44. package/dist/managers/slashCommandManager.d.ts +4 -1
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  46. package/dist/managers/slashCommandManager.js +16 -6
  47. package/dist/managers/subagentManager.d.ts +13 -2
  48. package/dist/managers/subagentManager.d.ts.map +1 -1
  49. package/dist/managers/subagentManager.js +144 -35
  50. package/dist/managers/toolManager.d.ts +11 -1
  51. package/dist/managers/toolManager.d.ts.map +1 -1
  52. package/dist/managers/toolManager.js +11 -3
  53. package/dist/services/GitService.d.ts.map +1 -1
  54. package/dist/services/GitService.js +6 -2
  55. package/dist/services/MarketplaceService.d.ts +14 -1
  56. package/dist/services/MarketplaceService.d.ts.map +1 -1
  57. package/dist/services/MarketplaceService.js +72 -4
  58. package/dist/services/MemoryRuleService.d.ts +1 -1
  59. package/dist/services/MemoryRuleService.d.ts.map +1 -1
  60. package/dist/services/MemoryRuleService.js +13 -2
  61. package/dist/services/aiService.js +1 -1
  62. package/dist/services/configurationService.d.ts +18 -2
  63. package/dist/services/configurationService.d.ts.map +1 -1
  64. package/dist/services/configurationService.js +62 -0
  65. package/dist/services/fileWatcher.d.ts +0 -5
  66. package/dist/services/fileWatcher.d.ts.map +1 -1
  67. package/dist/services/fileWatcher.js +0 -11
  68. package/dist/services/memory.js +1 -1
  69. package/dist/services/pluginLoader.d.ts.map +1 -1
  70. package/dist/services/pluginLoader.js +6 -1
  71. package/dist/services/reversionService.d.ts +24 -0
  72. package/dist/services/reversionService.d.ts.map +1 -0
  73. package/dist/services/reversionService.js +76 -0
  74. package/dist/services/session.d.ts +7 -0
  75. package/dist/services/session.d.ts.map +1 -1
  76. package/dist/services/session.js +126 -3
  77. package/dist/tools/bashTool.d.ts +0 -8
  78. package/dist/tools/bashTool.d.ts.map +1 -1
  79. package/dist/tools/bashTool.js +52 -174
  80. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  81. package/dist/tools/deleteFileTool.js +9 -0
  82. package/dist/tools/editTool.d.ts.map +1 -1
  83. package/dist/tools/editTool.js +15 -4
  84. package/dist/tools/multiEditTool.d.ts.map +1 -1
  85. package/dist/tools/multiEditTool.js +16 -5
  86. package/dist/tools/taskOutputTool.d.ts +3 -0
  87. package/dist/tools/taskOutputTool.d.ts.map +1 -0
  88. package/dist/tools/taskOutputTool.js +149 -0
  89. package/dist/tools/taskStopTool.d.ts +3 -0
  90. package/dist/tools/taskStopTool.d.ts.map +1 -0
  91. package/dist/tools/taskStopTool.js +65 -0
  92. package/dist/tools/taskTool.d.ts.map +1 -1
  93. package/dist/tools/taskTool.js +105 -63
  94. package/dist/tools/types.d.ts +7 -0
  95. package/dist/tools/types.d.ts.map +1 -1
  96. package/dist/tools/writeTool.d.ts.map +1 -1
  97. package/dist/tools/writeTool.js +9 -0
  98. package/dist/types/commands.d.ts +1 -0
  99. package/dist/types/commands.d.ts.map +1 -1
  100. package/dist/types/configuration.d.ts +3 -0
  101. package/dist/types/configuration.d.ts.map +1 -1
  102. package/dist/types/environment.d.ts +2 -1
  103. package/dist/types/environment.d.ts.map +1 -1
  104. package/dist/types/environment.js +0 -6
  105. package/dist/types/history.d.ts +5 -0
  106. package/dist/types/history.d.ts.map +1 -0
  107. package/dist/types/history.js +1 -0
  108. package/dist/types/index.d.ts +1 -0
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/index.js +1 -0
  111. package/dist/types/marketplace.d.ts +4 -0
  112. package/dist/types/marketplace.d.ts.map +1 -1
  113. package/dist/types/messaging.d.ts +7 -1
  114. package/dist/types/messaging.d.ts.map +1 -1
  115. package/dist/types/processes.d.ts +24 -4
  116. package/dist/types/processes.d.ts.map +1 -1
  117. package/dist/types/reversion.d.ts +29 -0
  118. package/dist/types/reversion.d.ts.map +1 -0
  119. package/dist/types/reversion.js +1 -0
  120. package/dist/utils/builtinSubagents.d.ts.map +1 -1
  121. package/dist/utils/builtinSubagents.js +16 -0
  122. package/dist/utils/constants.d.ts +2 -2
  123. package/dist/utils/constants.d.ts.map +1 -1
  124. package/dist/utils/constants.js +2 -2
  125. package/dist/utils/editUtils.d.ts +4 -9
  126. package/dist/utils/editUtils.d.ts.map +1 -1
  127. package/dist/utils/editUtils.js +54 -55
  128. package/dist/utils/messageOperations.d.ts +3 -1
  129. package/dist/utils/messageOperations.d.ts.map +1 -1
  130. package/dist/utils/messageOperations.js +8 -1
  131. package/dist/utils/openaiClient.d.ts.map +1 -1
  132. package/dist/utils/openaiClient.js +56 -26
  133. package/dist/utils/promptHistory.d.ts +20 -0
  134. package/dist/utils/promptHistory.d.ts.map +1 -0
  135. package/dist/utils/promptHistory.js +117 -0
  136. package/package.json +5 -3
  137. package/src/agent.ts +193 -109
  138. package/src/constants/prompts.ts +45 -5
  139. package/src/constants/tools.ts +2 -2
  140. package/src/index.ts +1 -1
  141. package/src/managers/MemoryRuleManager.ts +18 -2
  142. package/src/managers/aiManager.ts +87 -18
  143. package/src/managers/backgroundBashManager.ts +1 -0
  144. package/src/managers/backgroundTaskManager.ts +306 -0
  145. package/src/managers/bashManager.ts +0 -4
  146. package/src/managers/foregroundTaskManager.ts +26 -0
  147. package/src/managers/liveConfigManager.ts +2 -1
  148. package/src/managers/lspManager.ts +3 -1
  149. package/src/managers/messageManager.ts +136 -18
  150. package/src/managers/permissionManager.ts +11 -13
  151. package/src/managers/pluginManager.ts +4 -3
  152. package/src/managers/pluginScopeManager.ts +57 -8
  153. package/src/managers/reversionManager.ts +152 -0
  154. package/src/managers/slashCommandManager.ts +30 -7
  155. package/src/managers/subagentManager.ts +176 -31
  156. package/src/managers/toolManager.ts +23 -4
  157. package/src/services/GitService.ts +6 -2
  158. package/src/services/MarketplaceService.ts +100 -4
  159. package/src/services/MemoryRuleService.ts +18 -6
  160. package/src/services/aiService.ts +1 -1
  161. package/src/services/configurationService.ts +79 -1
  162. package/src/services/fileWatcher.ts +0 -13
  163. package/src/services/memory.ts +1 -1
  164. package/src/services/pluginLoader.ts +7 -1
  165. package/src/services/reversionService.ts +94 -0
  166. package/src/services/session.ts +161 -3
  167. package/src/tools/bashTool.ts +73 -200
  168. package/src/tools/deleteFileTool.ts +15 -0
  169. package/src/tools/editTool.ts +20 -10
  170. package/src/tools/multiEditTool.ts +21 -11
  171. package/src/tools/taskOutputTool.ts +174 -0
  172. package/src/tools/taskStopTool.ts +72 -0
  173. package/src/tools/taskTool.ts +130 -74
  174. package/src/tools/types.ts +7 -0
  175. package/src/tools/writeTool.ts +14 -0
  176. package/src/types/commands.ts +3 -0
  177. package/src/types/configuration.ts +4 -0
  178. package/src/types/environment.ts +3 -1
  179. package/src/types/history.ts +4 -0
  180. package/src/types/index.ts +1 -0
  181. package/src/types/marketplace.ts +5 -0
  182. package/src/types/messaging.ts +9 -1
  183. package/src/types/processes.ts +33 -4
  184. package/src/types/reversion.ts +29 -0
  185. package/src/utils/builtinSubagents.ts +18 -0
  186. package/src/utils/constants.ts +2 -2
  187. package/src/utils/editUtils.ts +66 -58
  188. package/src/utils/messageOperations.ts +10 -0
  189. package/src/utils/openaiClient.ts +69 -35
  190. package/src/utils/promptHistory.ts +133 -0
  191. package/dist/utils/bashHistory.d.ts +0 -50
  192. package/dist/utils/bashHistory.d.ts.map +0 -1
  193. package/dist/utils/bashHistory.js +0 -256
  194. package/src/utils/bashHistory.ts +0 -320
@@ -6,7 +6,6 @@ import {
6
6
  import { getMessagesToCompress } from "../utils/messageOperations.js";
7
7
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
8
8
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
9
- import * as memory from "../services/memory.js";
10
9
  import * as fs from "node:fs/promises";
11
10
  import type {
12
11
  Logger,
@@ -17,7 +16,7 @@ import type {
17
16
  import type { ToolManager } from "./toolManager.js";
18
17
  import type { ToolContext, ToolResult } from "../tools/types.js";
19
18
  import type { MessageManager } from "./messageManager.js";
20
- import type { BackgroundBashManager } from "./backgroundBashManager.js";
19
+ import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
21
20
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
22
21
  import type { HookManager } from "./hookManager.js";
23
22
  import type { ExtendedHookExecutionContext } from "../types/hooks.js";
@@ -36,19 +35,21 @@ export interface AIManagerOptions {
36
35
  messageManager: MessageManager;
37
36
  toolManager: ToolManager;
38
37
  logger?: Logger;
39
- backgroundBashManager?: BackgroundBashManager;
38
+ backgroundTaskManager?: BackgroundTaskManager;
40
39
  hookManager?: HookManager;
41
40
  permissionManager?: PermissionManager;
42
41
  callbacks?: AIManagerCallbacks;
43
42
  workdir: string;
44
43
  systemPrompt?: string;
45
44
  subagentType?: string; // Optional subagent type for hook context
45
+ reversionManager?: import("./reversionManager.js").ReversionManager;
46
46
  /**Whether to use streaming mode for AI responses - defaults to true */
47
47
  stream?: boolean;
48
48
  // Dynamic configuration getters
49
49
  getGatewayConfig: () => GatewayConfig;
50
50
  getModelConfig: () => ModelConfig;
51
51
  getMaxInputTokens: () => number;
52
+ getLanguage: () => string | undefined;
52
53
  getEnvironmentVars?: () => Record<string, string>; // Get configuration environment variables for hooks
53
54
  }
54
55
 
@@ -59,8 +60,9 @@ export class AIManager {
59
60
  private logger?: Logger;
60
61
  private toolManager: ToolManager;
61
62
  private messageManager: MessageManager;
62
- private backgroundBashManager?: BackgroundBashManager;
63
+ private backgroundTaskManager?: BackgroundTaskManager;
63
64
  private hookManager?: HookManager;
65
+ private reversionManager?: import("./reversionManager.js").ReversionManager;
64
66
  private permissionManager?: PermissionManager;
65
67
  private workdir: string;
66
68
  private systemPrompt?: string;
@@ -71,13 +73,15 @@ export class AIManager {
71
73
  private getGatewayConfigFn: () => GatewayConfig;
72
74
  private getModelConfigFn: () => ModelConfig;
73
75
  private getMaxInputTokensFn: () => number;
76
+ private getLanguageFn: () => string | undefined;
74
77
  private getEnvironmentVarsFn?: () => Record<string, string>;
75
78
 
76
79
  constructor(options: AIManagerOptions) {
77
80
  this.messageManager = options.messageManager;
78
81
  this.toolManager = options.toolManager;
79
- this.backgroundBashManager = options.backgroundBashManager;
82
+ this.backgroundTaskManager = options.backgroundTaskManager;
80
83
  this.hookManager = options.hookManager;
84
+ this.reversionManager = options.reversionManager;
81
85
  this.permissionManager = options.permissionManager;
82
86
  this.logger = options.logger;
83
87
  this.workdir = options.workdir;
@@ -90,6 +94,7 @@ export class AIManager {
90
94
  this.getGatewayConfigFn = options.getGatewayConfig;
91
95
  this.getModelConfigFn = options.getModelConfig;
92
96
  this.getMaxInputTokensFn = options.getMaxInputTokens;
97
+ this.getLanguageFn = options.getLanguage;
93
98
  this.getEnvironmentVarsFn = options.getEnvironmentVars;
94
99
  }
95
100
 
@@ -106,6 +111,10 @@ export class AIManager {
106
111
  return this.getMaxInputTokensFn();
107
112
  }
108
113
 
114
+ public getLanguage(): string | undefined {
115
+ return this.getLanguageFn();
116
+ }
117
+
109
118
  private isCompressing: boolean = false;
110
119
  private callbacks: AIManagerCallbacks;
111
120
 
@@ -150,6 +159,15 @@ export class AIManager {
150
159
  this.setIsLoading(false);
151
160
  }
152
161
 
162
+ /**
163
+ * Abort the AI recursion loop immediately.
164
+ * This is used when a tool is backgrounded via Ctrl-B, even if no foreground task was active.
165
+ */
166
+ public abortRecursion(): void {
167
+ this.logger?.info("Aborting AI recursion loop");
168
+ this.abortAIMessage();
169
+ }
170
+
153
171
  // Helper method to generate compactParams
154
172
  private generateCompactParams(
155
173
  toolName: string,
@@ -267,7 +285,7 @@ export class AIManager {
267
285
  options: {
268
286
  recursionDepth?: number;
269
287
  model?: string;
270
- /** Rules for automatic tool approval (e.g., "Bash(git status:*)") */
288
+ /** Rules for automatic tool approval (e.g., "Bash(git status*)") */
271
289
  allowedRules?: string[];
272
290
  /** List of tools available to the AI (e.g., ["Bash", "Read"]) */
273
291
  tools?: string[];
@@ -287,9 +305,6 @@ export class AIManager {
287
305
  return;
288
306
  }
289
307
 
290
- // Save session in each recursion to ensure message persistence
291
- await this.messageManager.saveSession();
292
-
293
308
  // Only create new AbortControllers for the initial call (recursionDepth === 0)
294
309
  // For recursive calls, reuse existing controllers to maintain abort signal
295
310
  let abortController: AbortController;
@@ -304,10 +319,14 @@ export class AIManager {
304
319
  this.toolAbortController = toolAbortController;
305
320
  } else {
306
321
  // Reuse existing controllers for recursive calls
307
- abortController = this.abortController!;
308
- toolAbortController = this.toolAbortController!;
322
+ // Fallback to new controllers if they were cleared (should not happen in normal flow but good for tests)
323
+ abortController = this.abortController || new AbortController();
324
+ toolAbortController = this.toolAbortController || new AbortController();
309
325
  }
310
326
 
327
+ // Save session in each recursion to ensure message persistence
328
+ await this.messageManager.saveSession();
329
+
311
330
  // Only set loading state for the initial call
312
331
  if (recursionDepth === 0) {
313
332
  this.setIsLoading(true);
@@ -323,9 +342,7 @@ export class AIManager {
323
342
 
324
343
  try {
325
344
  // Get combined memory content
326
- const combinedMemory = await memory.getCombinedMemoryContent(
327
- this.workdir,
328
- );
345
+ const combinedMemory = await this.messageManager.getCombinedMemory();
329
346
 
330
347
  // Track if assistant message has been created
331
348
  let assistantMessageCreated = false;
@@ -342,6 +359,13 @@ export class AIManager {
342
359
  toolsConfig,
343
360
  );
344
361
 
362
+ // Inject language prompt if configured
363
+ const language = this.getLanguage();
364
+ if (language) {
365
+ const languagePrompt = `\n\n# Language\nAlways respond in ${language}. Technical terms (e.g., code, tool names, file paths) should remain in their original language or English where appropriate.`;
366
+ effectiveSystemPrompt = (effectiveSystemPrompt || "") + languagePrompt;
367
+ }
368
+
345
369
  if (currentMode === "plan") {
346
370
  const planFilePath = this.permissionManager?.getPlanFilePath();
347
371
  if (planFilePath) {
@@ -601,8 +625,9 @@ export class AIManager {
601
625
  // Create tool execution context
602
626
  const context: ToolContext = {
603
627
  abortSignal: toolAbortController.signal,
604
- backgroundBashManager: this.backgroundBashManager,
628
+ backgroundTaskManager: this.backgroundTaskManager,
605
629
  workdir: this.workdir,
630
+ messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
606
631
  };
607
632
 
608
633
  // Execute tool
@@ -612,6 +637,23 @@ export class AIManager {
612
637
  context,
613
638
  );
614
639
 
640
+ // Check if the tool was backgrounded via Ctrl-B
641
+ // If it was backgrounded, we should abort the AI recursion
642
+ if (
643
+ toolResult.success &&
644
+ toolResult.content.includes(
645
+ "Command was manually backgrounded by user",
646
+ )
647
+ ) {
648
+ this.logger?.info(
649
+ `Tool ${toolName} was backgrounded via Ctrl-B, aborting AI recursion`,
650
+ );
651
+ // Use abortAIMessage directly instead of abortRecursion to avoid double logging
652
+ // and ensure we don't trigger the "Request was aborted" error block
653
+ this.abortAIMessage();
654
+ return;
655
+ }
656
+
615
657
  // Update message state - tool execution completed
616
658
  this.messageManager.updateToolBlock({
617
659
  id: toolId,
@@ -666,6 +708,15 @@ export class AIManager {
666
708
 
667
709
  // Check if there are tool operations, if so automatically initiate next AI service call
668
710
  if (toolCalls.length > 0) {
711
+ // Record committed snapshots to message history
712
+ if (this.reversionManager) {
713
+ const snapshots =
714
+ this.reversionManager.getAndClearCommittedSnapshots();
715
+ if (snapshots.length > 0) {
716
+ this.messageManager.addFileHistoryBlock(snapshots);
717
+ }
718
+ }
719
+
669
720
  // Check interruption status
670
721
  const isCurrentlyAborted =
671
722
  abortController.signal.aborted || toolAbortController.signal.aborted;
@@ -682,9 +733,18 @@ export class AIManager {
682
733
  }
683
734
  }
684
735
  } catch (error) {
685
- this.messageManager.addErrorBlock(
686
- error instanceof Error ? error.message : "Unknown error occurred",
687
- );
736
+ // Check if the error is an abort error
737
+ // Use the local variables to avoid null reference if this.abortController was cleared
738
+ const isCurrentlyAborted =
739
+ abortController.signal.aborted || toolAbortController.signal.aborted;
740
+
741
+ if (isCurrentlyAborted) {
742
+ this.logger?.info("AI message processing was aborted");
743
+ } else {
744
+ this.messageManager.addErrorBlock(
745
+ error instanceof Error ? error.message : "Unknown error occurred",
746
+ );
747
+ }
688
748
  } finally {
689
749
  // Only execute cleanup and hooks for the initial call
690
750
  if (recursionDepth === 0) {
@@ -705,6 +765,15 @@ export class AIManager {
705
765
  abortController.signal.aborted || toolAbortController.signal.aborted;
706
766
 
707
767
  if (!isCurrentlyAborted) {
768
+ // Record committed snapshots to message history for the final turn
769
+ if (this.reversionManager) {
770
+ const snapshots =
771
+ this.reversionManager.getAndClearCommittedSnapshots();
772
+ if (snapshots.length > 0) {
773
+ this.messageManager.addFileHistoryBlock(snapshots);
774
+ }
775
+ }
776
+
708
777
  const shouldContinue = await this.executeStopHooks();
709
778
 
710
779
  // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
@@ -42,6 +42,7 @@ export class BackgroundBashManager {
42
42
 
43
43
  const shell: BackgroundShell = {
44
44
  id,
45
+ type: "shell",
45
46
  process: child,
46
47
  command,
47
48
  startTime,
@@ -0,0 +1,306 @@
1
+ import { spawn, type ChildProcess } from "child_process";
2
+ import { BackgroundTask, BackgroundShell } from "../types/processes.js";
3
+ import { stripAnsiColors } from "../utils/stringUtils.js";
4
+ import { logger } from "../utils/globalLogger.js";
5
+
6
+ export interface BackgroundTaskManagerCallbacks {
7
+ onTasksChange?: (tasks: BackgroundTask[]) => void;
8
+ }
9
+
10
+ export interface BackgroundTaskManagerOptions {
11
+ callbacks?: BackgroundTaskManagerCallbacks;
12
+ workdir: string;
13
+ }
14
+
15
+ export class BackgroundTaskManager {
16
+ private tasks = new Map<string, BackgroundTask>();
17
+ private nextId = 1;
18
+ private callbacks: BackgroundTaskManagerCallbacks;
19
+ private workdir: string;
20
+
21
+ constructor(options: BackgroundTaskManagerOptions) {
22
+ this.callbacks = options.callbacks || {};
23
+ this.workdir = options.workdir;
24
+ }
25
+
26
+ private notifyTasksChange(): void {
27
+ this.callbacks.onTasksChange?.(Array.from(this.tasks.values()));
28
+ }
29
+
30
+ public generateId(): string {
31
+ return `task_${this.nextId++}`;
32
+ }
33
+
34
+ public addTask(task: BackgroundTask): void {
35
+ this.tasks.set(task.id, task);
36
+ this.notifyTasksChange();
37
+ }
38
+
39
+ public getTask(id: string): BackgroundTask | undefined {
40
+ return this.tasks.get(id);
41
+ }
42
+
43
+ public getAllTasks(): BackgroundTask[] {
44
+ return Array.from(this.tasks.values());
45
+ }
46
+
47
+ public startShell(
48
+ command: string,
49
+ timeout?: number,
50
+ ): { id: string; child: ChildProcess; detach: () => void } {
51
+ const id = this.generateId();
52
+ const startTime = Date.now();
53
+
54
+ const child = spawn(command, {
55
+ shell: true,
56
+ stdio: "pipe",
57
+ cwd: this.workdir,
58
+ env: {
59
+ ...process.env,
60
+ },
61
+ });
62
+
63
+ const shell: BackgroundShell = {
64
+ id,
65
+ type: "shell",
66
+ process: child,
67
+ command,
68
+ startTime,
69
+ status: "running",
70
+ stdout: "",
71
+ stderr: "",
72
+ };
73
+
74
+ this.tasks.set(id, shell);
75
+ this.notifyTasksChange();
76
+
77
+ // Set up timeout if specified
78
+ let timeoutHandle: NodeJS.Timeout | undefined;
79
+ if (timeout && timeout > 0) {
80
+ timeoutHandle = setTimeout(() => {
81
+ if (shell.status === "running") {
82
+ this.stopTask(id);
83
+ }
84
+ }, timeout);
85
+ }
86
+
87
+ const onStdout = (data: Buffer | string) => {
88
+ shell.stdout += stripAnsiColors(data.toString());
89
+ this.notifyTasksChange();
90
+ };
91
+
92
+ const onStderr = (data: Buffer | string) => {
93
+ shell.stderr += stripAnsiColors(data.toString());
94
+ this.notifyTasksChange();
95
+ };
96
+
97
+ const onExit = (code: number | null) => {
98
+ if (timeoutHandle) {
99
+ clearTimeout(timeoutHandle);
100
+ }
101
+ shell.status = code === 0 ? "completed" : "failed";
102
+ shell.exitCode = code ?? 0;
103
+ shell.endTime = Date.now();
104
+ shell.runtime = shell.endTime - startTime;
105
+ this.notifyTasksChange();
106
+ };
107
+
108
+ const onError = (error: Error) => {
109
+ if (timeoutHandle) {
110
+ clearTimeout(timeoutHandle);
111
+ }
112
+ shell.status = "failed";
113
+ shell.stderr += `\nProcess error: ${stripAnsiColors(error.message)}`;
114
+ shell.exitCode = 1;
115
+ shell.endTime = Date.now();
116
+ shell.runtime = shell.endTime - startTime;
117
+ this.notifyTasksChange();
118
+ };
119
+
120
+ child.stdout?.on("data", onStdout);
121
+ child.stderr?.on("data", onStderr);
122
+ child.on("exit", onExit);
123
+ child.on("error", onError);
124
+
125
+ const detach = () => {
126
+ child.stdout?.off("data", onStdout);
127
+ child.stderr?.off("data", onStderr);
128
+ child.off("exit", onExit);
129
+ child.off("error", onError);
130
+ if (timeoutHandle) {
131
+ clearTimeout(timeoutHandle);
132
+ }
133
+ this.tasks.delete(id);
134
+ this.notifyTasksChange();
135
+ };
136
+
137
+ return { id, child, detach };
138
+ }
139
+
140
+ public adoptProcess(
141
+ child: ChildProcess,
142
+ command: string,
143
+ initialStdout: string = "",
144
+ initialStderr: string = "",
145
+ ): string {
146
+ const id = this.generateId();
147
+ const startTime = Date.now();
148
+
149
+ const shell: BackgroundShell = {
150
+ id,
151
+ type: "shell",
152
+ process: child,
153
+ command,
154
+ startTime,
155
+ status: "running",
156
+ stdout: initialStdout,
157
+ stderr: initialStderr,
158
+ };
159
+
160
+ this.tasks.set(id, shell);
161
+ this.notifyTasksChange();
162
+
163
+ child.stdout?.on("data", (data) => {
164
+ shell.stdout += stripAnsiColors(data.toString());
165
+ this.notifyTasksChange();
166
+ });
167
+
168
+ child.stderr?.on("data", (data) => {
169
+ shell.stderr += stripAnsiColors(data.toString());
170
+ this.notifyTasksChange();
171
+ });
172
+
173
+ child.on("exit", (code) => {
174
+ shell.status = code === 0 ? "completed" : "failed";
175
+ shell.exitCode = code ?? 0;
176
+ shell.endTime = Date.now();
177
+ shell.runtime = shell.endTime - startTime;
178
+ this.notifyTasksChange();
179
+ });
180
+
181
+ child.on("error", (error) => {
182
+ shell.status = "failed";
183
+ shell.stderr += `\nProcess error: ${stripAnsiColors(error.message)}`;
184
+ shell.exitCode = 1;
185
+ shell.endTime = Date.now();
186
+ shell.runtime = shell.endTime - startTime;
187
+ this.notifyTasksChange();
188
+ });
189
+
190
+ return id;
191
+ }
192
+
193
+ public getOutput(
194
+ id: string,
195
+ filter?: string,
196
+ ): { stdout: string; stderr: string; status: string } | null {
197
+ const task = this.tasks.get(id);
198
+ if (!task) {
199
+ return null;
200
+ }
201
+
202
+ let stdout = task.stdout;
203
+ let stderr = task.stderr;
204
+
205
+ // Apply regex filter if provided
206
+ if (filter) {
207
+ try {
208
+ const regex = new RegExp(filter);
209
+ stdout = stdout
210
+ .split("\n")
211
+ .filter((line) => regex.test(line))
212
+ .join("\n");
213
+ stderr = stderr
214
+ .split("\n")
215
+ .filter((line) => regex.test(line))
216
+ .join("\n");
217
+ } catch (error) {
218
+ logger.warn(`Invalid filter regex: ${filter}`, error);
219
+ }
220
+ }
221
+
222
+ return {
223
+ stdout,
224
+ stderr,
225
+ status: task.status,
226
+ };
227
+ }
228
+
229
+ public stopTask(id: string): boolean {
230
+ const task = this.tasks.get(id);
231
+ if (!task || task.status !== "running") {
232
+ return false;
233
+ }
234
+
235
+ if (task.type === "shell") {
236
+ const shell = task as BackgroundShell;
237
+ try {
238
+ // Try to kill process group first
239
+ if (shell.process.pid) {
240
+ process.kill(-shell.process.pid, "SIGTERM");
241
+
242
+ // Force kill after timeout
243
+ setTimeout(() => {
244
+ if (
245
+ shell.status === "running" &&
246
+ shell.process.pid &&
247
+ !shell.process.killed
248
+ ) {
249
+ try {
250
+ process.kill(-shell.process.pid, "SIGKILL");
251
+ } catch (error) {
252
+ logger.error("Failed to force kill process:", error);
253
+ }
254
+ }
255
+ }, 1000);
256
+ }
257
+
258
+ shell.status = "killed";
259
+ shell.endTime = Date.now();
260
+ shell.runtime = shell.endTime - shell.startTime;
261
+ this.notifyTasksChange();
262
+ return true;
263
+ } catch {
264
+ // Fallback to direct process kill
265
+ try {
266
+ shell.process.kill("SIGTERM");
267
+ setTimeout(() => {
268
+ if (!shell.process.killed) {
269
+ shell.process.kill("SIGKILL");
270
+ }
271
+ }, 1000);
272
+ shell.status = "killed";
273
+ shell.endTime = Date.now();
274
+ shell.runtime = shell.endTime - shell.startTime;
275
+ this.notifyTasksChange();
276
+ return true;
277
+ } catch (directKillError) {
278
+ logger.error("Failed to kill child process:", directKillError);
279
+ return false;
280
+ }
281
+ }
282
+ } else if (task.type === "subagent") {
283
+ // Subagent termination logic will be handled by aborting the AI loop
284
+ // which is already managed by the SubagentManager and AIManager.
285
+ // Here we just update the status.
286
+ task.status = "killed";
287
+ task.endTime = Date.now();
288
+ task.runtime = task.endTime - task.startTime;
289
+ this.notifyTasksChange();
290
+ return true;
291
+ }
292
+
293
+ return false;
294
+ }
295
+
296
+ public cleanup(): void {
297
+ // Kill all running tasks
298
+ for (const [id, task] of this.tasks) {
299
+ if (task.status === "running") {
300
+ this.stopTask(id);
301
+ }
302
+ }
303
+ this.tasks.clear();
304
+ this.notifyTasksChange();
305
+ }
306
+ }
@@ -1,5 +1,4 @@
1
1
  import { spawn, type ChildProcess } from "child_process";
2
- import { addBashCommandToHistory } from "../utils/bashHistory.js";
3
2
  import type { MessageManager } from "./messageManager.js";
4
3
 
5
4
  export interface BashManagerOptions {
@@ -66,9 +65,6 @@ export class BashManager {
66
65
  child.on("exit", (code, signal) => {
67
66
  const exitCode = code === null && signal ? 130 : (code ?? 0);
68
67
 
69
- // Add command to bash history
70
- addBashCommandToHistory(command, this.workdir);
71
-
72
68
  this.messageManager.completeCommandMessage(command, exitCode);
73
69
 
74
70
  this.setCommandRunning(false);
@@ -0,0 +1,26 @@
1
+ import { ForegroundTask, IForegroundTaskManager } from "../types/processes.js";
2
+
3
+ export class ForegroundTaskManager implements IForegroundTaskManager {
4
+ private activeForegroundTasks: ForegroundTask[] = [];
5
+
6
+ public registerForegroundTask(task: ForegroundTask): void {
7
+ this.activeForegroundTasks.push(task);
8
+ }
9
+
10
+ public unregisterForegroundTask(id: string): void {
11
+ this.activeForegroundTasks = this.activeForegroundTasks.filter(
12
+ (t) => t.id !== id,
13
+ );
14
+ }
15
+
16
+ public async backgroundCurrentTask(): Promise<void> {
17
+ const task = this.activeForegroundTasks.pop();
18
+ if (task) {
19
+ await task.backgroundHandler();
20
+ }
21
+ }
22
+
23
+ public hasActiveTasks(): boolean {
24
+ return this.activeForegroundTasks.length > 0;
25
+ }
26
+ }
@@ -10,6 +10,7 @@
10
10
  import { existsSync } from "fs";
11
11
  import type { Logger } from "../types/index.js";
12
12
  import type { PermissionMode } from "../types/permissions.js";
13
+ import type { Scope } from "../types/configuration.js";
13
14
  import {
14
15
  FileWatcherService,
15
16
  type FileWatchEvent,
@@ -452,7 +453,7 @@ export class LiveConfigManager {
452
453
 
453
454
  private async handleFileChange(
454
455
  event: FileWatchEvent,
455
- source: "user" | "project",
456
+ source: Scope,
456
457
  ): Promise<void> {
457
458
  this.logger?.debug(
458
459
  `Live Config: File ${event.type} detected for ${source} config: ${event.path}`,
@@ -418,7 +418,9 @@ export class LspManager implements ILspManager {
418
418
  await this.sendRequest(lspProc, "shutdown", {}, timeout);
419
419
  await this.sendNotification(lspProc, "exit", {});
420
420
  // Give it a moment to exit
421
- await new Promise((resolve) => setTimeout(resolve, 100));
421
+ if (timeout > 100) {
422
+ await new Promise((resolve) => setTimeout(resolve, 100));
423
+ }
422
424
  } catch (error) {
423
425
  this.logger?.debug(
424
426
  `Failed to gracefully shutdown LSP for ${language}: ${error}`,