From e1f87601f6d38b043ce78593c7e1f09f48f6b79a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 11 Mar 2026 16:35:38 +0100 Subject: [PATCH 1/3] wip - sessions title --- eslint.config.js | 3 +++ src/vs/sessions/services/title/browser/titleService.ts | 10 ++++++++++ .../title}/electron-browser/titleService.ts | 6 +++--- src/vs/sessions/sessions.desktop.main.ts | 2 +- src/vs/sessions/sessions.web.main.ts | 4 +--- 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/vs/sessions/services/title/browser/titleService.ts rename src/vs/sessions/{ => services/title}/electron-browser/titleService.ts (59%) diff --git a/eslint.config.js b/eslint.config.js index 73f062af66a..98147ae0288 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1965,6 +1965,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/editor/editor.all.js', 'vs/sessions/~', + 'vs/sessions/services/*/~', 'vs/sessions/contrib/*/~', 'vs/workbench/~', 'vs/workbench/api/~', @@ -1984,6 +1985,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/editor/editor.all.js', 'vs/sessions/~', + 'vs/sessions/services/*/~', 'vs/sessions/contrib/*/~', 'vs/workbench/~', 'vs/workbench/api/~', @@ -2083,6 +2085,7 @@ export default tseslint.config( 'vs/editor/contrib/*/~', 'vs/workbench/~', 'vs/workbench/services/*/~', + 'vs/sessions/~', 'vs/sessions/services/*/~', { 'when': 'test', diff --git a/src/vs/sessions/services/title/browser/titleService.ts b/src/vs/sessions/services/title/browser/titleService.ts new file mode 100644 index 00000000000..b04868f061e --- /dev/null +++ b/src/vs/sessions/services/title/browser/titleService.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ITitleService } from '../../../../workbench/services/title/browser/titleService.js'; +import { TitleService } from '../../../browser/parts/titlebarPart.js'; + +registerSingleton(ITitleService, TitleService, InstantiationType.Eager); diff --git a/src/vs/sessions/electron-browser/titleService.ts b/src/vs/sessions/services/title/electron-browser/titleService.ts similarity index 59% rename from src/vs/sessions/electron-browser/titleService.ts rename to src/vs/sessions/services/title/electron-browser/titleService.ts index 9ffc7a93650..070b1eb7bc4 100644 --- a/src/vs/sessions/electron-browser/titleService.ts +++ b/src/vs/sessions/services/title/electron-browser/titleService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InstantiationType, registerSingleton } from '../../platform/instantiation/common/extensions.js'; -import { ITitleService } from '../../workbench/services/title/browser/titleService.js'; -import { NativeTitleService } from './parts/titlebarPart.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ITitleService } from '../../../../workbench/services/title/browser/titleService.js'; +import { NativeTitleService } from '../../../electron-browser/parts/titlebarPart.js'; registerSingleton(ITitleService, NativeTitleService, InstantiationType.Eager); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index f8f88d37215..587ce438dd0 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -42,7 +42,7 @@ import '../workbench/services/update/electron-browser/updateService.js'; import '../workbench/services/url/electron-browser/urlService.js'; import '../workbench/services/lifecycle/electron-browser/lifecycleService.js'; import '../workbench/services/host/electron-browser/nativeHostService.js'; -import './electron-browser/titleService.js'; +import './services/title/electron-browser/titleService.js'; import '../platform/meteredConnection/electron-browser/meteredConnectionService.js'; import '../workbench/services/request/electron-browser/requestService.js'; import '../workbench/services/clipboard/electron-browser/clipboardService.js'; diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts index 9a335e542f8..b6710d02294 100644 --- a/src/vs/sessions/sessions.web.main.ts +++ b/src/vs/sessions/sessions.web.main.ts @@ -90,8 +90,7 @@ import { UserDataAutoSyncService } from '../platform/userDataSync/common/userDat import { AccessibilityService } from '../platform/accessibility/browser/accessibilityService.js'; import { ICustomEndpointTelemetryService } from '../platform/telemetry/common/telemetry.js'; import { NullEndpointTelemetryService } from '../platform/telemetry/common/telemetryUtils.js'; -import { ITitleService } from '../workbench/services/title/browser/titleService.js'; -import { BrowserTitleService } from '../workbench/browser/parts/titlebar/titlebarPart.js'; +import './services/title/browser/titleService.js'; import { ITimerService, TimerService } from '../workbench/services/timer/browser/timerService.js'; import { IDiagnosticsService, NullDiagnosticsService } from '../platform/diagnostics/common/diagnostics.js'; import { ILanguagePackService } from '../platform/languagePacks/common/languagePacks.js'; @@ -111,7 +110,6 @@ registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService, Insta registerSingleton(IUserDataSyncService, UserDataSyncService, InstantiationType.Delayed); registerSingleton(IUserDataSyncResourceProviderService, UserDataSyncResourceProviderService, InstantiationType.Delayed); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService, InstantiationType.Eager); -registerSingleton(ITitleService, BrowserTitleService, InstantiationType.Eager); registerSingleton(IExtensionTipsService, ExtensionTipsService, InstantiationType.Delayed); registerSingleton(ITimerService, TimerService, InstantiationType.Delayed); registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, InstantiationType.Delayed); From a988b69b4e21c16e313d0a927afcbc243a8b03a7 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 11 Mar 2026 22:38:59 -0700 Subject: [PATCH 2/3] Main button commands and mocks --- src/vs/sessions/sessions.web.main.ts | 1 + src/vs/sessions/test/e2e/README.md | 26 +++++ .../extensions/sessions-e2e-mock/extension.js | 106 +++++++++++++++++- .../extensions/sessions-e2e-mock/package.json | 85 +++++++++++++- src/vs/sessions/test/web.test.ts | 2 +- 5 files changed, 217 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts index b6710d02294..5c21d23adeb 100644 --- a/src/vs/sessions/sessions.web.main.ts +++ b/src/vs/sessions/sessions.web.main.ts @@ -152,6 +152,7 @@ import './browser/layoutActions.js'; import './contrib/accountMenu/browser/account.contribution.js'; import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.js'; +import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; diff --git a/src/vs/sessions/test/e2e/README.md b/src/vs/sessions/test/e2e/README.md index a2b6cce5d3f..347d0660da9 100644 --- a/src/vs/sessions/test/e2e/README.md +++ b/src/vs/sessions/test/e2e/README.md @@ -21,6 +21,8 @@ runs through the real code paths. | Chat agents (`copilotcli`, etc.) | Canned keyword-matched responses with `textEdit` progress items | No real LLM backend | | `mock-fs://` FileSystemProvider | `InMemoryFileSystemProvider` registered directly in the workbench (not extension host) | Must be available before any service tries to resolve workspace files | | GitHub authentication | Always-signed-in mock provider (extension) | No real OAuth flow | +| Code Review command | Returns canned review comments per file (extension) | No real Copilot AI review | +| PR commands (Create/Open/Merge) | No-op handlers that log and show info messages (extension) | No real GitHub API | ### What's Real (Everything Else) @@ -39,6 +41,10 @@ exercise the actual code paths: observations - **Menu actions** — "Create PR", "Accept", "Reject" buttons appear based on real context key state +- **`CodeReviewService`** — Orchestrates review requests, processes results from + the mock `github.copilot.chat.codeReview.run` command, and stores comments +- **`CodeReviewToolbarContribution`** — Shows the Code Review button in the + Changes view toolbar based on real context key state ### Data Flow @@ -57,6 +63,26 @@ User types message → Chat Widget → ChatService The mock agent is the **only** point where canned data enters the system. Everything downstream uses real service implementations. +### Code Review & PR Button Flow + +``` +Code Review button clicked → sessions.codeReview.run (core action) + → CodeReviewService.requestReview() + → commandService.executeCommand('chat.internal.codeReview.run') + → Bridge forwards to 'github.copilot.chat.codeReview.run' + → Mock extension returns canned comments + → CodeReviewService stores results, updates observable state + → CodeReviewToolbarContribution updates button icon/badge + +Create PR button clicked → github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR + → Mock extension logs and shows info message +``` + +The PR buttons (Create PR, Open PR, Merge) are contributed via the mock +extension's `package.json` menus, gated by `chatSessionType == copilotcli`. +The `chatSessionType` context key is derived from the session URI scheme +(`getChatSessionType()`), which returns `copilotcli` for mock sessions. + ### Why the FileSystem Provider Is Registered in the Workbench The `mock-fs://` `InMemoryFileSystemProvider` is registered directly on diff --git a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js index 42991694ae3..de43487e9a9 100644 --- a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js +++ b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js @@ -10,7 +10,9 @@ /** * Mock extension for Sessions E2E testing. * - * Provides a fake GitHub authentication provider (skips sign-in). + * Provides: + * - A fake GitHub authentication provider (skips sign-in) + * - Mock command handlers for Code Review, Create PR, Open PR, and Merge * * The mock-fs:// FileSystemProvider and chat agents are registered * directly in the workbench (web.test.ts), not here. @@ -31,6 +33,9 @@ function activate(context) { // 1. Mock GitHub Authentication Provider context.subscriptions.push(registerMockAuth(vscode)); + // 2. Mock command handlers for Code Review and PR actions + context.subscriptions.push(...registerMockCommands(vscode)); + // Note: The mock-fs:// FileSystemProvider is registered directly in the // workbench (web.test.ts → registerMockFileSystemProvider) so it is // available before any service tries to resolve workspace files. @@ -82,6 +87,105 @@ function registerMockAuth(vscode) { }); } +// --------------------------------------------------------------------------- +// Mock Command Handlers (Code Review + PR Actions) +// --------------------------------------------------------------------------- + +/** + * Registers mock command handlers that stand in for the real GitHub Copilot + * extension commands. These allow the Code Review and Create PR buttons to + * function in the e2e test environment. + * + * @param {typeof import('vscode')} vscode + * @returns {import('vscode').Disposable[]} + */ +function registerMockCommands(vscode) { + const disposables = []; + + // Mock code review — returns canned review comments + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.codeReview.run', + (args) => { + console.log('[sessions-e2e-mock] Mock code review invoked', args); + const files = args?.files ?? []; + const comments = files.slice(0, 2).map((file, i) => ({ + uri: file.currentUri, + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, + body: `Mock review comment ${i + 1}: Consider improving this code.`, + kind: 'suggestion', + severity: 'info', + })); + return { type: 'success', comments }; + } + )); + + // Mock create PR — simulates successful PR creation + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR', + () => { + console.log('[sessions-e2e-mock] Mock Create PR invoked'); + vscode.window.showInformationMessage('Mock: Pull request created successfully'); + } + )); + + // Mock open PR — simulates opening a PR URL + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR', + () => { + console.log('[sessions-e2e-mock] Mock Open PR invoked'); + vscode.window.showInformationMessage('Mock: Opening pull request'); + } + )); + + // Mock merge — simulates merging changes + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge', + () => { + console.log('[sessions-e2e-mock] Mock Merge invoked'); + vscode.window.showInformationMessage('Mock: Changes merged successfully'); + } + )); + + // Mock merge and sync — simulates merging and syncing + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync', + () => { + console.log('[sessions-e2e-mock] Mock Merge and Sync invoked'); + vscode.window.showInformationMessage('Mock: Changes merged and synced successfully'); + } + )); + + // Mock apply changes — simulates applying session changes + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply', + () => { + console.log('[sessions-e2e-mock] Mock Apply Changes invoked'); + vscode.window.showInformationMessage('Mock: Changes applied successfully'); + } + )); + + // Mock checkout PR reroute — simulates checkout PR flow + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.checkoutPullRequestReroute', + () => { + console.log('[sessions-e2e-mock] Mock Checkout PR Reroute invoked'); + vscode.window.showInformationMessage('Mock: Checking out pull request'); + } + )); + + // Mock update changes — simulates updating session changes + disposables.push(vscode.commands.registerCommand( + 'github.copilot.chat.updateCopilotCLIAgentSessionChanges.update', + () => { + console.log('[sessions-e2e-mock] Mock Update Changes invoked'); + vscode.window.showInformationMessage('Mock: Changes updated successfully'); + } + )); + + console.log('[sessions-e2e-mock] Registered mock Code Review and PR command handlers'); + return disposables; +} + // --------------------------------------------------------------------------- // Exports // --------------------------------------------------------------------------- diff --git a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/package.json b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/package.json index a073ad0542d..fa02ea6068b 100644 --- a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/package.json +++ b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/package.json @@ -10,6 +10,7 @@ "extensionKind": ["ui", "workspace"], "browser": "./extension.js", "activationEvents": ["*"], + "enabledApiProposals": ["chatSessionsProvider"], "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { @@ -22,6 +23,88 @@ "id": "github", "label": "GitHub (Mock)" } - ] + ], + "commands": [ + { + "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply", + "title": "Apply Changes", + "icon": "$(check)" + }, + { + "command": "github.copilot.chat.checkoutPullRequestReroute", + "title": "Checkout Pull Request", + "icon": "$(git-pull-request)" + }, + { + "command": "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR", + "title": "Open Pull Request", + "icon": "$(link-external)" + }, + { + "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", + "title": "Merge", + "icon": "$(git-merge)" + }, + { + "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", + "title": "Merge and Sync", + "icon": "$(git-merge)" + }, + { + "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", + "title": "Create Pull Request", + "icon": "$(git-pull-request-create)" + }, + { + "command": "github.copilot.chat.updateCopilotCLIAgentSessionChanges.update", + "title": "Update", + "icon": "$(cloud-upload)" + }, + { + "command": "github.copilot.chat.codeReview.run", + "title": "Run Code Review" + } + ], + "menus": { + "chat/input/editing/sessionToolbar": [ + { + "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply", + "when": "chatSessionType == copilotcli && workbenchState != empty && !isSessionsWindow", + "group": "navigation@0" + }, + { + "command": "github.copilot.chat.checkoutPullRequestReroute", + "when": "chatSessionType == copilot-cloud-agent && !github.vscode-pull-request-github.activated && gitOpenRepositoryCount != 0", + "group": "navigation@0" + }, + { + "command": "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR", + "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasOpenPullRequest", + "group": "navigation@1" + } + ], + "chat/input/editing/sessionApplyActions": [ + { + "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", + "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.isMergeBaseBranchProtected", + "group": "merge@1" + }, + { + "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", + "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.isMergeBaseBranchProtected", + "group": "merge@2" + }, + { + "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", + "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isMergeBaseBranchProtected && !sessions.hasOpenPullRequest", + "group": "pull_request@1" + }, + { + "command": "github.copilot.chat.updateCopilotCLIAgentSessionChanges.update", + "when": "chatSessionType == copilotcli && isSessionsWindow", + "group": "update@1" + } + ] + } } } diff --git a/src/vs/sessions/test/web.test.ts b/src/vs/sessions/test/web.test.ts index 77c7a39df77..4addb9291c5 100644 --- a/src/vs/sessions/test/web.test.ts +++ b/src/vs/sessions/test/web.test.ts @@ -306,7 +306,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu extensionVersion: '0.0.1', extensionPublisherId: 'vscode', extensionDisplayName: 'Sessions E2E Mock', - isDefault: true, + isDefault: agentId === 'copilotcli', metadata: {}, slashCommands: [], locations: [ChatAgentLocation.Chat], From 739fb0ae6fb5b4d95d72e96d239185dd015e1488 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 12 Mar 2026 15:52:48 -0700 Subject: [PATCH 3/3] Mocks + full workflow scenario --- .../browser/sessionsTerminalContribution.ts | 11 +- src/vs/sessions/sessions.web.main.ts | 1 + .../extensions/sessions-e2e-mock/extension.js | 19 +-- .../scenarios/06-full-workflow.scenario.md | 25 ++++ .../generated/06-full-workflow.commands.json | 140 ++++++++++++++++++ src/vs/sessions/test/web.test.ts | 95 +++++++++++- 6 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 src/vs/sessions/test/e2e/scenarios/06-full-workflow.scenario.md create mode 100644 src/vs/sessions/test/e2e/scenarios/generated/06-full-workflow.commands.json diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 0469553aa77..c45ce72df32 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -99,9 +99,14 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben let existing = await this._findTerminalsForKey(key); if (existing.length === 0) { - existing = [await this._terminalService.createTerminal({ config: { cwd } })]; - this._terminalService.setActiveInstance(existing[0]); - this._logService.trace(`[SessionsTerminal] Created terminal ${existing[0].instanceId} for ${cwd.fsPath}`); + try { + existing = [await this._terminalService.createTerminal({ config: { cwd } })]; + this._terminalService.setActiveInstance(existing[0]); + this._logService.trace(`[SessionsTerminal] Created terminal ${existing[0].instanceId} for ${cwd.fsPath}`); + } catch (e) { + this._logService.trace(`[SessionsTerminal] Cannot create terminal for ${cwd.fsPath}: ${e}`); + return []; + } } if (focus) { diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts index 5c21d23adeb..b265cc7ea9f 100644 --- a/src/vs/sessions/sessions.web.main.ts +++ b/src/vs/sessions/sessions.web.main.ts @@ -154,6 +154,7 @@ import './contrib/accountMenu/browser/account.contribution.js'; import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.js'; import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/chat/browser/chat.contribution.js'; +import './contrib/terminal/browser/sessionsTerminalContribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changes/browser/changesView.contribution.js'; diff --git a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js index de43487e9a9..461ffb8207d 100644 --- a/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js +++ b/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock/extension.js @@ -102,23 +102,6 @@ function registerMockAuth(vscode) { function registerMockCommands(vscode) { const disposables = []; - // Mock code review — returns canned review comments - disposables.push(vscode.commands.registerCommand( - 'github.copilot.chat.codeReview.run', - (args) => { - console.log('[sessions-e2e-mock] Mock code review invoked', args); - const files = args?.files ?? []; - const comments = files.slice(0, 2).map((file, i) => ({ - uri: file.currentUri, - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - body: `Mock review comment ${i + 1}: Consider improving this code.`, - kind: 'suggestion', - severity: 'info', - })); - return { type: 'success', comments }; - } - )); - // Mock create PR — simulates successful PR creation disposables.push(vscode.commands.registerCommand( 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR', @@ -178,7 +161,7 @@ function registerMockCommands(vscode) { 'github.copilot.chat.updateCopilotCLIAgentSessionChanges.update', () => { console.log('[sessions-e2e-mock] Mock Update Changes invoked'); - vscode.window.showInformationMessage('Mock: Changes updated successfully'); + vscode.window.showInformationMessage('Mock: Changes updated successfully',); } )); diff --git a/src/vs/sessions/test/e2e/scenarios/06-full-workflow.scenario.md b/src/vs/sessions/test/e2e/scenarios/06-full-workflow.scenario.md new file mode 100644 index 00000000000..321446d2af2 --- /dev/null +++ b/src/vs/sessions/test/e2e/scenarios/06-full-workflow.scenario.md @@ -0,0 +1,25 @@ +# Scenario: Full workflow + +## Steps +1. Type "build the project" in the chat input +2. Press Enter to submit +3. Verify there is a response in the chat +4. Toggle the secondary side bar +5. Verify the changes view shows "CHANGES" with a badge +6. Verify "package.json" is visible in the changes list +7. Verify "build.ts" is visible in the changes list +8. Verify "index.ts" is visible in the changes list +9. Click on "index.ts" in the changes list +10. Verify a diff editor opens with modified content +11. Press Escape to close the diff editor +12. Verify "Merge" button is visible in changes view header +13. Verify the "Open Terminal" button is visible +14. Click the "Open Terminal" button +15. Verify the terminal panel becomes visible +16. Verify the terminal tab shows "session-1" in its label +17. Click "New Session" to create a new session +18. Type "fix the bug" in the chat input +19. Press Enter to submit +20. Verify the terminal tab label changes to show "session-2" +21. Click back on the first session in the sessions list +22. Verify the terminal tab label changes back to show "session-1" diff --git a/src/vs/sessions/test/e2e/scenarios/generated/06-full-workflow.commands.json b/src/vs/sessions/test/e2e/scenarios/generated/06-full-workflow.commands.json new file mode 100644 index 00000000000..5124b5d6d2d --- /dev/null +++ b/src/vs/sessions/test/e2e/scenarios/generated/06-full-workflow.commands.json @@ -0,0 +1,140 @@ +{ + "scenario": "Scenario: Full workflow", + "generatedAt": "2026-03-12T22:48:50.725Z", + "steps": [ + { + "description": "Type \"build the project\" in the chat input", + "commands": [ + "click textbox \"Chat input\"", + "type \"build the project\"" + ] + }, + { + "description": "Press Enter to submit", + "commands": [ + "press Enter" + ] + }, + { + "description": "Verify there is a response in the chat", + "commands": [ + "# ASSERT_VISIBLE: I'll help you build the project. Here are the changes:" + ] + }, + { + "description": "Toggle the secondary side bar", + "commands": [ + "click button \"Toggle Secondary Side Bar Visibility\"" + ] + }, + { + "description": "Verify the changes view shows \"CHANGES\" with a badge", + "commands": [ + "# ASSERT_VISIBLE: Changes - 3 files changed" + ] + }, + { + "description": "Verify \"package.json\" is visible in the changes list", + "commands": [ + "# ASSERT_VISIBLE: package.json" + ] + }, + { + "description": "Verify \"build.ts\" is visible in the changes list", + "commands": [ + "# ASSERT_VISIBLE: build.ts" + ] + }, + { + "description": "Verify \"index.ts\" is visible in the changes list", + "commands": [ + "# ASSERT_VISIBLE: index.ts" + ] + }, + { + "description": "Click on \"index.ts\" in the changes list", + "commands": [ + "click treeitem \"index.ts\"" + ] + }, + { + "description": "Verify a diff editor opens with modified content", + "commands": [ + "# ASSERT_VISIBLE: index.ts" + ] + }, + { + "description": "Press Escape to close the diff editor", + "commands": [ + "press Escape" + ] + }, + { + "description": "Verify \"Merge\" button is visible in changes view header", + "commands": [ + "# ASSERT_VISIBLE: Merge" + ] + }, + { + "description": "Verify the \"Open Terminal\" button is visible", + "commands": [ + "# ASSERT_VISIBLE: Open Terminal" + ] + }, + { + "description": "Click the \"Open Terminal\" button", + "commands": [ + "click button \"Open Terminal\"" + ] + }, + { + "description": "Verify the terminal panel becomes visible", + "commands": [ + "# ASSERT_VISIBLE: Terminal" + ] + }, + { + "description": "Verify the terminal tab shows \"session-1\" in its label", + "commands": [ + "# ASSERT_VISIBLE: bash - session-1" + ] + }, + { + "description": "Click \"New Session\" to create a new session", + "commands": [ + "click button \"New Session\"" + ] + }, + { + "description": "Type \"fix the bug\" in the chat input", + "commands": [ + "click textbox \"Chat input\"", + "type \"fix the bug\"" + ] + }, + { + "description": "Press Enter to submit", + "commands": [ + "press Enter" + ] + }, + { + "description": "Verify the terminal tab label changes to show \"session-2\"", + "commands": [ + "# ASSERT_VISIBLE: bash - session-2" + ] + }, + { + "description": "Click back on the first session in the sessions list", + "commands": [ + "click listitem \"build the project\"" + ] + }, + { + "description": "Verify the terminal tab label changes back to show \"session-1\"", + "commands": [ + "# ASSERT_VISIBLE: bash - session-1" + ] + } + ] +} diff --git a/src/vs/sessions/test/web.test.ts b/src/vs/sessions/test/web.test.ts index 4addb9291c5..193ce45b92a 100644 --- a/src/vs/sessions/test/web.test.ts +++ b/src/vs/sessions/test/web.test.ts @@ -25,6 +25,10 @@ import { IChatProgress } from '../../workbench/contrib/chat/common/chatService/c import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem } from '../../workbench/contrib/chat/common/chatSessionsService.js'; import { IGitService, IGitExtensionDelegate, IGitRepository } from '../../workbench/contrib/git/common/gitService.js'; import { IFileService } from '../../platform/files/common/files.js'; +import { ITerminalService } from '../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalBackend, ITerminalBackendRegistry, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, TerminalExtensions, ITerminalProcessOptions, IShellLaunchConfig } from '../../platform/terminal/common/terminal.js'; +import { IProcessEnvironment } from '../../base/common/platform.js'; +import { Registry } from '../../platform/registry/common/platform.js'; import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js'; import { VSBuffer } from '../../base/common/buffer.js'; @@ -230,16 +234,19 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu private readonly _sessionItems: IChatSessionItem[] = []; private readonly _itemsChangedEmitter = new Emitter(); private readonly _sessionHistory = new Map(); + private _worktreeCounter = 0; constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IStorageService private readonly storageService: IStorageService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @ITerminalService private readonly terminalService: ITerminalService, ) { super(); this._register(this._itemsChangedEmitter); this.registerMockAgents(); this.registerMockSessionProvider(); + this.registerMockTerminalBackend(); this.preseedFolder(); } @@ -285,6 +292,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu label: message.slice(0, 50) || 'Mock Session', status: ChatSessionStatus.Completed, timing: { created: now, lastRequestStarted: now, lastRequestEnded: now }, + metadata: { worktreePath: `/mock-worktrees/session-${++this._worktreeCounter}` }, ...(changes ? { changes } : {}), }); } @@ -380,11 +388,15 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu }, })); - // Register an item controller so sessions appear in the sidebar list - const items = this._sessionItems; + // Register an item controller so sessions appear in the sidebar list. + // Only copilotcli (Background) sessions need real items — the + // copilot-cloud-agent controller must return an empty array to + // prevent it from overwriting sessions with the wrong providerType + // during a full model resolve. + const controllerItems = scheme === 'copilotcli' ? this._sessionItems : []; this._register(this.chatSessionsService.registerChatSessionItemController(scheme, { onDidChangeChatSessionItems: this._itemsChangedEmitter.event, - get items() { return items; }, + get items() { return controllerItems; }, async refresh() { /* in-memory, no-op */ }, })); @@ -395,6 +407,83 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu } } + private registerMockTerminalBackend(): void { + const terminalService = this.terminalService; + const backend = this.createMockTerminalBackend(); + Registry.as(TerminalExtensions.Backend).registerTerminalBackend(backend); + terminalService.registerProcessSupport(true); + console.log('[Sessions Web Test] Registered mock terminal backend'); + } + + private createMockTerminalBackend(): ITerminalBackend { + return { + remoteAuthority: undefined, + isVirtualProcess: false, + onDidRequestDetach: Event.None, + attachToProcess: async () => { throw new Error('Not supported'); }, + attachToRevivedProcess: async () => { throw new Error('Not supported'); }, + listProcesses: async () => [], + getProfiles: async () => [], + getDefaultProfile: async () => undefined, + getDefaultSystemShell: async () => '/bin/mock-shell', + getShellEnvironment: async () => ({}), + setTerminalLayoutInfo: async () => { }, + getTerminalLayoutInfo: async () => undefined, + reduceConnectionGraceTime: () => { }, + requestDetachInstance: () => { }, + acceptDetachInstanceReply: () => { }, + persistTerminalState: () => { }, + createProcess: async (_shellLaunchConfig: IShellLaunchConfig, _cwd: string | URI, _cols: number, _rows: number, _unicodeVersion: string, _env: IProcessEnvironment, _options: ITerminalProcessOptions, _shouldPersist: boolean) => { + const onProcessData = new Emitter(); + const onProcessReady = new Emitter(); + const onProcessExit = new Emitter(); + const onDidChangeHasChildProcesses = new Emitter(); + const onDidChangeProperty = new Emitter>(); + + // Resolve cwd from createProcess arg or shellLaunchConfig + const rawCwd = _cwd || _shellLaunchConfig.cwd; + const cwd = !rawCwd ? '/' : typeof rawCwd === 'string' ? rawCwd : rawCwd.path; + console.log(`[Sessions Web Test] Mock terminal createProcess cwd: '${cwd}' (raw _cwd: '${_cwd}', slc.cwd: '${_shellLaunchConfig.cwd}')`); + + // Fire ready after a microtask so the terminal service can wire up listeners + setTimeout(() => { + onProcessReady.fire({ pid: 1, cwd, windowsPty: undefined }); + }, 0); + + return { + id: 0, + shouldPersist: false, + onProcessData: onProcessData.event, + onProcessReady: onProcessReady.event, + onDidChangeHasChildProcesses: onDidChangeHasChildProcesses.event, + onDidChangeProperty: onDidChangeProperty.event, + onProcessExit: onProcessExit.event, + start: async () => undefined, + shutdown: async () => { }, + input: async () => { }, + resize: () => { }, + clearBuffer: () => { }, + acknowledgeDataEvent: () => { }, + setUnicodeVersion: async () => { }, + getInitialCwd: async () => cwd, + getCwd: async () => cwd, + getLatency: async () => [], + processBinary: async () => { }, + refreshProperty: async (property: ProcessPropertyType) => { throw new Error(`Not supported: ${property}`); }, + updateProperty: async () => { }, + clearUnrespondedRequest: () => { }, + }; + }, + getWslPath: async (original: string, _direction: 'unix-to-win' | 'win-to-unix') => original, + getEnvironment: async () => ({}), + getPerformanceMarks: () => [], + onPtyHostUnresponsive: Event.None, + onPtyHostResponsive: Event.None, + onPtyHostRestart: Event.None, + onPtyHostConnected: Event.None, + } as unknown as ITerminalBackend; + } + private preseedFolder(): void { const mockFolderUri = URI.from({ scheme: 'mock-fs', authority: 'mock-repo', path: '/mock-repo' }).toString(); this.storageService.store('agentSessions.lastPickedFolder', mockFolderUri, StorageScope.PROFILE, StorageTarget.MACHINE);