Skip to content

Commit 81b4507

Browse files
authored
Merge pull request #1123 from shaybc/sbc_add_subtasks
subtasks alpha version (still in development)
2 parents f3b3058 + b46da7b commit 81b4507

File tree

10 files changed

+509
-183
lines changed

10 files changed

+509
-183
lines changed

src/activate/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const registerCommands = (options: RegisterCommandOptions) => {
6464
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
6565
return {
6666
"roo-cline.plusButtonClicked": async () => {
67-
await provider.clearTask()
67+
await provider.removeClineFromStack()
6868
await provider.postStateToWebview()
6969
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
7070
},

src/core/Cline.ts

Lines changed: 163 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ export type ClineOptions = {
9494

9595
export class Cline {
9696
readonly taskId: string
97+
private taskNumber: number
98+
// a flag that indicated if this Cline instance is a subtask (on finish return control to parent task)
99+
private isSubTask: boolean = false
100+
// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
101+
private isPaused: boolean = false
102+
// this is the parent task work mode when it launched the subtask to be used when it is restored (so the last used mode by parent task will also be restored)
103+
private pausedModeSlug: string = defaultModeSlug
104+
// if this is a subtask then this member holds a pointer to the parent task that launched it
105+
private parentTask: Cline | undefined = undefined
106+
// if this is a subtask then this member holds a pointer to the top parent task that launched it
107+
private rootTask: Cline | undefined = undefined
97108
readonly apiConfiguration: ApiConfiguration
98109
api: ApiHandler
99110
private terminalManager: TerminalManager
@@ -158,7 +169,7 @@ export class Cline {
158169
}
159170

160171
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
161-
172+
this.taskNumber = -1
162173
this.apiConfiguration = apiConfiguration
163174
this.api = buildApiHandler(apiConfiguration)
164175
this.terminalManager = new TerminalManager()
@@ -202,6 +213,46 @@ export class Cline {
202213
return [instance, promise]
203214
}
204215

216+
// a helper function to set the private member isSubTask to true
217+
// and by that set this Cline instance to be a subtask (on finish return control to parent task)
218+
setSubTask() {
219+
this.isSubTask = true
220+
}
221+
222+
// sets the task number (sequencial number of this task from all the subtask ran from this main task stack)
223+
setTaskNumber(taskNumber: number) {
224+
this.taskNumber = taskNumber
225+
}
226+
227+
// gets the task number, the sequencial number of this task from all the subtask ran from this main task stack
228+
getTaskNumber() {
229+
return this.taskNumber
230+
}
231+
232+
// this method returns the cline instance that is the parent task that launched this subtask (assuming this cline is a subtask)
233+
// if undefined is returned, then there is no parent task and this is not a subtask or connection has been severed
234+
getParentTask(): Cline | undefined {
235+
return this.parentTask
236+
}
237+
238+
// this method sets a cline instance that is the parent task that called this task (assuming this cline is a subtask)
239+
// if undefined is set, then the connection is broken and the parent is no longer saved in the subtask member
240+
setParentTask(parentToSet: Cline | undefined) {
241+
this.parentTask = parentToSet
242+
}
243+
244+
// this method returns the cline instance that is the root task (top most parent) that eventually launched this subtask (assuming this cline is a subtask)
245+
// if undefined is returned, then there is no root task and this is not a subtask or connection has been severed
246+
getRootTask(): Cline | undefined {
247+
return this.rootTask
248+
}
249+
250+
// this method sets a cline instance that is the root task (top most patrnt) that called this task (assuming this cline is a subtask)
251+
// if undefined is set, then the connection is broken and the root is no longer saved in the subtask member
252+
setRootTask(rootToSet: Cline | undefined) {
253+
this.rootTask = rootToSet
254+
}
255+
205256
// Add method to update diffStrategy
206257
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
207258
// If not provided, get from current state
@@ -308,6 +359,7 @@ export class Cline {
308359

309360
await this.providerRef.deref()?.updateTaskHistory({
310361
id: this.taskId,
362+
number: this.taskNumber,
311363
ts: lastRelevantMessage.ts,
312364
task: taskMessage.text ?? "",
313365
tokensIn: apiMetrics.totalTokensIn,
@@ -332,7 +384,7 @@ export class Cline {
332384
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
333385
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
334386
if (this.abort) {
335-
throw new Error("Roo Code instance aborted")
387+
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#1)`)
336388
}
337389
let askTs: number
338390
if (partial !== undefined) {
@@ -350,7 +402,7 @@ export class Cline {
350402
await this.providerRef
351403
.deref()
352404
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
353-
throw new Error("Current ask promise was ignored 1")
405+
throw new Error("Current ask promise was ignored (#1)")
354406
} else {
355407
// this is a new partial message, so add it with partial state
356408
// this.askResponse = undefined
@@ -360,7 +412,7 @@ export class Cline {
360412
this.lastMessageTs = askTs
361413
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
362414
await this.providerRef.deref()?.postStateToWebview()
363-
throw new Error("Current ask promise was ignored 2")
415+
throw new Error("Current ask promise was ignored (#2)")
364416
}
365417
} else {
366418
// partial=false means its a complete version of a previously partial message
@@ -434,7 +486,7 @@ export class Cline {
434486
checkpoint?: Record<string, unknown>,
435487
): Promise<undefined> {
436488
if (this.abort) {
437-
throw new Error("Roo Code instance aborted")
489+
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#2)`)
438490
}
439491

440492
if (partial !== undefined) {
@@ -522,6 +574,32 @@ export class Cline {
522574
])
523575
}
524576

577+
async resumePausedTask(lastMessage?: string) {
578+
// release this Cline instance from paused state
579+
this.isPaused = false
580+
581+
// fake an answer from the subtask that it has completed running and this is the result of what it has done
582+
// add the message to the chat history and to the webview ui
583+
try {
584+
await this.say("text", `${lastMessage ?? "Please continue to the next task."}`)
585+
586+
await this.addToApiConversationHistory({
587+
role: "user",
588+
content: [
589+
{
590+
type: "text",
591+
text: `[new_task completed] Result: ${lastMessage ?? "Please continue to the next task."}`,
592+
},
593+
],
594+
})
595+
} catch (error) {
596+
this.providerRef
597+
.deref()
598+
?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
599+
throw error
600+
}
601+
}
602+
525603
private async resumeTaskFromHistory() {
526604
const modifiedClineMessages = await this.getSavedClineMessages()
527605

@@ -1105,7 +1183,7 @@ export class Cline {
11051183

11061184
async presentAssistantMessage() {
11071185
if (this.abort) {
1108-
throw new Error("Roo Code instance aborted")
1186+
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#3)`)
11091187
}
11101188

11111189
if (this.presentAssistantMessageLocked) {
@@ -2565,10 +2643,7 @@ export class Cline {
25652643
}
25662644

25672645
// Switch the mode using shared handler
2568-
const provider = this.providerRef.deref()
2569-
if (provider) {
2570-
await provider.handleModeSwitch(mode_slug)
2571-
}
2646+
await this.providerRef.deref()?.handleModeSwitch(mode_slug)
25722647
pushToolResult(
25732648
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
25742649
targetMode.name
@@ -2630,19 +2705,25 @@ export class Cline {
26302705
break
26312706
}
26322707

2708+
// before switching roo mode (currently a global settings), save the current mode so we can
2709+
// resume the parent task (this Cline instance) later with the same mode
2710+
const currentMode =
2711+
(await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
2712+
this.pausedModeSlug = currentMode
2713+
26332714
// Switch mode first, then create new task instance
2634-
const provider = this.providerRef.deref()
2635-
if (provider) {
2636-
await provider.handleModeSwitch(mode)
2637-
await provider.initClineWithTask(message)
2638-
pushToolResult(
2639-
`Successfully created new task in ${targetMode.name} mode with message: ${message}`,
2640-
)
2641-
} else {
2642-
pushToolResult(
2643-
formatResponse.toolError("Failed to create new task: provider not available"),
2644-
)
2645-
}
2715+
await this.providerRef.deref()?.handleModeSwitch(mode)
2716+
// wait for mode to actually switch in UI and in State
2717+
await delay(500) // delay to allow mode change to take effect before next tool is executed
2718+
this.providerRef
2719+
.deref()
2720+
?.log(`[subtasks] Task: ${this.taskNumber} creating new task in '${mode}' mode`)
2721+
await this.providerRef.deref()?.initClineWithSubTask(message)
2722+
pushToolResult(
2723+
`Successfully created new task in ${targetMode.name} mode with message: ${message}`,
2724+
)
2725+
// set the isPaused flag to true so the parent task can wait for the sub-task to finish
2726+
this.isPaused = true
26462727
break
26472728
}
26482729
} catch (error) {
@@ -2698,6 +2779,15 @@ export class Cline {
26982779
undefined,
26992780
false,
27002781
)
2782+
2783+
if (this.isSubTask) {
2784+
// tell the provider to remove the current subtask and resume the previous task in the stack (it might decide to run the command)
2785+
await this.providerRef
2786+
.deref()
2787+
?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
2788+
break
2789+
}
2790+
27012791
await this.ask(
27022792
"command",
27032793
removeClosingTag("command", command),
@@ -2729,6 +2819,13 @@ export class Cline {
27292819
if (lastMessage && lastMessage.ask !== "command") {
27302820
// havent sent a command message yet so first send completion_result then command
27312821
await this.say("completion_result", result, undefined, false)
2822+
if (this.isSubTask) {
2823+
// tell the provider to remove the current subtask and resume the previous task in the stack
2824+
await this.providerRef
2825+
.deref()
2826+
?.finishSubTask(`Task complete: ${lastMessage?.text}`)
2827+
break
2828+
}
27322829
}
27332830

27342831
// complete command message
@@ -2746,6 +2843,13 @@ export class Cline {
27462843
commandResult = execCommandResult
27472844
} else {
27482845
await this.say("completion_result", result, undefined, false)
2846+
if (this.isSubTask) {
2847+
// tell the provider to remove the current subtask and resume the previous task in the stack
2848+
await this.providerRef
2849+
.deref()
2850+
?.finishSubTask(`Task complete: ${lastMessage?.text}`)
2851+
break
2852+
}
27492853
}
27502854

27512855
// we already sent completion_result says, an empty string asks relinquishes control over button and field
@@ -2821,12 +2925,26 @@ export class Cline {
28212925
}
28222926
}
28232927

2928+
// this function checks if this Cline instance is set to pause state and wait for being resumed,
2929+
// this is used when a sub-task is launched and the parent task is waiting for it to finish
2930+
async waitForResume() {
2931+
// wait until isPaused is false
2932+
await new Promise<void>((resolve) => {
2933+
const interval = setInterval(() => {
2934+
if (!this.isPaused) {
2935+
clearInterval(interval)
2936+
resolve()
2937+
}
2938+
}, 1000) // TBD: the 1 sec should be added to the settings, also should add a timeout to prevent infinit wait
2939+
})
2940+
}
2941+
28242942
async recursivelyMakeClineRequests(
28252943
userContent: UserContent,
28262944
includeFileDetails: boolean = false,
28272945
): Promise<boolean> {
28282946
if (this.abort) {
2829-
throw new Error("Roo Code instance aborted")
2947+
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#4)`)
28302948
}
28312949

28322950
if (this.consecutiveMistakeCount >= 3) {
@@ -2853,6 +2971,27 @@ export class Cline {
28532971
// get previous api req's index to check token usage and determine if we need to truncate conversation history
28542972
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
28552973

2974+
// in this Cline request loop, we need to check if this cline (Task) instance has been asked to wait
2975+
// for a sub-task (it has launched) to finish before continuing
2976+
if (this.isPaused) {
2977+
this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has paused`)
2978+
await this.waitForResume()
2979+
this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has resumed`)
2980+
// waiting for resume is done, resume the task mode
2981+
const currentMode = (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
2982+
if (currentMode !== this.pausedModeSlug) {
2983+
// the mode has changed, we need to switch back to the paused mode
2984+
await this.providerRef.deref()?.handleModeSwitch(this.pausedModeSlug)
2985+
// wait for mode to actually switch in UI and in State
2986+
await delay(500) // delay to allow mode change to take effect before next tool is executed
2987+
this.providerRef
2988+
.deref()
2989+
?.log(
2990+
`[subtasks] Task: ${this.taskNumber} has switched back to mode: '${this.pausedModeSlug}' from mode: '${currentMode}'`,
2991+
)
2992+
}
2993+
}
2994+
28562995
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
28572996
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
28582997
await this.say(
@@ -3042,7 +3181,7 @@ export class Cline {
30423181

30433182
// need to call here in case the stream was aborted
30443183
if (this.abort || this.abandoned) {
3045-
throw new Error("Roo Code instance aborted")
3184+
throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#5)`)
30463185
}
30473186

30483187
this.didCompleteReadingStream = true

src/core/__tests__/Cline.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ describe("Cline", () => {
237237
return [
238238
{
239239
id: "123",
240+
number: 0,
240241
ts: Date.now(),
241242
task: "historical task",
242243
tokensIn: 100,

0 commit comments

Comments
 (0)