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 @@ -78,6 +78,9 @@ export type ICreateSessionOptions = ISessionOptions & { sessionId?: string };
export interface ICopilotCLISessionService {
readonly _serviceBrand: undefined;

/**
* @deprecated Kept only for non-controller API
*/
onDidChangeSessions: Event<void>;
onDidDeleteSession: Event<string>;
onDidChangeSession: Event<ICopilotCLISessionItem>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export class ChatSessionMetadataStore extends Disposable implements IChatSession
await this.writeToGlobalStorage(data);
}
try {
await this.fileSystemService.delete(this.getMetadataFileUri(sessionId));
Comment thread
DonJayamanne marked this conversation as resolved.
await this.fileSystemService.delete(this.getRequestMappingFileUri(sessionId));
} catch {
// File may not exist, ignore.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class CopilotCLIChatSessionInitializer implements ICopilotCLIChatSessionI
}
} else {
// No chat session context (e.g., delegation) - initialize with active repository
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: options?.isolation, folder }, token);
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(undefined, { stream, toolInvocationToken, isolation: options?.isolation, folder, newBranch: options?.newBranch }, token);
}

if (folderInfo.trusted === false || folderInfo.cancelled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,6 @@ import { IPullRequestDetectionService } from './pullRequestDetectionService';
import { getSelectedSessionOptions, ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';
import { ISessionRequestLifecycle } from './sessionRequestLifecycle';

/**
* ODO:
* 3. Verify all command handlers do the exact same thing
* 6. Is chatSessionContext?.initialSessionOptions still valid with new API
* 7. Validated selected MRU item
* 8. We shouldn't have to pass model information into CLISession class, and then update sdk with the model info. Instead when we call get/create session, we should be able to pass the model info there and update the SDK session accordingly.
* This makes it unnecessary to pass model information.
* 2. Behavioral Change: trusted flag no longer unlocks dropdowns on trust failure
In the old code, when sessionResult.trusted === false, there was a call to this.unlockRepoOptionForSession(context, token) to reset dropdown selections. The new code at copilotCLIChatSessions.ts:634 simply returns {} without any dropdown reset. However, lockRepoOptionForSession and unlockRepoOptionForSession were already dead code (commented out), so this is actually correct — removing a no-op.
*
* Cases to cover:
* 1. Hook up the dropdowns for empty workspace folders as well
* 2. In mult-root workspace we need to display workspace/worktree dropdown along with the repo dropdown
* 3. Temporarily lock/unlock dropdowns while creating session
* 4. Lock dropdowns when opening an existing session
* 5. Browse folders command in empty workspaces
* 6. Branch dropdown should only be displayed when we select a folder/repo thats a git repo.
*
* Test:
* 1. All of the above
* 2. Forking sessions
* 3. Steering messages
* 4. Queued messages
* 5. Selecting a new folder in browse folders command should end up with that folder in the dropdown.
* 6. Delegate from CLI to Cloud
* 7. Delegate from Local to CLI
*/

export interface ICopilotCLIChatSessionItemProvider extends IDisposable {
refreshSession(refreshOptions: { reason: 'update'; sessionId: string } | { reason: 'update'; sessionIds: string[] } | { reason: 'delete'; sessionId: string }): Promise<void>;
Expand Down Expand Up @@ -196,6 +169,7 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
}
}
));
const inputStateForNewSession = new ResourceMap<WeakRef<vscode.ChatSessionInputState>>();
controller.newChatSessionItemHandler = async (context) => {
const sessionId = this.sessionService.createNewSessionId();
const resource = SessionIdForCLI.getResource(sessionId);
Expand All @@ -211,6 +185,8 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements

controller.items.add(session);
this.newSessions.set(resource, session);
const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.inputState);
inputStateForNewSession.set(resource, new WeakRef(controller.createChatSessionInputState(groups)));
return session;
Comment thread
DonJayamanne marked this conversation as resolved.
};
if (this.configurationService.getConfig(ConfigKey.Advanced.CLIForkSessionsEnabled)) {
Expand Down Expand Up @@ -273,7 +249,12 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
const groups = await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token);
return controller.createChatSessionInputState(groups);
} else {
const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);
// Possible we've already handled the newChatSessionItemHandler for this same uri
// In which case the proper inputState would have been sent.
// There's a bug in core where after newChatSessionItemHandler is called, we get
// another call for getChatSessionInputState, but this time the previous input state is incorrect.
const previousInputState = sessionResource ? inputStateForNewSession.get(sessionResource)?.deref() : undefined;
Comment thread
DonJayamanne marked this conversation as resolved.
const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(previousInputState);
const state = controller.createChatSessionInputState(groups);
// Only wire dynamic updates for new sessions (existing sessions are fully locked).
// Note: don't use the getChatSessionInputState token here — it's a one-shot token
Expand Down Expand Up @@ -309,7 +290,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
}

