mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Add chatSessions isReadOnly (#294255)
* PROTOTYPE: Add growth agent (https://github.com/microsoft/vscode-copilot-chat/pull/3460) * support vscode.ChatSessionStatus.NeedsInput in chatSessions ext api ref https://github.com/microsoft/vscode/issues/292430 * Add isReadOnly flag to chat sessions contributions Read-only session types (e.g., Growth) are passive/informational and should not be registered as agents, appear in session target pickers, or be delegation targets. Commands are still registered to support openSessionWithPrompt. * Collapse isReadOnly and canDelegate branches in _enableContribution Both need agent and command registration; picker filtering handles keeping isReadOnly sessions out of the UI separately. the alternative (and probably ideal) UI is to 'grey out'/'disable' the chat input for isReadOnly sessions. That way we don't have this problem at all of a non-functional chatInput * fix description * redundant doc * update test
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3552,7 +3552,8 @@ export enum ChatLocation {
|
||||
export enum ChatSessionStatus {
|
||||
Failed = 0,
|
||||
Completed = 1,
|
||||
InProgress = 2
|
||||
InProgress = 2,
|
||||
NeedsInput = 3
|
||||
}
|
||||
|
||||
export class ChatSessionChangedFile {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,11 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsEx
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
isReadOnly: {
|
||||
description: localize('chatSessionsExtPoint.isReadOnly', 'Whether this session type is for read-only agents that do not support interactive chat. This flag is incompatible with \'canDelegate\'.'),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
customAgentTarget: {
|
||||
description: localize('chatSessionsExtPoint.customAgentTarget', 'When set, the chat session will show a filtered mode picker that prefers custom agents whose target property matches this value. Custom agents without a target property are still shown in all session types. This enables the use of standard agent/mode with contributed sessions.'),
|
||||
type: 'string'
|
||||
@@ -366,6 +371,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
displayName = localize('chat.session.inProgress.background', "Background Agent");
|
||||
} else if (chatSessionType === AgentSessionProviders.Cloud) {
|
||||
displayName = localize('chat.session.inProgress.cloud', "Cloud Agent");
|
||||
} else if (chatSessionType === AgentSessionProviders.Growth) {
|
||||
displayName = localize('chat.session.inProgress.growth', "Growth");
|
||||
} else {
|
||||
displayName = this._contributions.get(chatSessionType)?.contribution.displayName;
|
||||
}
|
||||
@@ -648,7 +655,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void {
|
||||
const disposableStore = new DisposableStore();
|
||||
this._contributionDisposables.set(contribution.type, disposableStore);
|
||||
if (contribution.canDelegate) {
|
||||
if (contribution.isReadOnly || contribution.canDelegate) {
|
||||
disposableStore.add(this._registerAgent(contribution, ext));
|
||||
disposableStore.add(this._registerCommands(contribution));
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export class ChatSuggestNextWidget extends Disposable {
|
||||
return false;
|
||||
}
|
||||
const provider = getAgentSessionProvider(c.type);
|
||||
return provider !== undefined && getAgentCanContinueIn(provider);
|
||||
return provider !== undefined && getAgentCanContinueIn(provider, c);
|
||||
});
|
||||
|
||||
if (showContinueOn && availableContributions.length > 0) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user