deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,73 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - GITHUB COPILOT EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/ai-copilot` extension integrates GitHub Copilot language models with Theia AI.
This allows users to authenticate with their GitHub Copilot subscription and use Copilot models (e.g., GPT-4o, Claude Sonnet) through Theia's AI features.
### Authentication
The extension uses GitHub's OAuth Device Flow for authentication:
1. Click the "Copilot" status bar item or run the **Copilot: Sign In** command
2. A dialog appears with a device code - click the link to open GitHub's device authorization page
3. Enter the code and authorize the application
4. The dialog updates to show "Authenticated" and the status bar reflects the signed-in state
Once authenticated, Copilot models become available in the AI Configuration for use with any Theia AI agent.
> **Note:** This extension requires an active GitHub Copilot subscription.
### Configuration
Available models can be configured via the `ai-features.copilot.models` preference:
```json
{
"ai-features.copilot.models": [
"gpt-4o",
"claude-sonnet-4"
]
}
```
### GitHub Enterprise
For GitHub Enterprise users, configure the enterprise URL via the `ai-features.copilot.enterpriseUrl` preference:
```json
{
"ai-features.copilot.enterpriseUrl": "github.mycompany.com"
}
```
### Commands
- **Copilot: Sign In** - Initiates the OAuth device flow authentication
- **Copilot: Sign Out** - Signs out and clears stored credentials
## Additional Information
- [API documentation for `@theia/ai-copilot`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-copilot.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,51 @@
{
"name": "@theia/ai-copilot",
"version": "1.68.0",
"description": "Theia - GitHub Copilot Integration",
"dependencies": {
"@theia/ai-core": "1.68.0",
"@theia/ai-openai": "1.68.0",
"@theia/core": "1.68.0",
"openai": "^6.3.0",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/copilot-frontend-module",
"backend": "lib/node/copilot-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
}
}

View File

@@ -0,0 +1,326 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { DialogProps, DialogError } from '@theia/core/lib/browser/dialogs';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommandService, nls } from '@theia/core';
import { CopilotAuthService, DeviceCodeResponse } from '../common/copilot-auth-service';
const OPEN_AI_CONFIG_VIEW_COMMAND = 'aiConfiguration:open';
type AuthDialogState = 'loading' | 'waiting' | 'polling' | 'success' | 'error';
@injectable()
export class CopilotAuthDialogProps extends DialogProps {
enterpriseUrl?: string;
}
@injectable()
export class CopilotAuthDialog extends ReactDialog<boolean> {
@inject(CopilotAuthService)
protected readonly authService: CopilotAuthService;
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(CommandService)
protected readonly commandService: CommandService;
protected state: AuthDialogState = 'loading';
protected deviceCodeResponse?: DeviceCodeResponse;
protected errorMessage?: string;
protected copied = false;
static readonly ID = 'copilot-auth-dialog';
constructor(
@inject(CopilotAuthDialogProps) protected override readonly props: CopilotAuthDialogProps
) {
super(props);
}
@postConstruct()
protected init(): void {
this.titleNode.textContent = nls.localize('theia/ai/copilot/auth/title', 'Sign in to GitHub Copilot');
this.appendAcceptButton(nls.localize('theia/ai/copilot/auth/authorize', 'I have authorized'));
this.appendCloseButton(nls.localizeByDefault('Cancel'));
}
protected updateButtonStates(): void {
const isPolling = this.state === 'polling';
const isSuccess = this.state === 'success';
if (this.acceptButton) {
this.acceptButton.disabled = isPolling || isSuccess;
if (isSuccess) {
this.acceptButton.style.display = 'none';
}
}
if (this.closeButton) {
if (isSuccess) {
this.closeButton.textContent = nls.localizeByDefault('Close');
}
}
}
override async open(): Promise<boolean | undefined> {
this.initiateFlow();
return super.open();
}
override update(): void {
super.update();
this.updateButtonStates();
}
protected async initiateFlow(): Promise<void> {
try {
this.state = 'loading';
this.update();
this.deviceCodeResponse = await this.authService.initiateDeviceFlow(this.props.enterpriseUrl);
this.state = 'waiting';
this.update();
} catch (error) {
this.state = 'error';
this.errorMessage = error instanceof Error ? error.message : String(error);
this.update();
}
}
protected override async accept(): Promise<void> {
if (this.state !== 'waiting' || !this.deviceCodeResponse) {
return;
}
this.state = 'polling';
this.update();
try {
const success = await this.authService.pollForToken(
this.deviceCodeResponse.device_code,
this.deviceCodeResponse.interval,
this.props.enterpriseUrl
);
if (success) {
this.state = 'success';
this.update();
} else {
this.state = 'error';
this.errorMessage = nls.localize('theia/ai/copilot/auth/expired',
'Authorization expired or was denied. Please try again.');
this.update();
}
} catch (error) {
this.state = 'error';
this.errorMessage = error instanceof Error ? error.message : String(error);
this.update();
}
}
get value(): boolean {
return this.state === 'success';
}
protected override isValid(_value: boolean, _mode: DialogError): DialogError {
if (this.state === 'error') {
return this.errorMessage ?? 'An error occurred';
}
return '';
}
protected handleCopyCode = async (): Promise<void> => {
if (this.deviceCodeResponse) {
await this.clipboardService.writeText(this.deviceCodeResponse.user_code);
this.copied = true;
this.update();
setTimeout(() => {
this.copied = false;
this.update();
}, 2000);
}
};
protected handleOpenUrl = (): void => {
if (this.deviceCodeResponse) {
this.windowService.openNewWindow(this.deviceCodeResponse.verification_uri, { external: true });
}
};
protected render(): React.ReactNode {
return (
<div className="theia-copilot-auth-dialog-content">
{this.renderContent()}
</div>
);
}
protected renderContent(): React.ReactNode {
switch (this.state) {
case 'loading':
return this.renderLoading();
case 'waiting':
return this.renderWaiting();
case 'polling':
return this.renderPolling();
case 'success':
return this.renderSuccess();
case 'error':
return this.renderError();
default:
return undefined;
}
}
protected renderLoading(): React.ReactNode {
return (
<div className="theia-copilot-auth-state">
<div className="theia-spin">
<span className="codicon codicon-loading"></span>
</div>
<p>{nls.localize('theia/ai/copilot/auth/initiating', 'Initiating authentication...')}</p>
</div>
);
}
protected renderWaiting(): React.ReactNode {
const response = this.deviceCodeResponse!;
return (
<div className="theia-copilot-auth-waiting">
<p className="theia-copilot-auth-instructions">
{nls.localize('theia/ai/copilot/auth/instructions',
'To authorize Theia to use GitHub Copilot, visit the URL below and enter the code:')}
</p>
<div className="theia-copilot-auth-code-section">
<div className="theia-copilot-auth-code-display">
<span className="theia-copilot-auth-code">{response.user_code}</span>
<button
className="theia-button secondary theia-copilot-copy-button"
onClick={this.handleCopyCode}
title={this.copied
? nls.localize('theia/ai/copilot/auth/copied', 'Copied!')
: nls.localize('theia/ai/copilot/auth/copyCode', 'Copy code')}
>
<span className={`codicon ${this.copied ? 'codicon-check' : 'codicon-copy'}`}></span>
{nls.localizeByDefault('Copy')}
</button>
</div>
</div>
<div className="theia-copilot-auth-url-section">
<button
className="theia-button theia-copilot-open-url-button"
onClick={this.handleOpenUrl}
>
<span className="codicon codicon-link-external"></span>
{nls.localize('theia/ai/copilot/auth/openGitHub', 'Open GitHub')}
</button>
<span className="theia-copilot-auth-url">{response.verification_uri}</span>
</div>
<p className="theia-copilot-auth-hint">
{nls.localize('theia/ai/copilot/auth/hint',
'After entering the code and authorizing, click "I have authorized" below.')}
</p>
<div className="theia-copilot-auth-privacy">
<p className="theia-copilot-auth-privacy-text">
{nls.localize('theia/ai/copilot/auth/privacy',
'Theia is an open-source project. We only request access to your GitHub username ' +
'to connect to GitHub Copilot services — no other data is accessed or stored.')}
</p>
<p className="theia-copilot-auth-tos-text">
{nls.localize('theia/ai/copilot/auth/tos',
'By signing in, you agree to the ')}
<a
href="https://docs.github.com/en/site-policy/github-terms/github-terms-of-service"
target="_blank"
rel="noopener noreferrer"
onClick={this.handleOpenTos}
>
{nls.localize('theia/ai/copilot/auth/tosLink', 'GitHub Terms of Service')}
</a>.
</p>
</div>
</div>
);
}
protected handleOpenTos = (e: React.MouseEvent): void => {
e.preventDefault();
this.windowService.openNewWindow('https://docs.github.com/en/site-policy/github-terms/github-terms-of-service', { external: true });
};
protected renderPolling(): React.ReactNode {
return (
<div className="theia-copilot-auth-state">
<div className="theia-spin">
<span className="codicon codicon-loading"></span>
</div>
<p>{nls.localize('theia/ai/copilot/auth/verifying', 'Verifying authorization...')}</p>
</div>
);
}
protected renderSuccess(): React.ReactNode {
return (
<div className="theia-copilot-auth-state theia-copilot-auth-success">
<span className="codicon codicon-check"></span>
<p>{nls.localize('theia/ai/copilot/auth/success', 'Successfully signed in to GitHub Copilot!')}</p>
<p className="theia-copilot-auth-success-hint">
{nls.localize('theia/ai/copilot/auth/successHint',
'If your GitHub account has access to Copilot, you can now configure Copilot language models in the ')}
<a href="#" onClick={this.handleOpenAIConfig}>
{nls.localize('theia/ai/copilot/auth/aiConfiguration', 'AI Configuration')}
</a>.
</p>
</div>
);
}
protected handleOpenAIConfig = (e: React.MouseEvent): void => {
e.preventDefault();
this.commandService.executeCommand(OPEN_AI_CONFIG_VIEW_COMMAND);
};
protected handleRetry = (): void => {
this.initiateFlow();
};
protected renderError(): React.ReactNode {
return (
<div className="theia-copilot-auth-state theia-copilot-auth-error">
<span className="codicon codicon-error"></span>
<p>{this.errorMessage}</p>
<button
className="theia-button"
onClick={this.handleRetry}
>
{nls.localizeByDefault('Try Again')}
</button>
</div>
);
}
}

View File

@@ -0,0 +1,93 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Command, CommandContribution, CommandRegistry, Disposable, DisposableCollection, PreferenceService } from '@theia/core';
import { CopilotAuthService, CopilotAuthState } from '../common/copilot-auth-service';
import { CopilotAuthDialog, CopilotAuthDialogProps } from './copilot-auth-dialog';
import { COPILOT_ENTERPRISE_URL_PREF } from '../common/copilot-preferences';
export namespace CopilotCommands {
export const SIGN_IN: Command = Command.toLocalizedCommand(
{ id: 'copilot.signIn', label: 'Sign in to GitHub Copilot', category: 'Copilot' },
'theia/ai/copilot/commands/signIn',
'theia/ai/copilot/category'
);
export const SIGN_OUT: Command = Command.toLocalizedCommand(
{ id: 'copilot.signOut', label: 'Sign out of GitHub Copilot', category: 'Copilot' },
'theia/ai/copilot/commands/signOut',
'theia/ai/copilot/category'
);
}
/**
* Command contribution for GitHub Copilot authentication commands.
*/
@injectable()
export class CopilotCommandContribution implements CommandContribution, Disposable {
@inject(CopilotAuthService)
protected readonly authService: CopilotAuthService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CopilotAuthDialogProps)
protected readonly dialogProps: CopilotAuthDialogProps;
@inject(CopilotAuthDialog)
protected readonly authDialog: CopilotAuthDialog;
protected authState: CopilotAuthState = { isAuthenticated: false };
protected readonly toDispose = new DisposableCollection();
@postConstruct()
protected init(): void {
this.authService.getAuthState().then(state => {
this.authState = state;
});
this.toDispose.push(this.authService.onAuthStateChanged(state => {
this.authState = state;
}));
}
dispose(): void {
this.toDispose.dispose();
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(CopilotCommands.SIGN_IN, {
execute: async () => {
const enterpriseUrl = this.preferenceService.get<string>(COPILOT_ENTERPRISE_URL_PREF);
this.dialogProps.enterpriseUrl = enterpriseUrl || undefined;
const result = await this.authDialog.open();
if (result) {
this.authState = await this.authService.getAuthState();
}
},
isEnabled: () => !this.authState.isAuthenticated
});
registry.registerCommand(CopilotCommands.SIGN_OUT, {
execute: async () => {
await this.authService.signOut();
},
isEnabled: () => this.authState.isAuthenticated
});
}
}

View File

@@ -0,0 +1,89 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { PreferenceService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID } from '../common';
import { COPILOT_MODELS_PREF, COPILOT_ENTERPRISE_URL_PREF } from '../common/copilot-preferences';
import { AICorePreferences, PREFERENCE_NAME_MAX_RETRIES } from '@theia/ai-core/lib/common/ai-core-preferences';
@injectable()
export class CopilotFrontendApplicationContribution implements FrontendApplicationContribution {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CopilotLanguageModelsManager)
protected readonly manager: CopilotLanguageModelsManager;
@inject(AICorePreferences)
protected readonly aiCorePreferences: AICorePreferences;
protected prevModels: string[] = [];
onStart(): void {
this.preferenceService.ready.then(() => {
const models = this.preferenceService.get<string[]>(COPILOT_MODELS_PREF, []);
this.manager.createOrUpdateLanguageModels(...models.map((modelId: string) => this.createCopilotModelDescription(modelId)));
this.prevModels = [...models];
this.preferenceService.onPreferenceChanged(event => {
if (event.preferenceName === COPILOT_MODELS_PREF) {
this.handleModelChanges(this.preferenceService.get<string[]>(COPILOT_MODELS_PREF, []));
} else if (event.preferenceName === COPILOT_ENTERPRISE_URL_PREF) {
this.manager.refreshModelsStatus();
}
});
this.aiCorePreferences.onPreferenceChanged(event => {
if (event.preferenceName === PREFERENCE_NAME_MAX_RETRIES) {
this.updateAllModels();
}
});
});
}
protected handleModelChanges(newModels: string[]): void {
const oldModels = new Set(this.prevModels);
const updatedModels = new Set(newModels);
const modelsToRemove = [...oldModels].filter(model => !updatedModels.has(model));
const modelsToAdd = [...updatedModels].filter(model => !oldModels.has(model));
this.manager.removeLanguageModels(...modelsToRemove.map(model => `${COPILOT_PROVIDER_ID}/${model}`));
this.manager.createOrUpdateLanguageModels(...modelsToAdd.map((modelId: string) => this.createCopilotModelDescription(modelId)));
this.prevModels = newModels;
}
protected updateAllModels(): void {
const models = this.preferenceService.get<string[]>(COPILOT_MODELS_PREF, []);
this.manager.createOrUpdateLanguageModels(...models.map((modelId: string) => this.createCopilotModelDescription(modelId)));
}
protected createCopilotModelDescription(modelId: string): CopilotModelDescription {
const id = `${COPILOT_PROVIDER_ID}/${modelId}`;
const maxRetries = this.aiCorePreferences.get(PREFERENCE_NAME_MAX_RETRIES) ?? 3;
return {
id,
model: modelId,
enableStreaming: true,
supportsStructuredOutput: true,
maxRetries
};
}
}

View File

@@ -0,0 +1,86 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { CommandContribution, Emitter, Event, nls, PreferenceContribution } from '@theia/core';
import {
FrontendApplicationContribution,
RemoteConnectionProvider,
ServiceConnectionProvider
} from '@theia/core/lib/browser';
import {
CopilotLanguageModelsManager,
COPILOT_LANGUAGE_MODELS_MANAGER_PATH,
CopilotAuthService,
COPILOT_AUTH_SERVICE_PATH,
CopilotAuthServiceClient,
CopilotAuthState
} from '../common';
import { CopilotPreferencesSchema } from '../common/copilot-preferences';
import { CopilotFrontendApplicationContribution } from './copilot-frontend-application-contribution';
import { CopilotCommandContribution } from './copilot-command-contribution';
import { CopilotStatusBarContribution } from './copilot-status-bar-contribution';
import { CopilotAuthDialog, CopilotAuthDialogProps } from './copilot-auth-dialog';
class CopilotAuthServiceClientImpl implements CopilotAuthServiceClient {
protected readonly onAuthStateChangedEmitter = new Emitter<CopilotAuthState>();
readonly onAuthStateChangedEvent: Event<CopilotAuthState> = this.onAuthStateChangedEmitter.event;
onAuthStateChanged(state: CopilotAuthState): void {
this.onAuthStateChangedEmitter.fire(state);
}
}
export default new ContainerModule(bind => {
bind(PreferenceContribution).toConstantValue({ schema: CopilotPreferencesSchema });
bind(CopilotCommandContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(CopilotCommandContribution);
bind(CopilotStatusBarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CopilotStatusBarContribution);
bind(CopilotAuthDialogProps).toConstantValue({
title: nls.localize('theia/ai/copilot/commands/signIn', 'Sign in to GitHub Copilot')
});
bind(CopilotAuthDialog).toSelf().inSingletonScope();
bind(CopilotFrontendApplicationContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(CopilotFrontendApplicationContribution);
bind(CopilotAuthServiceClientImpl).toConstantValue(new CopilotAuthServiceClientImpl());
bind(CopilotAuthServiceClient).toService(CopilotAuthServiceClientImpl);
bind(CopilotLanguageModelsManager).toDynamicValue(ctx => {
const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
return provider.createProxy<CopilotLanguageModelsManager>(COPILOT_LANGUAGE_MODELS_MANAGER_PATH);
}).inSingletonScope();
bind(CopilotAuthService).toDynamicValue(ctx => {
const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
const clientImpl = ctx.container.get(CopilotAuthServiceClientImpl);
const proxy = provider.createProxy<CopilotAuthService>(COPILOT_AUTH_SERVICE_PATH, clientImpl);
return new Proxy(proxy, {
get(target: CopilotAuthService, prop: string | symbol, receiver: unknown): unknown {
if (prop === 'onAuthStateChanged') {
return clientImpl.onAuthStateChangedEvent;
}
return Reflect.get(target, prop, receiver);
}
}) as CopilotAuthService;
}).inSingletonScope();
});

View File

@@ -0,0 +1,88 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar-types';
import { Disposable, DisposableCollection, nls } from '@theia/core';
import { CopilotAuthService, CopilotAuthState } from '../common/copilot-auth-service';
import { CopilotCommands } from './copilot-command-contribution';
const COPILOT_STATUS_BAR_ID = 'copilot-auth-status';
/**
* Frontend contribution that displays GitHub Copilot authentication status in the status bar.
*/
@injectable()
export class CopilotStatusBarContribution implements FrontendApplicationContribution, Disposable {
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(CopilotAuthService)
protected readonly authService: CopilotAuthService;
protected authState: CopilotAuthState = { isAuthenticated: false };
protected readonly toDispose = new DisposableCollection();
@postConstruct()
protected init(): void {
this.toDispose.push(this.authService.onAuthStateChanged(state => {
this.authState = state;
this.updateStatusBar();
}));
}
dispose(): void {
this.toDispose.dispose();
}
onStart(): void {
this.authService.getAuthState().then(state => {
this.authState = state;
this.updateStatusBar();
});
}
protected updateStatusBar(): void {
const isAuthenticated = this.authState.isAuthenticated;
let text: string;
let tooltip: string;
let command: string;
if (isAuthenticated) {
const accountLabel = this.authState.accountLabel ?? 'GitHub';
text = `$(github) ${accountLabel}`;
tooltip = nls.localize('theia/ai/copilot/statusBar/signedIn',
'Signed in to GitHub Copilot as {0}. Click to sign out.', accountLabel);
command = CopilotCommands.SIGN_OUT.id;
} else {
text = '$(github) Copilot';
tooltip = nls.localize('theia/ai/copilot/statusBar/signedOut',
'Not signed in to GitHub Copilot. Click to sign in.');
command = CopilotCommands.SIGN_IN.id;
}
this.statusBar.setElement(COPILOT_STATUS_BAR_ID, {
text,
tooltip,
alignment: StatusBarAlignment.RIGHT,
priority: 100,
command
});
}
}

View File

@@ -0,0 +1,20 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './copilot-auth-dialog';
export * from './copilot-command-contribution';
export * from './copilot-status-bar-contribution';
export * from './copilot-frontend-application-contribution';

View File

@@ -0,0 +1,167 @@
/* *****************************************************************************
* Copyright (C) 2026 EclipseSource GmbH.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
**************************************************************************** */
.theia-copilot-auth-dialog-content {
padding: 16px;
min-width: 400px;
}
.theia-copilot-auth-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 24px;
}
.theia-copilot-auth-state .theia-spin {
font-size: 32px;
}
.theia-copilot-auth-success .codicon-check {
font-size: 48px;
color: var(--theia-successBackground);
}
.theia-copilot-auth-error .codicon-error {
font-size: 48px;
color: var(--theia-errorForeground);
}
.theia-copilot-auth-waiting {
display: flex;
flex-direction: column;
gap: 16px;
}
.theia-copilot-auth-instructions {
margin: 0;
line-height: 1.5;
}
.theia-copilot-auth-code-section {
display: flex;
justify-content: center;
}
.theia-copilot-auth-code-display {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--theia-editor-background);
border-radius: 4px;
border: 1px solid var(--theia-panel-border);
}
.theia-copilot-auth-code {
font-family: var(--theia-code-font-family);
font-size: 24px;
font-weight: bold;
letter-spacing: 2px;
color: var(--theia-textLink-foreground);
}
.theia-copilot-copy-button {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
}
.theia-copilot-auth-url-section {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.theia-copilot-open-url-button {
display: flex;
align-items: center;
gap: 6px;
}
.theia-copilot-auth-url {
font-family: var(--theia-code-font-family);
font-size: 12px;
color: var(--theia-descriptionForeground);
}
.theia-copilot-auth-hint {
margin: 0;
font-size: 12px;
color: var(--theia-descriptionForeground);
text-align: center;
}
.theia-copilot-auth-privacy {
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid var(--theia-panel-border);
}
.theia-copilot-auth-privacy-text,
.theia-copilot-auth-tos-text {
margin: 0 0 8px 0;
font-size: 11px;
color: var(--theia-descriptionForeground);
line-height: 1.4;
}
.theia-copilot-auth-tos-text:last-child {
margin-bottom: 0;
}
.theia-copilot-auth-privacy a {
color: var(--theia-textLink-foreground);
text-decoration: none;
}
.theia-copilot-auth-privacy a:hover {
text-decoration: underline;
}
.theia-copilot-auth-success-hint {
margin: 8px 0 0 0;
font-size: 12px;
color: var(--theia-descriptionForeground);
text-align: center;
}
.theia-copilot-auth-success-hint a {
color: var(--theia-textLink-foreground);
text-decoration: none;
cursor: pointer;
}
.theia-copilot-auth-success-hint a:hover {
text-decoration: underline;
}
/* Spinning animation for loading indicator */
.theia-spin .codicon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,103 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Event } from '@theia/core';
export const COPILOT_AUTH_SERVICE_PATH = '/services/copilot/auth';
export const CopilotAuthService = Symbol('CopilotAuthService');
export const CopilotAuthServiceClient = Symbol('CopilotAuthServiceClient');
/**
* Response from GitHub's device code endpoint.
*/
export interface DeviceCodeResponse {
/** URL where user should enter the code (e.g., https://github.com/login/device) */
verification_uri: string;
/** Code to display to the user (e.g., XXXX-XXXX) */
user_code: string;
/** Device code used for polling */
device_code: string;
/** Polling interval in seconds */
interval: number;
/** Expiration time in seconds */
expires_in: number;
}
/**
* Current authentication state.
*/
export interface CopilotAuthState {
/** Whether the user is authenticated */
isAuthenticated: boolean;
/** GitHub username if authenticated */
accountLabel?: string;
/** GitHub Enterprise URL if using enterprise */
enterpriseUrl?: string;
}
/**
* Client interface for receiving auth state change notifications.
*/
export interface CopilotAuthServiceClient {
onAuthStateChanged(state: CopilotAuthState): void;
}
/**
* Service for handling GitHub Copilot OAuth Device Flow authentication.
*/
export interface CopilotAuthService {
/**
* Initiates the OAuth Device Flow.
* Returns device code information for the UI to display.
* @param enterpriseUrl Optional GitHub Enterprise domain
*/
initiateDeviceFlow(enterpriseUrl?: string): Promise<DeviceCodeResponse>;
/**
* Polls for the access token after user authorizes.
* @param deviceCode The device code from initiateDeviceFlow
* @param interval Polling interval in seconds
* @param enterpriseUrl Optional GitHub Enterprise domain
* @returns true if authentication succeeded, false if expired/denied
*/
pollForToken(deviceCode: string, interval: number, enterpriseUrl?: string): Promise<boolean>;
/**
* Get the current authentication state.
*/
getAuthState(): Promise<CopilotAuthState>;
/**
* Get the access token for API calls.
* @returns The access token or undefined if not authenticated
*/
getAccessToken(): Promise<string | undefined>;
/**
* Sign out and clear stored credentials.
*/
signOut(): Promise<void>;
/**
* Set the client to receive auth state change notifications.
*/
setClient(client: CopilotAuthServiceClient | undefined): void;
/**
* Event fired when authentication state changes.
*/
readonly onAuthStateChanged: Event<CopilotAuthState>;
}

View File

@@ -0,0 +1,59 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export const COPILOT_LANGUAGE_MODELS_MANAGER_PATH = '/services/copilot/language-model-manager';
export const CopilotLanguageModelsManager = Symbol('CopilotLanguageModelsManager');
export const COPILOT_PROVIDER_ID = 'copilot';
export interface CopilotModelDescription {
/**
* The identifier of the model which will be shown in the UI.
* Format: copilot/{modelName}
*/
id: string;
/**
* The model ID as used by the Copilot API (e.g., 'gpt-4o', 'claude-3.5-sonnet').
*/
model: string;
/**
* Indicate whether the streaming API shall be used.
*/
enableStreaming: boolean;
/**
* Flag to configure whether the model supports structured output.
*/
supportsStructuredOutput: boolean;
/**
* Maximum number of retry attempts when a request fails.
*/
maxRetries: number;
}
export interface CopilotLanguageModelsManager {
/**
* Create or update language models in the registry.
*/
createOrUpdateLanguageModels(...models: CopilotModelDescription[]): Promise<void>;
/**
* Remove language models from the registry.
*/
removeLanguageModels(...modelIds: string[]): void;
/**
* Refresh the status of all Copilot models (e.g., after authentication state changes).
*/
refreshModelsStatus(): Promise<void>;
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/common/ai-core-preferences';
import { nls, PreferenceSchema } from '@theia/core';
export const COPILOT_MODELS_PREF = 'ai-features.copilot.models';
export const COPILOT_ENTERPRISE_URL_PREF = 'ai-features.copilot.enterpriseUrl';
export const CopilotPreferencesSchema: PreferenceSchema = {
properties: {
[COPILOT_MODELS_PREF]: {
type: 'array',
description: nls.localize('theia/ai/copilot/models/description',
'GitHub Copilot models to use. Available models depend on your Copilot subscription.'),
title: AI_CORE_PREFERENCES_TITLE,
// https://models.dev/?search=copilot
default: [
'claude-haiku-4.5',
'claude-sonnet-4.5',
'claude-opus-4.5',
'gemini-2.5-pro',
'gpt-4.1',
'gpt-4o',
'gpt-5-mini',
'gpt-5.2',
],
items: {
type: 'string'
}
},
[COPILOT_ENTERPRISE_URL_PREF]: {
type: 'string',
markdownDescription: nls.localize('theia/ai/copilot/enterpriseUrl/mdDescription',
'GitHub Enterprise domain for Copilot API (e.g., `github.mycompany.com`). Leave empty for GitHub.com.'),
title: AI_CORE_PREFERENCES_TITLE,
default: ''
}
}
};

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './copilot-language-models-manager';
export * from './copilot-auth-service';
export * from './copilot-preferences';

View File

@@ -0,0 +1,274 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core';
import { KeyStoreService } from '@theia/core/lib/common/key-store';
import {
CopilotAuthService,
CopilotAuthServiceClient,
CopilotAuthState,
DeviceCodeResponse
} from '../common/copilot-auth-service';
const COPILOT_CLIENT_ID = 'Iv23ctNZvWb5IGBKdyPY';
const COPILOT_SCOPE = 'read:user';
const COPILOT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
const KEYSTORE_SERVICE = 'theia-copilot-auth';
const KEYSTORE_ACCOUNT = 'github-copilot';
const USER_AGENT = 'Theia-Copilot/1.0.0';
/**
* Maximum number of polling attempts for token retrieval.
* With a default 5-second interval, this allows approximately 5 minutes of polling.
*/
const MAX_POLLING_ATTEMPTS = 60;
interface StoredCredentials {
accessToken: string;
accountLabel?: string;
enterpriseUrl?: string;
}
/**
* Backend implementation of the GitHub Copilot OAuth Device Flow authentication service.
* Handles device code generation, token polling, and credential storage.
*/
@injectable()
export class CopilotAuthServiceImpl implements CopilotAuthService {
@inject(KeyStoreService)
protected readonly keyStoreService: KeyStoreService;
protected client: CopilotAuthServiceClient | undefined;
protected cachedState: CopilotAuthState | undefined;
protected readonly onAuthStateChangedEmitter = new Emitter<CopilotAuthState>();
readonly onAuthStateChanged: Event<CopilotAuthState> = this.onAuthStateChangedEmitter.event;
setClient(client: CopilotAuthServiceClient | undefined): void {
this.client = client;
}
protected getOAuthEndpoints(enterpriseUrl?: string): { deviceCodeUrl: string; accessTokenUrl: string } {
if (enterpriseUrl) {
const domain = enterpriseUrl
.replace(/^https?:\/\//, '')
.replace(/\/$/, '');
return {
deviceCodeUrl: `https://${domain}/login/device/code`,
accessTokenUrl: `https://${domain}/login/oauth/access_token`
};
}
return {
deviceCodeUrl: 'https://github.com/login/device/code',
accessTokenUrl: 'https://github.com/login/oauth/access_token'
};
}
async initiateDeviceFlow(enterpriseUrl?: string): Promise<DeviceCodeResponse> {
const endpoints = this.getOAuthEndpoints(enterpriseUrl);
const response = await fetch(endpoints.deviceCodeUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
},
body: JSON.stringify({
client_id: COPILOT_CLIENT_ID,
scope: COPILOT_SCOPE
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to initiate device authorization: ${response.status} - ${errorText}`);
}
const data = await response.json() as DeviceCodeResponse;
return data;
}
async pollForToken(deviceCode: string, interval: number, enterpriseUrl?: string): Promise<boolean> {
const endpoints = this.getOAuthEndpoints(enterpriseUrl);
let attempts = 0;
while (attempts < MAX_POLLING_ATTEMPTS) {
await this.delay(interval * 1000);
attempts++;
const response = await fetch(endpoints.accessTokenUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
},
body: JSON.stringify({
client_id: COPILOT_CLIENT_ID,
device_code: deviceCode,
grant_type: COPILOT_GRANT_TYPE
})
});
if (!response.ok) {
console.error(`Token request failed: ${response.status}`);
continue;
}
const data = await response.json() as {
access_token?: string;
error?: string;
error_description?: string;
};
if (data.access_token) {
// Get user info for account label
const accountLabel = await this.fetchAccountLabel(data.access_token, enterpriseUrl);
// Store credentials
const credentials: StoredCredentials = {
accessToken: data.access_token,
accountLabel,
enterpriseUrl
};
await this.keyStoreService.setPassword(
KEYSTORE_SERVICE,
KEYSTORE_ACCOUNT,
JSON.stringify(credentials)
);
// Update cached state and notify
const newState: CopilotAuthState = {
isAuthenticated: true,
accountLabel,
enterpriseUrl
};
this.updateAuthState(newState);
return true;
}
if (data.error === 'authorization_pending') {
// User hasn't authorized yet, continue polling
continue;
}
if (data.error === 'slow_down') {
// Increase polling interval
interval += 5;
continue;
}
if (data.error === 'expired_token' || data.error === 'access_denied') {
console.error(`Authorization failed: ${data.error} - ${data.error_description}`);
return false;
}
if (data.error) {
console.error(`Unexpected error: ${data.error} - ${data.error_description}`);
return false;
}
}
return false;
}
protected async fetchAccountLabel(accessToken: string, enterpriseUrl?: string): Promise<string | undefined> {
try {
const apiBaseUrl = enterpriseUrl
? `https://${enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}/api/v3`
: 'https://api.github.com';
const response = await fetch(`${apiBaseUrl}/user`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'User-Agent': USER_AGENT,
'Accept': 'application/vnd.github.v3+json'
}
});
if (response.ok) {
const userData = await response.json() as { login?: string };
return userData.login;
}
} catch (error) {
console.warn('Failed to fetch GitHub user info:', error);
}
return undefined;
}
async getAuthState(): Promise<CopilotAuthState> {
if (this.cachedState) {
return this.cachedState;
}
try {
const stored = await this.keyStoreService.getPassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
if (stored) {
const credentials: StoredCredentials = JSON.parse(stored);
this.cachedState = {
isAuthenticated: true,
accountLabel: credentials.accountLabel,
enterpriseUrl: credentials.enterpriseUrl
};
return this.cachedState;
}
} catch (error) {
console.warn('Failed to retrieve Copilot credentials:', error);
}
this.cachedState = { isAuthenticated: false };
return this.cachedState;
}
async getAccessToken(): Promise<string | undefined> {
try {
const stored = await this.keyStoreService.getPassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
if (stored) {
const credentials: StoredCredentials = JSON.parse(stored);
return credentials.accessToken;
}
} catch (error) {
console.warn('Failed to retrieve Copilot access token:', error);
}
return undefined;
}
async signOut(): Promise<void> {
try {
await this.keyStoreService.deletePassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
} catch (error) {
console.warn('Failed to delete Copilot credentials:', error);
}
const newState: CopilotAuthState = { isAuthenticated: false };
this.updateAuthState(newState);
}
protected updateAuthState(state: CopilotAuthState): void {
this.cachedState = state;
this.onAuthStateChangedEmitter.fire(state);
this.client?.onAuthStateChanged(state);
}
protected delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,58 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import {
CopilotLanguageModelsManager,
COPILOT_LANGUAGE_MODELS_MANAGER_PATH,
CopilotAuthService,
COPILOT_AUTH_SERVICE_PATH,
CopilotAuthServiceClient
} from '../common';
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(CopilotAuthServiceImpl).toSelf().inSingletonScope();
bind(CopilotAuthService).toService(CopilotAuthServiceImpl);
bind(CopilotLanguageModelsManagerImpl).toSelf().inSingletonScope();
bind(CopilotLanguageModelsManager).toService(CopilotLanguageModelsManagerImpl);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler<CopilotAuthServiceClient>(
COPILOT_AUTH_SERVICE_PATH,
client => {
const authService = ctx.container.get<CopilotAuthServiceImpl>(CopilotAuthService);
authService.setClient(client);
return authService;
}
)
).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(
COPILOT_LANGUAGE_MODELS_MANAGER_PATH,
() => ctx.container.get(CopilotLanguageModelsManager)
)
).inSingletonScope();
});
export default new ContainerModule(bind => {
bind(ConnectionContainerModule).toConstantValue(copilotConnectionModule);
});

