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
6 changes: 3 additions & 3 deletions extensions/github-authentication/src/common/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode',
'vscode-insiders',
'vscode-exploration',
'vscode-sessions',
'vscode-sessions-insiders',
'vscode-sessions-exploration',
'vscode-agents',
'vscode-agents-insiders',
'vscode-agents-exploration',
// On Windows, some browsers don't seem to redirect back to OSS properly.
// As a result, you get stuck in the auth flow. We exclude this from the
// list until we can figure out a way to fix this behavior in browsers.
Expand Down
6 changes: 3 additions & 3 deletions extensions/microsoft-authentication/src/common/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const VALID_DESKTOP_CALLBACK_SCHEMES = [
'vscode',
'vscode-insiders',
'vscode-exploration',
'vscode-sessions',
'vscode-sessions-insiders',
'vscode-sessions-exploration',
'vscode-agents',
'vscode-agents-insiders',
'vscode-agents-exploration',
// On Windows, some browsers don't seem to redirect back to OSS properly.
// As a result, you get stuck in the auth flow. We exclude this from the
// list until we can figure out a way to fix this behavior in browsers.
Expand Down
33 changes: 23 additions & 10 deletions src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
import { localize } from '../../../nls.js';
import { ILogService } from '../../log/common/log.js';
import { IProductService } from '../../product/common/productService.js';
import {
ISSHRemoteAgentHostMainService,
SSHAuthMethod,
Expand Down Expand Up @@ -50,8 +51,13 @@ interface SSHClient {
const LOG_PREFIX = '[SSHRemoteAgentHost]';

/** Install location for the VS Code CLI on the remote machine. */
const REMOTE_CLI_DIR = '~/.vscode-cli';
const REMOTE_CLI_BIN = `${REMOTE_CLI_DIR}/code`;
function getRemoteCLIDir(quality: string): string {
return quality === 'stable' || !quality ? '~/.vscode-cli' : `~/.vscode-cli-${quality}`;
}
function getRemoteCLIBin(quality: string): string {
const binaryName = quality === 'stable' ? 'code' : 'code-insiders';
return `${getRemoteCLIDir(quality)}/${binaryName}`;
}

/** Escape a string for use as a single shell argument (single-quote wrapping). */
function shellEscape(s: string): string {
Expand Down Expand Up @@ -135,10 +141,11 @@ function redactToken(text: string): string {
function startRemoteAgentHost(
client: SSHClient,
logService: ILogService,
quality: string,
commandOverride?: string,
): Promise<{ port: number; connectionToken: string | undefined; stream: SSHChannel }> {
return new Promise((resolve, reject) => {
const baseCmd = commandOverride ?? `${REMOTE_CLI_BIN} agent-host --port 0 --accept-server-license-terms`;
const baseCmd = commandOverride ?? `${getRemoteCLIBin(quality)} agent-host --port 0 --accept-server-license-terms`;
// Wrap in a login shell so the agent host process inherits the
// user's PATH and environment from ~/.bash_profile / ~/.bashrc
// (ssh2 exec runs a non-interactive non-login shell by default).
Expand Down Expand Up @@ -338,6 +345,7 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem

constructor(
@ILogService private readonly _logService: ILogService,
@IProductService private readonly _productService: IProductService,
) {
super();
}
Expand Down Expand Up @@ -393,7 +401,7 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem

// 4. Start agent-host and capture port/token
reportProgress(localize('sshProgressStartingAgent', "Starting remote agent host..."));
const { port: remotePort, connectionToken, stream: agentStream } = await startRemoteAgentHost(sshClient, this._logService, config.remoteAgentHostCommand);
const { port: remotePort, connectionToken, stream: agentStream } = await startRemoteAgentHost(sshClient, this._logService, this._quality, config.remoteAgentHostCommand);

// 5. Connect to remote agent host via WebSocket relay (no local TCP port)
reportProgress(localize('sshProgressForwarding', "Connecting to remote agent host..."));
Expand Down Expand Up @@ -627,21 +635,26 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem
});
}

private get _quality(): string {
return this._productService.quality || 'insider';
}

private async _ensureCLIInstalled(client: SSHClient, platform: { os: string; arch: string }, reportProgress: (message: string) => void): Promise<void> {
const { code } = await sshExec(client, `${REMOTE_CLI_BIN} --version`, { ignoreExitCode: true });
const cliDir = getRemoteCLIDir(this._quality);
const cliBin = getRemoteCLIBin(this._quality);
const { code } = await sshExec(client, `${cliBin} --version`, { ignoreExitCode: true });
if (code === 0) {
this._logService.info(`${LOG_PREFIX} VS Code CLI already installed on remote`);
return;
}

reportProgress(localize('sshProgressDownloadingCLI', "Installing VS Code CLI on remote..."));
const quality = 'stable';
const url = buildCLIDownloadUrl(platform.os, platform.arch, quality);
const url = buildCLIDownloadUrl(platform.os, platform.arch, this._quality);

const installCmd = [
`mkdir -p ${REMOTE_CLI_DIR}`,
`curl -fsSL '${url}' | tar xz -C ${REMOTE_CLI_DIR}`,
`chmod +x ${REMOTE_CLI_BIN}`,
`mkdir -p ${cliDir}`,
`curl -fsSL '${url}' | tar xz -C ${cliDir}`,
`chmod +x ${cliBin}`,
].join(' && ');

await sshExec(client, installCmd);
Expand Down
107 changes: 71 additions & 36 deletions src/vs/sessions/contrib/changes/browser/changesView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const hasPullRequestContextKey = new RawContextKey<boolean>('sessions.hasPullReq
const hasOpenPullRequestContextKey = new RawContextKey<boolean>('sessions.hasOpenPullRequest', false);
const hasIncomingChangesContextKey = new RawContextKey<boolean>('sessions.hasIncomingChanges', false);
const hasOutgoingChangesContextKey = new RawContextKey<boolean>('sessions.hasOutgoingChanges', false);
const hasUncommittedChangesContextKey = new RawContextKey<boolean>('sessions.hasUncommittedChanges', true);

// --- List Item

Expand All @@ -129,8 +130,6 @@ interface IChangesFileItem {
readonly changeType: ChangeType;
readonly linesAdded: number;
readonly linesRemoved: number;
readonly reviewCommentCount: number;
readonly agentFeedbackCount: number;
}

interface IChangesRootItem {
Expand Down Expand Up @@ -243,9 +242,7 @@ function toChangesFileItem(changes: GitDiffChange[], modifiedRef: string | undef
isDeletion,
changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified',
linesAdded: change.insertions,
linesRemoved: change.deletions,
reviewCommentCount: 0,
agentFeedbackCount: 0,
linesRemoved: change.deletions
} satisfies IChangesFileItem;
});
}
Expand Down Expand Up @@ -752,8 +749,6 @@ export class ChangesViewPane extends ViewPane {

// Convert session file changes to list items (cloud/background sessions)
const sessionFilesObs = derived(reader => {
const reviewCommentCountByFile = this.viewModel.activeSessionReviewCommentCountByFileObs.read(reader);
const agentFeedbackCountByFile = this.viewModel.activeSessionAgentFeedbackCountByFileObs.read(reader);
const changes = [...this.viewModel.activeSessionChangesObs.read(reader)];

return changes.map((entry): IChangesFileItem => {
Expand All @@ -770,9 +765,7 @@ export class ChangesViewPane extends ViewPane {
isDeletion,
changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified',
linesAdded: entry.insertions,
linesRemoved: entry.deletions,
reviewCommentCount: reviewCommentCountByFile.get(uri.fsPath) ?? 0,
agentFeedbackCount: agentFeedbackCountByFile.get(uri.fsPath) ?? 0,
linesRemoved: entry.deletions
};
});
});
Expand Down Expand Up @@ -912,6 +905,10 @@ export class ChangesViewPane extends ViewPane {
const outgoingChangesObs = derived(reader => {
const repository = this.viewModel.activeSessionRepositoryObs.read(reader);
const repositoryState = repository?.state.read(reader);
if (!repositoryState) {
return 0;
}

return repositoryState?.HEAD?.ahead ?? 0;
});

Expand All @@ -920,6 +917,19 @@ export class ChangesViewPane extends ViewPane {
return outgoingChanges > 0;
}));

this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, reader => {
const repository = this.viewModel.activeSessionRepositoryObs.read(reader);
const repositoryState = repository?.state.read(reader);
if (!repositoryState) {
return true;
}

return (repositoryState?.mergeChanges.length ?? 0) > 0 ||
(repositoryState?.indexChanges.length ?? 0) > 0 ||
(repositoryState?.workingTreeChanges.length ?? 0) > 0 ||
(repositoryState?.untrackedChanges.length ?? 0) > 0;
}));

const scopedServiceCollection = new ServiceCollection([IContextKeyService, this.scopedContextKeyService]);
const scopedInstantiationService = this.instantiationService.createChild(scopedServiceCollection);
this.renderDisposables.add(scopedInstantiationService);
Expand Down Expand Up @@ -965,7 +975,10 @@ export class ChangesViewPane extends ViewPane {
? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] }
: { shouldForwardArgs: true },
buttonConfigProvider: (action) => {
if (action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR') {
if (
action.id === 'github.copilot.sessions.sync' ||
action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR'
) {
const customLabel = outgoingChanges > 0
? `${action.label} ${outgoingChanges}↑`
: action.label;
Expand Down Expand Up @@ -995,7 +1008,7 @@ export class ChangesViewPane extends ViewPane {
action.id === 'github.copilot.chat.checkoutPullRequestReroute' ||
action.id === 'pr.checkoutFromChat' ||
action.id === 'github.copilot.sessions.initializeRepository' ||
action.id === 'github.copilot.sessions.commitChanges' ||
action.id === 'github.copilot.sessions.commit' ||
action.id === 'agentSession.markAsDone'
) {
return { showIcon: true, showLabel: true, isSecondary: false };
Expand Down Expand Up @@ -1496,7 +1509,6 @@ class ChangesTreeDelegate implements IListVirtualDelegate<ChangesTreeElement> {

interface IChangesTreeTemplate {
readonly label: IResourceLabel;
readonly templateDisposables: DisposableStore;
readonly toolbar: MenuWorkbenchToolBar | undefined;
readonly contextKeyService: IContextKeyService | undefined;
readonly reviewCommentsBadge: HTMLElement;
Expand All @@ -1505,6 +1517,8 @@ interface IChangesTreeTemplate {
readonly addedSpan: HTMLElement;
readonly removedSpan: HTMLElement;
readonly lineCountsContainer: HTMLElement;
readonly elementDisposables: DisposableStore;
readonly templateDisposables: DisposableStore;
}

class ChangesTreeRenderer implements ICompressibleTreeRenderer<ChangesTreeElement, void, IChangesTreeTemplate> {
Expand Down Expand Up @@ -1561,7 +1575,7 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer<ChangesTreeElemen
const decorationBadge = dom.$('.changes-decoration-badge');
label.element.appendChild(decorationBadge);

return { templateDisposables, label, toolbar, contextKeyService, reviewCommentsBadge, agentFeedbackBadge, decorationBadge, addedSpan, removedSpan, lineCountsContainer };
return { label, toolbar, contextKeyService, reviewCommentsBadge, agentFeedbackBadge, decorationBadge, addedSpan, removedSpan, lineCountsContainer, elementDisposables: new DisposableStore(), templateDisposables };
}

renderElement(node: ITreeNode<ChangesTreeElement, void>, _index: number, templateData: IChangesTreeTemplate): void {
Expand Down Expand Up @@ -1630,29 +1644,41 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer<ChangesTreeElemen
templateData.lineCountsContainer.style.display = showChangeDecorations ? '' : 'none';
templateData.decorationBadge.style.display = showChangeDecorations ? '' : 'none';

if (data.reviewCommentCount > 0) {
templateData.reviewCommentsBadge.style.display = '';
templateData.reviewCommentsBadge.className = 'changes-review-comments-badge';
templateData.reviewCommentsBadge.replaceChildren(
dom.$('.codicon.codicon-comment-unresolved'),
dom.$('span', undefined, `${data.reviewCommentCount}`)
);
} else {
templateData.reviewCommentsBadge.style.display = 'none';
templateData.reviewCommentsBadge.replaceChildren();
}
// Review comments
templateData.elementDisposables.add(autorun(reader => {
const reviewCommentByFile = this.viewModel.activeSessionReviewCommentCountByFileObs.read(reader);
const reviewCommentCount = reviewCommentByFile?.get(data.uri.fsPath) ?? 0;

if (reviewCommentCount > 0) {
templateData.reviewCommentsBadge.style.display = '';
templateData.reviewCommentsBadge.className = 'changes-review-comments-badge';
templateData.reviewCommentsBadge.replaceChildren(
dom.$('.codicon.codicon-comment-unresolved'),
dom.$('span', undefined, `${reviewCommentCount}`)
);
} else {
templateData.reviewCommentsBadge.style.display = 'none';
templateData.reviewCommentsBadge.replaceChildren();
}
}));

if (data.agentFeedbackCount > 0) {
templateData.agentFeedbackBadge.style.display = '';
templateData.agentFeedbackBadge.className = 'changes-agent-feedback-badge';
templateData.agentFeedbackBadge.replaceChildren(
dom.$('.codicon.codicon-comment'),
dom.$('span', undefined, `${data.agentFeedbackCount}`)
);
} else {
templateData.agentFeedbackBadge.style.display = 'none';
templateData.agentFeedbackBadge.replaceChildren();
}
// Agent feedback
templateData.elementDisposables.add(autorun(reader => {
const agentFeedbackByFile = this.viewModel.activeSessionAgentFeedbackCountByFileObs.read(reader);
const agentFeedbackCount = agentFeedbackByFile?.get(data.uri.fsPath) ?? 0;

if (agentFeedbackCount > 0) {
templateData.agentFeedbackBadge.style.display = '';
templateData.agentFeedbackBadge.className = 'changes-agent-feedback-badge';
templateData.agentFeedbackBadge.replaceChildren(
dom.$('.codicon.codicon-comment'),
dom.$('span', undefined, `${agentFeedbackCount}`)
);
} else {
templateData.agentFeedbackBadge.style.display = 'none';
templateData.agentFeedbackBadge.replaceChildren();
}
}));

const badge = templateData.decorationBadge;
badge.className = 'changes-decoration-badge';
Expand Down Expand Up @@ -1735,7 +1761,16 @@ class ChangesTreeRenderer implements ICompressibleTreeRenderer<ChangesTreeElemen
}
}

disposeElement(_element: ITreeNode<ChangesTreeElement, void>, _index: number, templateData: IChangesTreeTemplate): void {
templateData.elementDisposables.clear();
}

disposeCompressedElements(_element: ITreeNode<ICompressedTreeNode<ChangesTreeElement>, void>, _index: number, templateData: IChangesTreeTemplate): void {
templateData.elementDisposables.clear();
}

disposeTemplate(templateData: IChangesTreeTemplate): void {
templateData.elementDisposables.dispose();
templateData.templateDisposables.dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,16 @@ registerAction2(class MarkSessionAsDoneAction extends Action2 {
order: 1,
when: ContextKeyExpr.and(
IsSessionsWindowContext,
ContextKeyExpr.equals('sessions.hasPullRequest', true),
ContextKeyExpr.equals('sessions.hasOpenPullRequest', false),
ContextKeyExpr.or(
ContextKeyExpr.and(
ContextKeyExpr.equals('sessions.hasPullRequest', false),
ContextKeyExpr.equals('sessions.hasOutgoingChanges', false),
),
ContextKeyExpr.and(
ContextKeyExpr.equals('sessions.hasPullRequest', true),
ContextKeyExpr.equals('sessions.hasOpenPullRequest', false),
)
)
)
}]
});
Expand Down
16 changes: 2 additions & 14 deletions src/vs/workbench/api/browser/mainThreadChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,13 +644,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
}

private _applyOptionGroups(handle: number, chatSessionType: string, optionGroups: readonly IChatSessionProviderOptionGroup[]): void {
const groupsWithCallbacks = optionGroups.map(group => ({
...group,
onSearch: group.searchable ? async (query: string, token: CancellationToken) => {
return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token);
} : undefined,
}));
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, groupsWithCallbacks);
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, optionGroups);
}

private getController(handle: number): MainThreadChatSessionItemController {
Expand Down Expand Up @@ -929,13 +923,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
private _refreshProviderOptions(handle: number, chatSessionScheme: string): void {
this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {
if (options?.optionGroups && options.optionGroups.length) {
const groupsWithCallbacks = options.optionGroups.map(group => ({
...group,
onSearch: group.searchable ? async (query: string, token: CancellationToken) => {
return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token);
} : undefined,
}));
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks);
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, [...options.optionGroups]);
}
}).catch(err => this._logService.error('Error fetching chat session options', err));
}
Expand Down
1 change: 0 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3676,7 +3676,6 @@ export interface ExtHostChatSessionsShape {
$disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise<void>;
$invokeChatSessionRequestHandler(providerHandle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult>;
$provideChatSessionProviderOptions(providerHandle: number, token: CancellationToken): Promise<IChatSessionProviderOptions | undefined>;
$invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]>;
$provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem | undefined>, token: CancellationToken): Promise<void>;
$forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise<Dto<IChatSessionItem>>;
$provideChatSessionInputState(controllerHandle: number, sessionResource: UriComponents | undefined, token: CancellationToken): Promise<IChatSessionProviderOptionGroup[] | undefined>;
Expand Down
Loading
Loading