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 c9ed88f7342470..68e57c4bd9038c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -1321,7 +1321,7 @@ export type NewChatSessionOpenOptions = { readonly replaceEditor?: boolean; }; -async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise { +export async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise { const viewsService = accessor.get(IViewsService); const chatService = accessor.get(IChatService); const chatSessionService = accessor.get(IChatSessionsService); diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 3620b9a1c7bd7e..ce000e508240b8 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { timeout } from '../../../../base/common/async.js'; import { autorun } from '../../../../base/common/observable.js'; import { resolve } from '../../../../base/common/path.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/globals.js'; import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -17,6 +18,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILocalGitService } from '../../../../platform/git/common/localGitService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -24,21 +26,20 @@ import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/co import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { ViewContainerLocation } from '../../../common/views.js'; import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { AgentHostContribution } from '../browser/agentSessions/agentHost/agentHostChatContribution.js'; import { AgentHostTerminalContribution } from '../browser/agentSessions/agentHost/agentHostTerminalContribution.js'; -import { AgentSessionProviders } from '../browser/agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderName } from '../browser/agentSessions/agentSessions.js'; import { isSessionInProgressStatus } from '../browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../browser/agentSessions/agentSessionsService.js'; -import { ChatViewId, ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; -import { ChatEditorInput } from '../browser/widgetHosts/editor/chatEditorInput.js'; -import { ChatViewPane } from '../browser/widgetHosts/viewPane/chatViewPane.js'; +import { ChatViewPaneTarget, IChatWidgetService } from '../browser/chat.js'; +import { ChatSessionPosition, openChatSession } from '../browser/chatSessions/chatSessions.contribution.js'; +import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js'; +import { type AgentInfo, type RootState } from '../../../../platform/agentHost/common/state/sessionState.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatModeKind } from '../common/constants.js'; @@ -255,29 +256,99 @@ registerWorkbenchContribution2(ChatLifecycleHandler.ID, ChatLifecycleHandler, Wo registerWorkbenchContribution2(AgentHostContribution.ID, AgentHostContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(AgentHostTerminalContribution.ID, AgentHostTerminalContribution, WorkbenchPhase.AfterRestored); +// How long to wait for the agent host to surface an AgentInfo before +// throwing an error. Long enough for normal startup, short enough to avoid +// hanging automation indefinitely if the agent host is disabled or fails +// to start. +const AGENT_HOST_REGISTRATION_TIMEOUT_MS = 30_000; + +function getCopilotAgentInfo(rootState: RootState | Error | undefined): AgentInfo | undefined { + if (!rootState || rootState instanceof Error) { + return undefined; + } + return rootState.agents.find(a => a.provider === 'copilotcli'); +} + +/** + * Resolve the actual session-content-provider scheme registered by the local + * agent host. The agent host registers chat sessions under + * `agent-host-${agent.provider}` (e.g. `agent-host-copilotcli`) only after it + * surfaces an `AgentInfo` via `rootState`. This is asynchronous, so the static + * `agent-host-copilot` umbrella commands need to wait for that registration + * before opening a session — otherwise we'd build a URI with a scheme that has + * no content provider and fall back to a fresh local chat session. + */ +async function resolveAgentHostSessionType(agentHostService: IAgentHostService): Promise { + const agent = getCopilotAgentInfo(agentHostService.rootState.value); + if (agent) { + return `agent-host-${agent.provider}`; + } + + // Wait for the first non-empty root state, capped by a timeout. + // The subscription must be disposed on both success and timeout to avoid leaks. + const cts = new CancellationTokenSource(); + const waitForAgent = new Promise(res => { + const sub = agentHostService.rootState.onDidChange(state => { + const found = getCopilotAgentInfo(state); + if (found) { + sub.dispose(); + res(found); + } + }); + cts.token.onCancellationRequested(() => { + sub.dispose(); + res(undefined); + }); + }); + const resolved = await Promise.race([ + waitForAgent, + timeout(AGENT_HOST_REGISTRATION_TIMEOUT_MS).then(() => { + cts.cancel(); + cts.dispose(); + return undefined; + }), + ]); + if (!resolved) { + throw new Error('Agent host did not register a copilotcli agent within the timeout period. Ensure the agent host is enabled and running.'); + } + return `agent-host-${resolved.provider}`; +} + +// Open a new Agent Host session at the given position. Shared by the session +// type picker command and the static sidebar/editor commands below. +// Delegates to `openChatSession` so the session type picker, context keys, +// and welcome flows all stay in sync with the dynamic per-agent path. +async function openNewAgentHostSession(accessor: ServicesAccessor, position: ChatSessionPosition): Promise { + // Snapshot the services we need synchronously — `accessor` is only valid + // before the first `await`. Use the instantiation service to mint a fresh + // accessor for the downstream `openChatSession` call. + const agentHostService = accessor.get(IAgentHostService); + const instantiationService = accessor.get(IInstantiationService); + const sessionType = await resolveAgentHostSessionType(agentHostService); + return instantiationService.invokeFunction(innerAccessor => openChatSession(innerAccessor, { + type: sessionType, + displayName: getAgentSessionProviderName(sessionType), + position, + })); +} + // Register command for opening a new Agent Host session from the session type picker CommandsRegistry.registerCommand( `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.AgentHostCopilot}`, - async (accessor, chatSessionPosition: string) => { - const viewsService = accessor.get(IViewsService); - const resource = URI.from({ - scheme: AgentSessionProviders.AgentHostCopilot, - path: `/untitled-${generateUuid()}`, - }); + (accessor, chatSessionPosition: string) => + openNewAgentHostSession(accessor, chatSessionPosition === 'editor' ? ChatSessionPosition.Editor : ChatSessionPosition.Sidebar) +); - if (chatSessionPosition === 'editor') { - const editorService = accessor.get(IEditorService); - await editorService.openEditor({ - resource, - options: { - override: ChatEditorInput.EditorID, - pinned: true, - }, - }); - } else { - const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(resource); - view.focus(); - } - } +// Static sidebar/editor open commands for the Agent Host umbrella scheme. +// The dynamic per-agent commands (e.g. `agent-host-copilot`) are only +// registered after the agent host starts and surfaces an AgentInfo, which +// is asynchronous. Provide stable command ids that automation (evals) can +// invoke before the dynamic registration has occurred. +CommandsRegistry.registerCommand( + `workbench.action.chat.openNewSessionSidebar.${AgentSessionProviders.AgentHostCopilot}`, + accessor => openNewAgentHostSession(accessor, ChatSessionPosition.Sidebar) +); +CommandsRegistry.registerCommand( + `workbench.action.chat.openNewSessionEditor.${AgentSessionProviders.AgentHostCopilot}`, + accessor => openNewAgentHostSession(accessor, ChatSessionPosition.Editor) );