View File

@@ -0,0 +1,262 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
ImageContent,
LanguageModel,
LanguageModelMessage,
LanguageModelParsedResponse,
LanguageModelRequest,
LanguageModelResponse,
LanguageModelStatus,
LanguageModelTextResponse,
TokenUsageService,
UserRequest
} from '@theia/ai-core';
import { CancellationToken } from '@theia/core';
import OpenAI from 'openai';
import { RunnableToolFunctionWithoutParse } from 'openai/lib/RunnableFunction';
import { ChatCompletionMessageParam } from 'openai/resources';
import { StreamingAsyncIterator } from '@theia/ai-openai/lib/node/openai-streaming-iterator';
import { COPILOT_PROVIDER_ID } from '../common';
import type { RunnerOptions } from 'openai/lib/AbstractChatCompletionRunner';
import type { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
const COPILOT_API_BASE_URL = 'https://api.githubcopilot.com';
const USER_AGENT = 'Theia-Copilot/1.0.0';
/**
* Language model implementation for GitHub Copilot.
* Uses the OpenAI SDK to communicate with the Copilot API.
*/
export class CopilotLanguageModel implements LanguageModel {
protected runnerOptions: RunnerOptions = {
maxChatCompletions: 100
};
constructor(
public readonly id: string,
public model: string,
public status: LanguageModelStatus,
public enableStreaming: boolean,
public supportsStructuredOutput: boolean,
public maxRetries: number,
protected readonly accessTokenProvider: () => Promise<string | undefined>,
protected readonly enterpriseUrlProvider: () => string | undefined,
protected readonly tokenUsageService?: TokenUsageService
) { }
protected getSettings(request: LanguageModelRequest): Record<string, unknown> {
return request.settings ?? {};
}
async request(request: UserRequest, cancellationToken?: CancellationToken): Promise<LanguageModelResponse> {
const openai = await this.initializeCopilotClient();
if (request.response_format?.type === 'json_schema' && this.supportsStructuredOutput) {
return this.handleStructuredOutputRequest(openai, request);
}
const settings = this.getSettings(request);
if (!this.enableStreaming || (typeof settings.stream === 'boolean' && !settings.stream)) {
return this.handleNonStreamingRequest(openai, request);
}
if (cancellationToken?.isCancellationRequested) {
return { text: '' };
}
if (this.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
settings['stream_options'] = { include_usage: true };
}
let runner: ChatCompletionStream;
const tools = this.createTools(request);
if (tools) {
runner = openai.chat.completions.runTools({
model: this.model,
messages: this.processMessages(request.messages),
stream: true,
tools: tools,
tool_choice: 'auto',
...settings
}, {
...this.runnerOptions,
maxRetries: this.maxRetries
});
} else {
runner = openai.chat.completions.stream({
model: this.model,
messages: this.processMessages(request.messages),
stream: true,
...settings
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { stream: new StreamingAsyncIterator(runner as any, request.requestId, cancellationToken, this.tokenUsageService, this.id) };
}
protected async handleNonStreamingRequest(openai: OpenAI, request: UserRequest): Promise<LanguageModelTextResponse> {
const settings = this.getSettings(request);
const response = await openai.chat.completions.create({
model: this.model,
messages: this.processMessages(request.messages),
...settings
});
const message = response.choices[0].message;
if (this.tokenUsageService && response.usage) {
await this.tokenUsageService.recordTokenUsage(
this.id,
{
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
requestId: request.requestId
}
);
}
return {
text: message.content ?? ''
};
}
protected async handleStructuredOutputRequest(openai: OpenAI, request: UserRequest): Promise<LanguageModelParsedResponse> {
const settings = this.getSettings(request);
const result = await openai.chat.completions.parse({
model: this.model,
messages: this.processMessages(request.messages),
response_format: request.response_format,
...settings
});
const message = result.choices[0].message;
if (message.refusal || message.parsed === undefined) {
console.error('Error in Copilot chat completion:', JSON.stringify(message));
}
if (this.tokenUsageService && result.usage) {
await this.tokenUsageService.recordTokenUsage(
this.id,
{
inputTokens: result.usage.prompt_tokens,
outputTokens: result.usage.completion_tokens,
requestId: request.requestId
}
);
}
return {
content: message.content ?? '',
parsed: message.parsed
};
}
protected createTools(request: LanguageModelRequest): RunnableToolFunctionWithoutParse[] | undefined {
return request.tools?.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
function: (args_string: string) => tool.handler(args_string)
}
} as RunnableToolFunctionWithoutParse));
}
protected async initializeCopilotClient(): Promise<OpenAI> {
const accessToken = await this.accessTokenProvider();
if (!accessToken) {
throw new Error('Not authenticated with GitHub Copilot. Please sign in first.');
}
const enterpriseUrl = this.enterpriseUrlProvider();
const baseURL = enterpriseUrl
? `https://copilot-api.${enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}`
: COPILOT_API_BASE_URL;
return new OpenAI({
apiKey: accessToken,
baseURL,
defaultHeaders: {
'User-Agent': USER_AGENT,
'Openai-Intent': 'conversation-edits',
'X-Initiator': 'user'
}
});
}
protected processMessages(messages: LanguageModelMessage[]): ChatCompletionMessageParam[] {
return messages.filter(m => m.type !== 'thinking').map(m => this.toOpenAIMessage(m));
}
protected toOpenAIMessage(message: LanguageModelMessage): ChatCompletionMessageParam {
if (LanguageModelMessage.isTextMessage(message)) {
return {
role: this.toOpenAiRole(message),
content: message.text
};
}
if (LanguageModelMessage.isToolUseMessage(message)) {
return {
role: 'assistant',
tool_calls: [{
id: message.id,
function: {
name: message.name,
arguments: JSON.stringify(message.input)
},
type: 'function'
}]
};
}
if (LanguageModelMessage.isToolResultMessage(message)) {
return {
role: 'tool',
tool_call_id: message.tool_use_id,
content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
};
}
if (LanguageModelMessage.isImageMessage(message) && message.actor === 'user') {
return {
role: 'user',
content: [{
type: 'image_url',
image_url: {
url: ImageContent.isBase64(message.image)
? `data:${message.image.mimeType};base64,${message.image.base64data}`
: message.image.url
}
}]
};
}
throw new Error(`Unknown message type: '${JSON.stringify(message)}'`);
}
protected toOpenAiRole(message: LanguageModelMessage): 'developer' | 'user' | 'assistant' | 'system' {
if (message.actor === 'system') {
return 'developer';
} else if (message.actor === 'ai') {
return 'assistant';
}
return 'user';
}
}

