Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import { IChatWidgetService } from '../chat.js';
import { CHAT_SETUP_ACTION_ID } from './chatActions.js';
import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
import { NEW_CHAT_SESSION_ACTION_ID } from '../chatSessions/common.js';
import { isResponseVM } from '../../common/chatViewModel.js';

export const enum ActionLocation {
ChatWidget = 'chatWidget',
Expand Down Expand Up @@ -285,57 +284,7 @@ class CreateRemoteAgentJobAction {
});

if (requestData) {
await requestData.responseCompletePromise;

const checkAndClose = () => {
const items = widget.viewModel?.getItems() ?? [];
const lastItem = items[items.length - 1];

if (lastItem && isResponseVM(lastItem) && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {
return true;
}
return false;
};

if (checkAndClose()) {
await widget.clear();
return;
}

// Monitor subsequent responses when pending confirmations block us from closing
await new Promise<void>((resolve, reject) => {
let disposed = false;
let disposable: IDisposable | undefined;
let timeout: ReturnType<typeof setTimeout> | undefined;
const cleanup = () => {
if (!disposed) {
disposed = true;
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (disposable) {
disposable.dispose();
}
}
};
try {
disposable = widget.viewModel!.onDidChange(() => {
if (checkAndClose()) {
cleanup();
resolve();
}
});
timeout = setTimeout(() => {
cleanup();
resolve();
}, 30_000); // 30 second timeout
} catch (e) {
cleanup();
reject(e);
}
});

await widget.clear();
await widget.handleDelegationExitIfNeeded(requestData.agent);
}
} catch (e) {
console.error('Error creating remote coding agent job', e);
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/chat/browser/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export interface IChatWidget {
clear(): Promise<void>;
getViewState(): IChatModelInputState | undefined;
lockToCodingAgent(name: string, displayName: string, agentId?: string): void;
handleDelegationExitIfNeeded(agent: IChatAgentData | undefined): Promise<void>;

delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,15 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
.filter(contribution => this._isContributionAvailable(contribution));
}

getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
const contribution = this._contributions.get(chatSessionType)?.contribution;
if (!contribution) {
return undefined;
}

return this._isContributionAvailable(contribution) ? contribution : undefined;
}

getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
return [...this._itemsProviders.values()].filter(provider => {
// Check if the provider's corresponding contribution is available
Expand Down
123 changes: 83 additions & 40 deletions src/vs/workbench/contrib/chat/browser/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1315,46 +1315,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.input.setValue(`@${agentId} ${promptToUse}`, false);
this.input.focus();
// Auto-submit for delegated chat sessions
this.acceptInput().then(async (response) => {
if (!response || !this.viewModel) {
return;
}

// Wait for response to complete without any user-pending confirmations
const checkForComplete = () => {
const items = this.viewModel?.getItems() ?? [];
const lastItem = items[items.length - 1];
if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {
return true;
}
return false;
};

if (checkForComplete()) {
await this.clear();
return;
}

await new Promise<void>(resolve => {
const disposable = this.viewModel!.onDidChange(() => {
if (checkForComplete()) {
cleanup();
resolve();
}
});
const timeout = setTimeout(() => {
cleanup();
resolve();
}, 30000); // 30 second timeout
const cleanup = () => {
clearTimeout(timeout);
disposable.dispose();
};
});

// Clear parent editor
await this.clear();
}).catch(e => this.logService.error('Failed to handle handoff continueOn', e));
this.acceptInput().catch(e => this.logService.error('Failed to handle handoff continueOn', e));
} else if (handoff.agent) {
// Regular handoff to specified agent
this._switchToAgentByName(handoff.agent);
Expand All @@ -1369,6 +1330,87 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
}

async handleDelegationExitIfNeeded(agent: IChatAgentData | undefined): Promise<void> {
if (!this._shouldExitAfterDelegation(agent)) {
return;
}

try {
await this._handleDelegationExit();
} catch (e) {
this.logService.error('Failed to handle delegation exit', e);
}
}

private _shouldExitAfterDelegation(agent: IChatAgentData | undefined): boolean {
if (!agent) {
return false;
}

if (!isIChatViewViewContext(this.viewContext)) {
return false;
}

const contribution = this.chatSessionsService.getChatSessionContribution(agent.id);
if (!contribution) {
return false;
}

if (contribution.canDelegate !== true) {
return false;
}

return true;
}

/**
* Handles the exit of the panel chat when a delegation to another session occurs.
* Waits for the response to complete and any pending confirmations to be resolved,
* then clears the widget.
*/
private async _handleDelegationExit(): Promise<void> {
const viewModel = this.viewModel;
if (!viewModel) {
return;
}

// Check if response is already complete without pending confirmations
const checkForComplete = () => {
const items = viewModel.getItems();
const lastItem = items[items.length - 1];
if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {
return true;
}
return false;
};

if (checkForComplete()) {
await this.clear();
return;
}

// Wait for response to complete with a timeout
await new Promise<void>(resolve => {
const disposable = viewModel.onDidChange(() => {
if (checkForComplete()) {
cleanup();
resolve();
}
});
const timeout = setTimeout(() => {
cleanup();
resolve();
}, 30_000); // 30 second timeout
const cleanup = () => {
clearTimeout(timeout);
disposable.dispose();
};
});

// Clear the widget after delegation completes
await this.clear();
}

setVisible(visible: boolean): void {
const wasVisible = this._visible;
this._visible = visible;
Expand Down Expand Up @@ -2283,6 +2325,7 @@ export class ChatWidget extends Disposable implements IChatWidget {

this.input.acceptInput(isUserQuery);
this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });
this.handleDelegationExitIfNeeded(result.agent);
this.currentRequest = result.responseCompletePromise.then(() => {
const responses = this.viewModel?.getItems().filter(isResponseVM);
const lastResponse = responses?.[responses.length - 1];
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/chat/common/chatSessionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export interface IChatSessionsService {
readonly onDidChangeAvailability: Event<void>;
readonly onDidChangeInProgress: Event<void>;

getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined;

registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable;
activateChatSessionItemProvider(chatSessionType: string): Promise<IChatSessionItemProvider | undefined>;
getAllChatSessionItemProviders(): IChatSessionItemProvider[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export class MockChatSessionsService implements IChatSessionsService {
return this.contributions;
}

getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
return this.contributions.find(contrib => contrib.type === chatSessionType);
}

setContributions(contributions: IChatSessionsExtensionPoint[]): void {
this.contributions = contributions;
}
Expand Down
Loading