Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,7 @@ export type NewChatSessionOpenOptions = {
readonly replaceEditor?: boolean;
};

async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise<void> {
export async function openChatSession(accessor: ServicesAccessor, openOptions: NewChatSessionOpenOptions, chatSendOptions?: NewChatSessionSendOptions): Promise<void> {
const viewsService = accessor.get(IViewsService);
const chatService = accessor.get(IChatService);
const chatSessionService = accessor.get(IChatSessionsService);
Expand Down
127 changes: 99 additions & 28 deletions src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,28 +18,28 @@ 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';
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
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';
Expand Down Expand Up @@ -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<string> {
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<AgentInfo | undefined>(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<void> {
// 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)
);
Loading