Skip to content

Commit df09e55

Browse files
committed
feat(chat): port concurrent conversations
1 parent d2fb21e commit df09e55

9 files changed

Lines changed: 1286 additions & 462 deletions

File tree

packages/types/src/task.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface TaskProviderLike {
2222
options?: CreateTaskOptions,
2323
configuration?: RooCodeSettings,
2424
): Promise<TaskLike>
25-
cancelTask(): Promise<void>
25+
cancelTask(taskId?: string): Promise<void>
2626
clearTask(): Promise<void>
2727
resumeTask(taskId: string): void
2828

@@ -94,6 +94,8 @@ export interface CreateTaskOptions {
9494
consecutiveMistakeLimit?: number
9595
experiments?: Record<string, boolean>
9696
initialTodos?: TodoItem[]
97+
/** Whether the task should become the visible task in the UI (default: true). */
98+
focus?: boolean
9799
/** Initial status for the task's history item (e.g., "active" for child tasks) */
98100
initialStatus?: "active" | "delegated" | "completed"
99101
/** Whether to start the task loop immediately (default: true).

packages/types/src/vscode-extension-host.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ export interface OpenAiCodexRateLimitsMessage {
245245
error?: string
246246
}
247247

248+
export interface ActiveConversationSummary {
249+
rootTaskId: string
250+
activeTaskId: string
251+
rootTask: string
252+
activeTask: string
253+
ts: number
254+
status: "running" | "interactive" | "resumable" | "idle" | "none"
255+
parentTaskId?: string
256+
queuedMessageCount: number
257+
steerMessageCount: number
258+
}
259+
248260
export type ExtensionState = Pick<
249261
GlobalSettings,
250262
| "currentApiConfigName"
@@ -315,6 +327,7 @@ export type ExtensionState = Pick<
315327
apiConfiguration: ProviderSettings
316328
uriScheme?: string
317329
shouldShowAnnouncement: boolean
330+
activeConversations?: ActiveConversationSummary[]
318331

319332
taskHistory: HistoryItem[]
320333

src/core/task/Task.ts

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type ClineMessage,
3030
type ClineSay,
3131
type ClineAsk,
32+
type ExtensionMessage,
3233
type ToolProgressStatus,
3334
type HistoryItem,
3435
type CreateTaskOptions,
@@ -523,7 +524,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
523524
this.messageQueueStateChangedHandler = () => {
524525
this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
525526
this.emit(RooCodeEventName.QueuedMessagesUpdated, this.taskId, this.messageQueueService.messages)
526-
this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
527+
this.postTaskStateToWebview().catch(() => undefined)
527528
}
528529

529530
this.messageQueueService.on("stateChanged", this.messageQueueStateChangedHandler)
@@ -666,9 +667,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
666667

667668
this.providerProfileChangeListener = async () => {
668669
try {
670+
if (!provider.isTaskVisible(this.taskId)) {
671+
return
672+
}
669673
const newState = await provider.getState()
670674
if (newState?.apiConfiguration) {
671675
this.updateApiConfiguration(newState.apiConfiguration)
676+
this.setTaskApiConfigName(newState.currentApiConfigName)
672677
}
673678
} catch (error) {
674679
console.error(
@@ -681,6 +686,50 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
681686
provider.on(RooCodeEventName.ProviderProfileChanged, this.providerProfileChangeListener)
682687
}
683688

689+
private async getTaskScopedState(): Promise<Awaited<ReturnType<ClineProvider["getState"]>> | undefined> {
690+
const provider = this.providerRef.deref()
691+
if (!provider) {
692+
return undefined
693+
}
694+
695+
const state = await provider.getState()
696+
697+
return {
698+
...state,
699+
mode: await this.getTaskMode().catch(() => state.mode ?? defaultModeSlug),
700+
apiConfiguration: this.apiConfiguration,
701+
currentApiConfigName: await this.getTaskApiConfigName().catch(() => state.currentApiConfigName),
702+
}
703+
}
704+
705+
private async postTaskStateToWebview(): Promise<void> {
706+
const provider = this.providerRef.deref()
707+
if (!provider) {
708+
return
709+
}
710+
711+
if (typeof provider.postTaskStateToWebview === "function") {
712+
await provider.postTaskStateToWebview(this.taskId)
713+
return
714+
}
715+
716+
await provider.postStateToWebviewWithoutTaskHistory?.()
717+
}
718+
719+
private async postTaskMessageToWebview(message: ExtensionMessage): Promise<void> {
720+
const provider = this.providerRef.deref()
721+
if (!provider) {
722+
return
723+
}
724+
725+
if (typeof provider.postTaskMessageToWebview === "function") {
726+
await provider.postTaskMessageToWebview(this.taskId, message)
727+
return
728+
}
729+
730+
await provider.postMessageToWebview?.(message)
731+
}
732+
684733
/**
685734
* Wait for the task mode to be initialized before proceeding.
686735
* This method ensures that any operations depending on the task mode
@@ -1155,10 +1204,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
11551204

11561205
private async addToClineMessages(message: ClineMessage) {
11571206
this.clineMessages.push(message)
1158-
const provider = this.providerRef.deref()
11591207
// Avoid resending large, mostly-static fields (notably taskHistory) on every chat message update.
11601208
// taskHistory is maintained in-memory in the webview and updated via taskHistoryItemUpdated.
1161-
await provider?.postStateToWebviewWithoutTaskHistory()
1209+
await this.postTaskStateToWebview()
11621210
this.emit(RooCodeEventName.Message, { action: "created", message })
11631211
await this.saveClineMessages()
11641212

@@ -1190,8 +1238,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
11901238
}
11911239

11921240
private async updateClineMessage(message: ClineMessage) {
1193-
const provider = this.providerRef.deref()
1194-
await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message })
1241+
await this.postTaskMessageToWebview({ type: "messageUpdated", clineMessage: message })
11951242
this.emit(RooCodeEventName.Message, { action: "updated", message })
11961243

11971244
// Check if we should sync to cloud and haven't already synced this message
@@ -1362,7 +1409,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
13621409

13631410
// Automatically approve if the ask according to the user's settings.
13641411
const provider = this.providerRef.deref()
1365-
const state = provider ? await provider.getState() : undefined
1412+
const state = await this.getTaskScopedState()
13661413
const approval = await checkAutoApproval({ state, ask: type, text, isProtected })
13671414

13681415
if (approval.decision === "approve") {
@@ -1399,7 +1446,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
13991446
if (message) {
14001447
this.interactiveAsk = message
14011448
this.emit(RooCodeEventName.TaskInteractive, this.taskId)
1402-
provider?.postMessageToWebview({ type: "interactionRequired" })
1449+
this.postTaskMessageToWebview({ type: "interactionRequired" }).catch(() => undefined)
14031450
}
14041451
}, statusMutationTimeout),
14051452
)
@@ -1608,7 +1655,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16081655

16091656
// Update this task's API configuration to match the new profile
16101657
// This ensures the parser state is synchronized with the selected model
1611-
const newState = await provider.getState()
1658+
const newState = await this.getTaskScopedState()
16121659
if (newState?.apiConfiguration) {
16131660
this.updateApiConfiguration(newState.apiConfiguration)
16141661
}
@@ -1653,7 +1700,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16531700
const systemPrompt = await this.getSystemPrompt()
16541701

16551702
// Get condensing configuration
1656-
const state = await this.providerRef.deref()?.getState()
1703+
const state = await this.getTaskScopedState()
16571704
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
16581705
const { mode, apiConfiguration } = state ?? {}
16591706

@@ -1892,7 +1939,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
18921939
return { enabledToolCount: 0, enabledServerCount: 0 }
18931940
}
18941941

1895-
const { mcpEnabled } = (await provider.getState()) ?? {}
1942+
const { mcpEnabled } = (await this.getTaskScopedState()) ?? {}
18961943
if (!(mcpEnabled ?? true)) {
18971944
return { enabledToolCount: 0, enabledServerCount: 0 }
18981945
}
@@ -1948,7 +1995,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
19481995
// The todo list is already set in the constructor if initialTodos were provided
19491996
// No need to add any messages - the todoList property is already set
19501997

1951-
await this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
1998+
await this.postTaskStateToWebview()
19521999

19532000
await this.say("text", task, images)
19542001

@@ -2593,7 +2640,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
25932640
)
25942641

25952642
const provider = this.providerRef.deref()
2596-
const state = provider ? await provider.getState() : undefined
2643+
const state = await this.getTaskScopedState()
25972644

25982645
const showRooIgnoredFiles = state?.showRooIgnoredFiles ?? false
25992646
const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true
@@ -2616,7 +2663,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26162663
if (slashCommandMode) {
26172664
const provider = this.providerRef.deref()
26182665
if (provider) {
2619-
const state = await provider.getState()
2666+
const state = await this.getTaskScopedState()
26202667
const targetMode = getModeBySlug(slashCommandMode, state?.customModes)
26212668
if (targetMode) {
26222669
await provider.handleModeSwitch(slashCommandMode)
@@ -2671,7 +2718,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26712718
} satisfies ClineApiReqInfo)
26722719

26732720
await this.saveClineMessages()
2674-
await this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
2721+
await this.postTaskStateToWebview()
26752722

26762723
try {
26772724
let cacheWriteTokens = 0
@@ -3300,7 +3347,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
33003347
)
33013348

33023349
// Apply exponential backoff similar to first-chunk errors when auto-resubmit is enabled
3303-
const stateForBackoff = await this.providerRef.deref()?.getState()
3350+
const stateForBackoff = await this.getTaskScopedState()
33043351
if (stateForBackoff?.autoApprovalEnabled) {
33053352
await this.backoffAndAnnounce(currentItem.retryAttempt ?? 0, error)
33063353

@@ -3432,7 +3479,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34323479
}
34333480

34343481
await this.saveClineMessages()
3435-
await this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
3482+
await this.postTaskStateToWebview()
34363483

34373484
// No legacy text-stream tool parser state to reset.
34383485

@@ -3676,7 +3723,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
36763723
// apiConversationHistory at line 1876. Since the assistant failed to respond,
36773724
// we need to remove that message before retrying to avoid having two consecutive
36783725
// user messages (which would cause tool_result validation errors).
3679-
let state = await this.providerRef.deref()?.getState()
3726+
let state = await this.getTaskScopedState()
36803727
if (this.apiConversationHistory.length > 0) {
36813728
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
36823729
if (lastMessage.role === "user") {
@@ -3773,7 +3820,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
37733820
}
37743821

37753822
private async getSystemPrompt(): Promise<string> {
3776-
const { mcpEnabled } = (await this.providerRef.deref()?.getState()) ?? {}
3823+
const state = await this.getTaskScopedState()
3824+
const { mcpEnabled } = state ?? {}
37773825
let mcpHub: McpHub | undefined
37783826
if (mcpEnabled ?? true) {
37793827
const provider = this.providerRef.deref()
@@ -3797,8 +3845,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
37973845

37983846
const rooIgnoreInstructions = this.rooIgnoreController?.getInstructions()
37993847

3800-
const state = await this.providerRef.deref()?.getState()
3801-
38023848
const {
38033849
mode,
38043850
customModes,
@@ -3857,7 +3903,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38573903
}
38583904

38593905
private async handleContextWindowExceededError(): Promise<void> {
3860-
const state = await this.providerRef.deref()?.getState()
3906+
const state = await this.getTaskScopedState()
38613907
const { profileThresholds = {}, mode, apiConfiguration } = state ?? {}
38623908

38633909
const { contextTokens } = this.getTokenUsage()
@@ -3881,7 +3927,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38813927
`Forcing truncation to ${FORCED_CONTEXT_REDUCTION_PERCENT}% of current context.`,
38823928
)
38833929
// Send condenseTaskContextStarted to show in-progress indicator
3884-
await this.providerRef.deref()?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
3930+
await this.postTaskMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
38853931

38863932
// Build tools for condensing metadata (same tools used for normal API calls)
38873933
const provider = this.providerRef.deref()
@@ -3975,9 +4021,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39754021
} finally {
39764022
// Notify webview that context management is complete (removes in-progress spinner)
39774023
// IMPORTANT: Must always be sent to dismiss the spinner, even on error
3978-
await this.providerRef
3979-
.deref()
3980-
?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
4024+
await this.postTaskMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
39814025
}
39824026
}
39834027

@@ -3988,7 +4032,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39884032
* the `api_req_rate_limit_wait` say type (not an error).
39894033
*/
39904034
private async maybeWaitForProviderRateLimit(retryAttempt: number): Promise<void> {
3991-
const state = await this.providerRef.deref()?.getState()
4035+
const state = await this.getTaskScopedState()
39924036
const rateLimitSeconds =
39934037
state?.apiConfiguration?.rateLimitSeconds ?? this.apiConfiguration?.rateLimitSeconds ?? 0
39944038

@@ -4019,7 +4063,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
40194063
retryAttempt: number = 0,
40204064
options: { skipProviderRateLimit?: boolean } = {},
40214065
): ApiStream {
4022-
const state = await this.providerRef.deref()?.getState()
4066+
const state = await this.getTaskScopedState()
40234067

40244068
const {
40254069
apiConfiguration,
@@ -4090,9 +4134,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
40904134
// This notification must be sent here (not earlier) because the early check uses stale token count
40914135
// (before user message is added to history), which could incorrectly skip showing the indicator
40924136
if (contextManagementWillRun && autoCondenseContext) {
4093-
await this.providerRef
4094-
.deref()
4095-
?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
4137+
await this.postTaskMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
40964138
}
40974139

40984140
// Build tools for condensing metadata (same tools used for normal API calls)
@@ -4212,9 +4254,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
42124254
// This removes the in-progress spinner and allows the completed result to show
42134255
// IMPORTANT: Must always be sent to dismiss the spinner, even on error
42144256
if (contextManagementWillRun && autoCondenseContext) {
4215-
await this.providerRef
4216-
.deref()
4217-
?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
4257+
await this.postTaskMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
42184258
}
42194259
}
42204260
}
@@ -4409,7 +4449,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
44094449
// Shared exponential backoff for retries (first-chunk and mid-stream)
44104450
private async backoffAndAnnounce(retryAttempt: number, error: any): Promise<void> {
44114451
try {
4412-
const state = await this.providerRef.deref()?.getState()
4452+
const state = await this.getTaskScopedState()
44134453
const baseDelay = state?.requestDelaySeconds || 5
44144454

44154455
let exponentialDelay = Math.min(

0 commit comments

Comments
 (0)