public async updateInputStateAfterFolderSelection(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): Promise<void> {
this._optionGroupBuilder.setNewFolderForInputState(inputState, folderUri);
await this._optionGroupBuilder.rebuildInputState(inputState, folderUri);
}

Expand Down Expand Up @@ -720,6 +700,15 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
return { input, attachments };
}

private generateNewBranchName(request: vscode.ChatRequest, token: vscode.CancellationToken): Promise<string | undefined> {
const requestTurn = new ChatRequestTurn2(request.prompt ?? '', request.command, [], '', [], [], undefined, undefined, undefined);
const fakeContext: vscode.ChatContext = {
history: [requestTurn],
yieldRequested: false,
};
const branchNamePromise = (request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
return branchNamePromise;
}
private async handleRequestImpl(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> {
const { chatSessionContext } = context;
const disposables = new DisposableStore();
Expand Down Expand Up @@ -752,12 +741,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
return {};
}

const requestTurn = new ChatRequestTurn2(request.prompt ?? '', request.command, [], '', [], [], undefined, undefined, undefined);
const fakeContext: vscode.ChatContext = {
history: [requestTurn],
yieldRequested: false,
};
const branchNamePromise = (isNewSession && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
const branchNamePromise = isNewSession ? this.generateNewBranchName(request, token) : Promise.resolve(undefined);

if (isNewSession) {
this._optionGroupBuilder.lockInputStateGroups(chatSessionContext.inputState);
Expand Down Expand Up @@ -869,8 +853,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
userPrompt = userPrompt || request.prompt;
return summary ? `${userPrompt}\n${summary}` : userPrompt;
})();

const { workspaceInfo, cancelled } = await this.sessionInitializer.initializeWorkingDirectory(undefined, { stream }, request.toolInvocationToken, token);
const branchNamePromise = this.generateNewBranchName(request, token);
const { workspaceInfo, cancelled } = await this.sessionInitializer.initializeWorkingDirectory(undefined, { stream, isolation: IsolationMode.Worktree, newBranch: branchNamePromise }, request.toolInvocationToken, token);

if (cancelled || token.isCancellationRequested) {
stream.markdown(l10n.t('Copilot CLI delegation cancelled.'));
Expand Down Expand Up @@ -966,8 +950,8 @@ export function registerCLIChatCommands(
);

if (result === deleteLabel) {
await copilotCLISessionService.deleteSession(id);
await copilotCliWorkspaceSession.deleteTrackedWorkspaceFolder(id);
await copilotCLISessionService.deleteSession(id);

if (worktreePath) {
try {
Expand Down Expand Up @@ -1256,7 +1240,7 @@ export function registerCLIChatCommands(
}));

const applyChanges = async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {
const resource = sessionItemOrResource instanceof vscode.Uri
const resource = isUri(sessionItemOrResource)
? sessionItemOrResource
: sessionItemOrResource?.resource;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ export abstract class FolderRepositoryManager extends Disposable implements IFol
deleteFromSource: moveOrCopyChanges === 'move',
untracked: true
});
stream.markdown(l10n.t('Changes migrated to worktree.'));
stream.markdown(l10n.t('Changes migrated to worktree.\n'));
}
} catch (error) {
// Continue even if migration fails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,6 @@ export function folderMRUToChatProviderOptions(mruItems: FolderRepositoryMRUEntr
*/
export interface ISessionOptionGroupBuilder {
readonly _serviceBrand: undefined;
setNewFolderForInputState(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): void;
provideChatSessionProviderOptionGroups(previousInputState: vscode.ChatSessionInputState | undefined): Promise<vscode.ChatSessionProviderOptionGroup[]>;
buildBranchOptionGroup(branches: vscode.ChatSessionProviderOptionItem[], headBranchName: string | undefined, isolationEnabled: boolean, currentIsolation: IsolationMode | undefined, previousSelection: vscode.ChatSessionProviderOptionItem | undefined): vscode.ChatSessionProviderOptionGroup | undefined;
handleInputStateChange(state: vscode.ChatSessionInputState): Promise<void>;
Expand All @@ -340,7 +339,7 @@ export class SessionOptionGroupBuilder implements ISessionOptionGroupBuilder {
declare readonly _serviceBrand: undefined;
private readonly _getBranchOptionItemsForRepositorySequencer = new SequencerByKey<string>();
private readonly _pendingBuildGroups = new WeakMap<vscode.ChatSessionInputState, Promise<vscode.ChatSessionProviderOptionGroup[]>>();
// Keeps track of the new folders selected by user, by using folder dialog to select a new folder.
// Keeps track of the new folders selected by user
private readonly _inputStateNewFolders = new WeakMap<vscode.ChatSessionInputState, vscode.Uri>();
constructor(
@IGitService private readonly gitService: IGitService,
Expand All @@ -354,9 +353,6 @@ export class SessionOptionGroupBuilder implements ISessionOptionGroupBuilder {
) { }


setNewFolderForInputState(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): void {
this._inputStateNewFolders.set(inputState, folderUri);
}
/**
* Return the git repository for a URI only if the folder is trusted.
* Untrusted folders are treated as non-git.
Expand Down Expand Up @@ -409,27 +405,37 @@ export class SessionOptionGroupBuilder implements ISessionOptionGroupBuilder {

// For untitled workspaces, show last used repositories and "Open Repository..." command
const repositories = await this.copilotCLIFolderMruService.getRecentlyUsedFolders(CancellationToken.None);
const newFolder = previousInputState ? this._inputStateNewFolders.get(previousInputState) : undefined;
items = folderMRUToChatProviderOptions(repositories);
const addFolderToList = async (uri: Uri) => {
const newFolderRepo = await this.getTrustedRepository(uri, true);
const newFolderItem = newFolderRepo
? toRepositoryOptionItem(newFolderRepo.rootUri)
: toWorkspaceFolderOptionItem(uri, uri.path.split('/').pop() ?? uri.fsPath);
// Remove duplicate if already in the list, then add to top
items = items.filter(item => item.id !== newFolderItem.id);
items.unshift(newFolderItem);
};
if (selectedFolderUri) {
await addFolderToList(selectedFolderUri);
}
const previouslySelectedUri = previouslySelected ? vscode.Uri.file(previouslySelected.id) : undefined;
if (previouslySelectedUri) {
await addFolderToList(previouslySelectedUri);
}
Comment thread
DonJayamanne marked this conversation as resolved.
// Ensure previously selected folder is added back into the list of folders.
const newFolder = previousInputState ? this._inputStateNewFolders.get(previousInputState) : undefined;
if (newFolder) {
await addFolderToList(newFolder);
}
const selectedFolderItem = selectedFolderUri ? items.find(i => i.id === selectedFolderUri.fsPath) : undefined;
const previouslySelectedItem = previouslySelected ? items.find(i => i.id === previouslySelected.id) : undefined;
const selectedItem = selectedFolderItem
?? (previouslySelected
? items.find(i => i.id === previouslySelected.id) ?? items[0]
: items[0]);
?? previouslySelectedItem ?? items[0];
if (selectedItem) {
defaultRepoUri = vscode.Uri.file(selectedItem.id);
}

items.splice(MAX_MRU_ENTRIES); // Limit to max entries
if (newFolder) {
const newFolderRepo = await this.getTrustedRepository(newFolder, true);
const newFolderItem = newFolderRepo
? toRepositoryOptionItem(newFolderRepo.rootUri)
: toWorkspaceFolderOptionItem(newFolder, newFolder.path.split('/').pop() ?? newFolder.fsPath);
// Remove duplicate if already in the list, then add to top
items = items.filter(item => item.id !== newFolderItem.id);
items.unshift(newFolderItem);
}
// If user selected something from the list but it's not there anymore (perhaps its an item at the end of MRU).
if (selectedItem && !items.some(item => item.id === selectedItem.id)) {
items.push(selectedItem);
Expand Down Expand Up @@ -546,6 +552,9 @@ export class SessionOptionGroupBuilder implements ISessionOptionGroupBuilder {
if (!optionGroupsEqual(state.groups, newGroups)) {
state.groups = newGroups;
}
if (selectedFolderUri) {
this._inputStateNewFolders.set(state, selectedFolderUri);
}
}

/**
Expand Down
Loading
Loading