Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/copilot-sdk-mirror.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Copilot SDK Mirror Check

permissions:
contents: read

on:
workflow_dispatch:
inputs:
version:
description: 'Version of @github/copilot-sdk to check against'
required: false
default: 'latest'
pull_request:
branches:
- master
paths:
- 'packages/ai-copilot/src/node/copilot-sdk-types.ts'
- 'scripts/copilot-sdk-mirror/**'
- '.github/workflows/copilot-sdk-mirror.yml'
schedule:
- cron: '0 5 * * 1' # Runs every Monday at 5am

jobs:
mirror-check:
name: Copilot SDK Mirror Check

strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
node: ['24.x']

runs-on: ${{ matrix.os }}
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
registry-url: 'https://registry.npmjs.org'

# `@theia/ai-copilot` mirrors the part of the Copilot SDK API it uses instead of depending on the
# package, which would pull the proprietary CLI binary into the tree. Only TypeScript is needed to
# compare the two; the check installs the SDK itself, into a temporary directory.
- name: Install TypeScript
shell: bash
run: npm install --no-save --no-package-lock typescript@~5.9.3

- name: Check the mirrored types
shell: bash
run: node scripts/copilot-sdk-mirror/run.js "${{ inputs.version || 'latest' }}"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<a name="breaking_changes_1.76.0">[Breaking Changes:](#breaking_changes_1.76.0)</a>

- [ai-copilot] removed the direct REST transport and its GitHub OAuth App sign-in; all requests are now served by the official GitHub Copilot CLI, which is signed in from within the application and needs to be reachable on the machine hosting the backend. Access to the Copilot models is granted per OAuth application, and the built-in application was never entitled for the current lineup, so the REST path only ever exposed a small legacy subset. Removed as a consequence: the `CopilotOAuthConfig` symbol and its `DEFAULT_COPILOT_OAUTH_CONFIG` value, the `CopilotLanguageModel` class, `getCopilotApiBaseUrl`, `COPILOT_API_BASE_URL`, and the `enableStreaming` and `supportsStructuredOutput` members of `CopilotModelDescription`. `CopilotAuthService.initiateDeviceFlow`, `pollForToken`, `getAccessToken` and `signOut` are replaced by `startSignIn`, `waitForSignIn` and `cancelSignIn`, since the CLI performs and polls the flow itself and owns the resulting token, and `setExecutablePath` has been added so that the frontend can hand the configured location of the CLI to the backend. Users have to sign in again: the sign-in of the previous version belongs to an OAuth app that is no longer used, so it is removed from the credential store on first start and a notification asks for a new sign-in. Adopters that rebound `CopilotOAuthConfig` to their own OAuth App no longer need to, and adopters relying on the REST transport should note that structured output is not available on the CLI path. See the migration guide [#17919](https://github.com/eclipse-theia/theia/pull/17919)
- [core] widened `DecorationsProvider.onDidChange` to `Event<URI[] | undefined>`, where `undefined` is a flush signalling that all decorations may have changed. `DecorationsService.onDidChangeDecorations` now fires batched payloads that never list removals: an empty map means an unspecified set changed, and clients must re-query the decorations they display on every event [#17766](https://github.com/eclipse-theia/theia/pull/17766)

## 1.75.0 - 8/27/2026
Expand Down
6 changes: 6 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ The project maintains the following source code repositories:

This project leverages the following third party content.

API type declarations mirrored from project github/copilot-sdk (1.0.9)

* License: MIT
* Project: <https://github.com/github/copilot-sdk>
* Source: <https://github.com/github/copilot-sdk>

chalk (2.4.1)

* License: MIT
Expand Down
62 changes: 62 additions & 0 deletions doc/Migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,68 @@ For example, in an `electron-builder` configuration, ensure the `lib/backend/she

The `lib/**/*` glob already covers `lib/backend/shell-integrations/`. If you use a more restrictive `files` pattern, make sure `lib/backend/shell-integrations/**/*` is explicitly included, as `ShellIntegrationInjector` resolves these scripts relative to `__dirname` (i.e. `lib/backend/`).

### v1.76.0

#### GitHub Copilot is served through the Copilot CLI

`@theia/ai-copilot` no longer talks to the Copilot REST API with a GitHub OAuth token of its own. All requests are served by
the official GitHub Copilot CLI, which is launched as a background process on the machine running the backend and spoken to
over the Copilot SDK.

The reason is that GitHub grants access to the Copilot models per OAuth application. The application the previous integration
used is not entitled for the current lineup, so it only ever saw a small legacy subset of the models regardless of the user's
subscription. The Copilot CLI is entitled, so routing through it makes the current models available.

The integration is experimental, and its preferences are marked as such in the settings UI: it is under development and its
API and preferences are subject to change or removal.

**For users:** the Copilot CLI has to be available on the machine running the backend, for example through
`npm install -g @github/copilot`. It is looked up in the installation of the application, on the `PATH` and in the global `npm`
directory; the new `ai-features.copilot.executablePath` preference (or the `COPILOT_CLI_PATH` environment variable of the
backend) points at it when it is installed elsewhere. A packaged application cannot rely on carrying the CLI itself, since it
is a platform-specific binary that cannot be executed from inside an application archive.

The sign-in of the previous version cannot be carried over, because it belongs to that other OAuth application. It is removed
from the credential store on first start, and a notification asks for a new sign-in. Nothing else has to be done: the sign-in
dialog works as before, and it now signs the Copilot CLI in on the user's behalf. Signing out removes the credentials of the
application again and never touches the system keyring entries of other tools or a sign-in of the GitHub CLI.

**For adopters:** the following API has been removed.

- `CopilotOAuthConfig` and `DEFAULT_COPILOT_OAUTH_CONFIG`. Configuring an own OAuth application no longer has an effect, since
the sign-in is performed by the Copilot CLI. Remove any rebinding of the symbol.
- `CopilotLanguageModel`, `getCopilotApiBaseUrl` and `COPILOT_API_BASE_URL`, together with the REST transport they belonged to.
Copilot models are now instances of `CopilotSdkLanguageModel`.
- `CopilotModelDescription.enableStreaming` and `CopilotModelDescription.supportsStructuredOutput`. Requests always stream, and
structured output is not available on this path.
- `CopilotAuthService.initiateDeviceFlow`, `pollForToken` and `getAccessToken`. The sign-in is now driven with `startSignIn`,
`waitForSignIn` and `cancelSignIn`, since the CLI performs and polls the flow itself, and the resulting token is not exposed.

`CopilotAuthService.setExecutablePath` has been added, so that the frontend can hand the configured location of the CLI to the
backend, which cannot read preferences itself. Adopters implementing the interface from scratch have to provide it; the
lookup itself is `CopilotCliLocator` and can be rebound.

`@github/copilot-sdk` is deliberately **not** a dependency of this extension, not even a development one. The package depends
on the CLI, which would put a large proprietary binary into the dependency tree and the lockfile of every application that
includes `@theia/ai-copilot`, and a packaged application cannot execute it from inside its archive anyway. The CLI carries its
own copy of the SDK, and `CopilotSdkLoader` loads it from the CLI that is going to serve the requests. An installed
`@github/copilot-sdk` is used when the CLI does not carry one, so an application that does depend on the package keeps working.
The part of the SDK API this integration uses is mirrored in `copilot-sdk-types.ts`, which records the SDK version it was taken
from and how to update it.

Two consequences are worth planning for. The CLI is a prerequisite on the backend host rather than something the application
ships, so a distribution should either install it or tell its users to. And because the CLI runs on the backend host with one
process per frontend connection, this integration is not suitable for multi-user backend deployments, where every connected
frontend would share a single identity.

Three behavioural details for adopters who look closely: the system prompt of a Theia agent is now the system message of the
Copilot session, replacing the agent instructions the CLI would use, instead of being prepended to the user prompt. The
runtime is pointed at a Copilot home below Theia's configuration directory, so requests sent from Theia no longer appear among
the conversations the user started with their own CLI, and the session of a request is deleted once it has been answered. And
the runtime is configured without the ambient behaviour of the CLI: only the tools of the request are available, and
instructions and skills found on the host, the memory and session stores, host git operations and plugins are off, since
Theia drives the conversation itself.

### v1.75.0

#### React 19 and the automatic JSX runtime
Expand Down
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 86 additions & 14 deletions packages/ai-copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,96 @@
## 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.
This allows users to authenticate with their GitHub Copilot subscription and use Copilot models (e.g., GPT-5, Claude Sonnet) through Theia's AI features.

> **Experimental:** This integration is under development and may be unstable. Its preferences are marked as experimental
> and are subject to change or removal.

Requests are served by the official GitHub Copilot CLI via [`@github/copilot-sdk`](https://www.npmjs.com/package/@github/copilot-sdk).
The CLI is launched as a background process on the machine hosting the Theia backend and is spoken to over JSON-RPC.

> **Note:** This extension requires an active GitHub Copilot subscription and the GitHub Copilot CLI.

### Installing the Copilot CLI

The CLI is not shipped with the application and has to be available on the machine hosting the backend:

```sh
npm install -g @github/copilot
```
Comment thread
ndoschek marked this conversation as resolved.

It is looked up in the installation of the application, on the `PATH` of the backend process and in the global `npm` directory.
When it is installed somewhere else, point at its executable with the `ai-features.copilot.executablePath` preference:

```json
{
"ai-features.copilot.executablePath": "/opt/copilot/copilot"
}
```

The `COPILOT_CLI_PATH` environment variable of the backend process does the same, for a deployment that configures this
centrally rather than per user.

The CLI is not bundled because it is a large platform-specific binary under a proprietary license, and because a packaged
application cannot execute a binary from inside its own archive. An application that wants to ship it has to install it
next to itself and make sure it stays extracted.

Only the CLI has to be installed. It ships the Copilot SDK next to its executable, and that is the copy this integration
loads, so `@github/copilot-sdk` does not have to be installed separately. Should a CLI distribution ever ship without that
copy, an installed `@github/copilot-sdk` is used as a fallback:

```sh
npm install -g @github/copilot-sdk
```

### Why the Copilot CLI

Access to the Copilot models is granted per OAuth application, not per user or per subscription.
An application that GitHub has not entitled only sees a small legacy subset of the models, regardless of the subscription or of the request headers it sends.
The Copilot CLI is an entitled first-party application, so routing through it makes the current model lineup available without Theia having to obtain an entitlement of its own.

### Authentication

The extension uses GitHub's OAuth Device Flow for authentication:
The sign-in is a device code flow performed by the Copilot CLI, driven from within the application:

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
2. A dialog appears with a device code, click the link to open GitHub's device authorization page
3. Enter the code and authorize
4. The dialog reports success 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.
The credentials are owned by the application, not by the CLI: the sign-in runs against a private, temporary Copilot home so that
the token is not written into the credential store of the machine, and it is then kept in the credential store of Theia.
The CLI is given that token for its requests and nothing else, so a token in the environment or an existing sign-in of the
GitHub CLI is never used, and **Copilot: Sign Out** removes the credentials of this application without touching either.

### Configuration

Available models can be configured via the `ai-features.copilot.models` preference:
Available models can be configured via the `ai-features.copilot.modelOverrides` preference.
When it is empty, the models are discovered from your Copilot subscription:

```json
{
"ai-features.copilot.models": [
"gpt-4o",
"claude-sonnet-4"
"ai-features.copilot.modelOverrides": [
"gpt-5.5",
"claude-sonnet-5"
]
}
```

### GitHub Enterprise
### Copilot Business and Enterprise

Copilot Business and Enterprise seats are served by their own API host, but nothing has to be configured for them:
the endpoint belonging to the subscription is resolved from the credentials of the sign-in.

Should that resolution ever fail for a deployment, the host can be forced with the `COPILOT_API_URL` environment variable
of the process running the backend, for example `https://api.business.githubcopilot.com`. This is a last resort rather than
part of the normal setup: a value that does not match the subscription makes every request fail, and GitHub reports that
the same way as a missing entitlement, which is hard to tell apart.

For GitHub Enterprise users, configure the enterprise URL via the `ai-features.copilot.enterpriseUrl` preference:
For GitHub Enterprise deployments, configure the domain via the `ai-features.copilot.enterpriseUrl` preference.
It is used for the sign-in, and remembered with the credentials so that requests go to the same deployment:

```json
{
Expand All @@ -53,8 +112,21 @@ For GitHub Enterprise users, configure the enterprise URL via the `ai-features.c

### Commands

- **Copilot: Sign In** - Initiates the OAuth device flow authentication
- **Copilot: Sign Out** - Signs out and clears stored credentials
- **Copilot: Sign In** - Signs in via the device code flow
- **Copilot: Sign Out** - Removes the stored credentials

### Known limitations

- The Copilot CLI has to be installed on the machine hosting the backend, see above.
- The Copilot CLI runs as a process on the machine hosting the backend, one per frontend connection.
This is not suitable for multi-user backend deployments, where every connected frontend would share a single identity.
- Structured output is not available on this path.
- The request mapping is lossy, because the CLI is an agent that takes a single prompt per turn rather than a message history:
- The conversation is flattened into one prompt. A single user turn is forwarded as it is, a longer history is rendered as a
role-labelled transcript. The system prompt of the Theia agent is not part of that, it becomes the system message of the
session and takes the place of the agent instructions the CLI would use.
- Images in a request are dropped and only noted as omitted.
- Tool calls and tool results of the history are rendered as text rather than as the structured entries they were.

## Additional Information

Expand Down
2 changes: 0 additions & 2 deletions packages/ai-copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
"description": "Theia - GitHub Copilot Integration",
"dependencies": {
"@theia/ai-core": "1.75.0",
"@theia/ai-openai": "1.75.0",
"@theia/core": "1.75.0",
"openai": "^6.49.0",
"tslib": "^2.8.1"
},
"publishConfig": {
Expand Down
20 changes: 12 additions & 8 deletions packages/ai-copilot/src/browser/copilot-auth-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ export class CopilotAuthDialog extends ReactDialog<boolean> {

override async open(): Promise<boolean | undefined> {
this.initiateFlow();
return super.open();
const result = await super.open();
if (this.state !== 'success') {
// Leave no Copilot CLI process behind when the dialog is dismissed or retried.
await this.authService.cancelSignIn();
}
return result;
}

override update(): void {
Expand All @@ -102,7 +107,7 @@ export class CopilotAuthDialog extends ReactDialog<boolean> {
this.state = 'loading';
this.update();

this.deviceCodeResponse = await this.authService.initiateDeviceFlow(this.props.enterpriseUrl);
this.deviceCodeResponse = await this.authService.startSignIn(this.props.enterpriseUrl);
this.state = 'waiting';
this.update();
} catch (error) {
Expand All @@ -121,11 +126,7 @@ export class CopilotAuthDialog extends ReactDialog<boolean> {
this.update();

try {
const success = await this.authService.pollForToken(
this.deviceCodeResponse.device_code,
this.deviceCodeResponse.interval,
this.props.enterpriseUrl
);
const success = await this.authService.waitForSignIn();

if (success) {
this.state = 'success';
Expand All @@ -149,7 +150,10 @@ export class CopilotAuthDialog extends ReactDialog<boolean> {

protected override isValid(_value: boolean, _mode: DialogError): DialogError {
if (this.state === 'error') {
return this.errorMessage ?? 'An error occurred';
// The error is shown in the dialog body by renderError, where it can wrap. Returning only a
// failed result keeps the accept button disabled without repeating the (often long) message
// next to it, where it does not fit.
return { result: false, message: '' };
}
return '';
}
Expand Down
Loading
Loading