diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 46a7230a092..70d972b1d5f 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -478,7 +478,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio return ChatSessionStatus.Completed; case 2: // vscode.ChatSessionStatus.InProgress return ChatSessionStatus.InProgress; - // Need to support NeedsInput status if we ever export it to the extension API + case 3: // vscode.ChatSessionStatus.NeedsInput + return ChatSessionStatus.NeedsInput; default: return undefined; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9e47fb165dc..d7d1c805b87 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3552,7 +3552,8 @@ export enum ChatLocation { export enum ChatSessionStatus { Failed = 0, Completed = 1, - InProgress = 2 + InProgress = 2, + NeedsInput = 3 } export class ChatSessionChangedFile { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index 8ac2ab653fd..ff81e1273fa 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -9,6 +9,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { IChatSessionTiming } from '../../common/chatService/chatService.js'; +import { IChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; @@ -18,6 +19,7 @@ export enum AgentSessionProviders { Cloud = 'copilot-cloud-agent', Claude = 'claude-code', Codex = 'openai-codex', + Growth = 'copilot-growth', } export function isBuiltInAgentSessionProvider(provider: string): boolean { @@ -35,6 +37,7 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes case AgentSessionProviders.Cloud: case AgentSessionProviders.Claude: case AgentSessionProviders.Codex: + case AgentSessionProviders.Growth: return type; default: return undefined; @@ -59,6 +62,8 @@ export function getAgentSessionProviderName(provider: AgentSessionProviders): st return 'Claude'; case AgentSessionProviders.Codex: return 'Codex'; + case AgentSessionProviders.Growth: + return 'Growth'; } } @@ -74,6 +79,8 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th return Codicon.openai; case AgentSessionProviders.Claude: return Codicon.claude; + case AgentSessionProviders.Growth: + return Codicon.lightbulb; } } @@ -85,11 +92,16 @@ export function isFirstPartyAgentSessionProvider(provider: AgentSessionProviders return true; case AgentSessionProviders.Claude: case AgentSessionProviders.Codex: + case AgentSessionProviders.Growth: return false; } } -export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean { +export function getAgentCanContinueIn(provider: AgentSessionProviders, contribution?: IChatSessionsExtensionPoint): boolean { + // Read-only sessions (e.g., Growth) are passive/informational and cannot be delegation targets + if (contribution?.isReadOnly) { + return false; + } switch (provider) { case AgentSessionProviders.Local: case AgentSessionProviders.Background: @@ -97,6 +109,7 @@ export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean return true; case AgentSessionProviders.Claude: case AgentSessionProviders.Codex: + case AgentSessionProviders.Growth: return false; } } @@ -113,6 +126,8 @@ export function getAgentSessionProviderDescription(provider: AgentSessionProvide return localize('chat.session.providerDescription.claude', "Delegate tasks to the Claude Agent SDK using the Claude models included in your GitHub Copilot subscription. The agent iterates via chat and works interactively to implement changes on your main workspace."); case AgentSessionProviders.Codex: return localize('chat.session.providerDescription.codex', "Opens a new Codex session in the editor. Codex sessions can be managed from the chat sessions view."); + case AgentSessionProviders.Growth: + return localize('chat.session.providerDescription.growth', "Educational messages to help you learn Copilot features."); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index d75bdc57ac6..344bc51d962 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -200,6 +200,11 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint 0) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts index 9e5218f2afb..37a075a3970 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -54,7 +54,8 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt return true; // Always show active session type } - return getAgentCanContinueIn(type); + const contribution = this.chatSessionsService.getChatSessionContribution(type); + return getAgentCanContinueIn(type, contribution); } protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index c5ab3aae3f3..c14a5416cda 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -163,6 +163,10 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const contributions = this.chatSessionsService.getAllChatSessionContributions(); for (const contribution of contributions) { + if (contribution.isReadOnly) { + continue; // Read-only sessions are not interactive and should not appear in session target picker + } + const agentSessionType = getAgentSessionProvider(contribution.type); if (!agentSessionType) { continue; diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 9f280a98fb8..839510819e7 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -85,6 +85,7 @@ export interface IChatSessionsExtensionPoint { readonly capabilities?: IChatAgentAttachmentCapabilities; readonly commands?: IChatSessionCommandContribution[]; readonly canDelegate?: boolean; + readonly isReadOnly?: boolean; /** * When set, the chat session will show a filtered mode picker with custom agents * that have a matching `target` property. This enables contributed chat sessions diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 77685cafa97..f7fb12e4c80 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -22,7 +22,7 @@ import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentCanContinueIn, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; suite('AgentSessions', () => { @@ -1949,6 +1949,16 @@ suite('AgentSessions', () => { assert.strictEqual(icon.id, Codicon.cloud.id); }); + test('should return correct name for Growth provider', () => { + const name = getAgentSessionProviderName(AgentSessionProviders.Growth); + assert.strictEqual(name, 'Growth'); + }); + + test('should return correct icon for Growth provider', () => { + const icon = getAgentSessionProviderIcon(AgentSessionProviders.Growth); + assert.strictEqual(icon.id, Codicon.lightbulb.id); + }); + test('should handle Local provider type in model', async () => { return runWithFakedTimers({}, async () => { const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); @@ -2087,6 +2097,25 @@ suite('AgentSessions', () => { }); }); + suite('AgentSessionsViewModel - getAgentCanContinueIn', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return false when contribution.isReadOnly is true', () => { + const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: true }); + assert.strictEqual(result, false); + }); + + test('should return true for Cloud when contribution is not read-only', () => { + const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: false }); + assert.strictEqual(result, true); + }); + + test('should return false for Growth provider', () => { + const result = getAgentCanContinueIn(AgentSessionProviders.Growth); + assert.strictEqual(result, false); + }); + }); + suite('AgentSessionsViewModel - Cancellation and Lifecycle', () => { const disposables = new DisposableStore(); let mockChatSessionsService: MockChatSessionsService; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index a2dc921d69f..df078abc002 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -23,7 +23,12 @@ declare module 'vscode' { /** * The chat session is currently in progress. */ - InProgress = 2 + InProgress = 2, + + /** + * The chat session needs user input (e.g. an unresolved confirmation). + */ + NeedsInput = 3 } export namespace chat {