View File

@@ -0,0 +1,118 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { LanguageModelRegistry, LanguageModelStatus, TokenUsageService } from '@theia/ai-core';
import { Disposable, DisposableCollection } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID } from '../common';
import { CopilotLanguageModel } from './copilot-language-model';
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
/**
* Backend implementation of the Copilot language models manager.
* Manages registration and lifecycle of Copilot language models in the AI language model registry.
*/
@injectable()
export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsManager, Disposable {
@inject(LanguageModelRegistry)
protected readonly languageModelRegistry: LanguageModelRegistry;
@inject(TokenUsageService)
protected readonly tokenUsageService: TokenUsageService;
@inject(CopilotAuthServiceImpl)
protected readonly authService: CopilotAuthServiceImpl;
protected enterpriseUrl: string | undefined;
protected readonly toDispose = new DisposableCollection();
@postConstruct()
protected init(): void {
this.toDispose.push(this.authService.onAuthStateChanged(() => {
this.refreshModelsStatus();
}));
}
dispose(): void {
this.toDispose.dispose();
}
setEnterpriseUrl(url: string | undefined): void {
this.enterpriseUrl = url;
}
protected async calculateStatus(): Promise<LanguageModelStatus> {
const authState = await this.authService.getAuthState();
if (authState.isAuthenticated) {
return { status: 'ready' };
}
return { status: 'unavailable', message: 'Not signed in to GitHub Copilot' };
}
async createOrUpdateLanguageModels(...modelDescriptions: CopilotModelDescription[]): Promise<void> {
const status = await this.calculateStatus();
for (const modelDescription of modelDescriptions) {
const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);
if (model) {
if (!(model instanceof CopilotLanguageModel)) {
console.warn(`Copilot: model ${modelDescription.id} is not a Copilot model`);
continue;
}
await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(modelDescription.id, {
model: modelDescription.model,
enableStreaming: modelDescription.enableStreaming,
supportsStructuredOutput: modelDescription.supportsStructuredOutput,
status,
maxRetries: modelDescription.maxRetries
});
} else {
this.languageModelRegistry.addLanguageModels([
new CopilotLanguageModel(
modelDescription.id,
modelDescription.model,
status,
modelDescription.enableStreaming,
modelDescription.supportsStructuredOutput,
modelDescription.maxRetries,
() => this.authService.getAccessToken(),
() => this.enterpriseUrl,
this.tokenUsageService
)
]);
}
}
}
removeLanguageModels(...modelIds: string[]): void {
this.languageModelRegistry.removeLanguageModels(modelIds);
}
async refreshModelsStatus(): Promise<void> {
const status = await this.calculateStatus();
const allModels = await this.languageModelRegistry.getLanguageModels();
for (const model of allModels) {
if (model instanceof CopilotLanguageModel && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(model.id, {
status
});
}
}
}
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './copilot-auth-service-impl';
export * from './copilot-language-model';
export * from './copilot-language-models-manager-impl';

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('ai-copilot package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,22 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../ai-core"
},
{
"path": "../ai-openai"
},
{
"path": "../core"
}
]
}