mirror of
https://github.com/larsbaunwall/vscode-copilot-bridge.git
synced 2025-10-05 22:22:59 +00:00
Mandatory authorization: Always use a bearer token
This commit is contained in:
parent
11e0b9cb37
commit
778e93cfc1
7 changed files with 147 additions and 31 deletions
43
README.md
43
README.md
|
|
@ -18,7 +18,7 @@ Copilot Bridge lets you access your personal Copilot session locally through an
|
||||||
- SSE streaming for incremental responses
|
- SSE streaming for incremental responses
|
||||||
- Real-time model discovery via VS Code Language Model API
|
- Real-time model discovery via VS Code Language Model API
|
||||||
- Concurrency and rate limits to keep VS Code responsive
|
- Concurrency and rate limits to keep VS Code responsive
|
||||||
- Optional bearer token authentication
|
- Mandatory bearer token authentication with `HTTP 401 Unauthorized` protection
|
||||||
- Lightweight Polka-based server integrated directly with the VS Code runtime
|
- Lightweight Polka-based server integrated directly with the VS Code runtime
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -73,35 +73,51 @@ The author collects no data and has no access to user prompts or completions.
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Install from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=thinkability.copilot-bridge) or load the `.vsix`.
|
1. Install from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=thinkability.copilot-bridge) or load the `.vsix`.
|
||||||
2. Launch VS Code and open the **Command Palette** → “Copilot Bridge: Enable”.
|
2. Set **Copilot Bridge › Token** to a secret value (Settings UI or JSON). Requests without this token receive `401 Unauthorized`.
|
||||||
3. Check status anytime with “Copilot Bridge: Status”.
|
3. Open the **Command Palette** → “Copilot Bridge: Enable” to start the bridge.
|
||||||
4. Keep VS Code open — the bridge runs only while the editor is active.
|
4. Check status anytime with “Copilot Bridge: Status” or by hovering the status bar item (it links directly to the token setting when missing).
|
||||||
|
5. Keep VS Code open — the bridge runs only while the editor is active.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📡 Using the Bridge
|
## 📡 Using the Bridge
|
||||||
|
|
||||||
Replace `PORT` with the one shown in “Copilot Bridge: Status”.
|
Replace `PORT` with the one shown in “Copilot Bridge: Status”. Use the same token value you configured in VS Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PORT=12345 # Replace with the port from the status command
|
||||||
|
export BRIDGE_TOKEN="<your-copilot-bridge-token>"
|
||||||
|
```
|
||||||
|
|
||||||
List models:
|
List models:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:$PORT/v1/models
|
curl -H "Authorization: Bearer $BRIDGE_TOKEN" \
|
||||||
|
http://127.0.0.1:$PORT/v1/models
|
||||||
```
|
```
|
||||||
|
|
||||||
Stream a completion:
|
Stream a completion:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -N -H "Content-Type: application/json" \
|
curl -N \
|
||||||
|
-H "Authorization: Bearer $BRIDGE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
-d '{"model":"gpt-4o-copilot","messages":[{"role":"user","content":"hello"}]}' \
|
-d '{"model":"gpt-4o-copilot","messages":[{"role":"user","content":"hello"}]}' \
|
||||||
http://127.0.0.1:$PORT/v1/chat/completions
|
http://127.0.0.1:$PORT/v1/chat/completions
|
||||||
```
|
```
|
||||||
|
|
||||||
Use with OpenAI SDK:
|
Use with OpenAI SDK:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
if (!process.env.BRIDGE_TOKEN) {
|
||||||
|
throw new Error("Set BRIDGE_TOKEN to the same token configured in VS Code settings (bridge.token).");
|
||||||
|
}
|
||||||
|
|
||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
baseURL: `http://127.0.0.1:${process.env.PORT}/v1`,
|
baseURL: `http://127.0.0.1:${process.env.PORT}/v1`,
|
||||||
apiKey: process.env.BRIDGE_TOKEN || "unused",
|
apiKey: process.env.BRIDGE_TOKEN,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rsp = await client.chat.completions.create({
|
const rsp = await client.chat.completions.create({
|
||||||
|
|
@ -128,13 +144,15 @@ Responses stream back via SSE with concurrency controls for editor stability.
|
||||||
|----------|----------|-------------|
|
|----------|----------|-------------|
|
||||||
| `bridge.enabled` | false | Start automatically with VS Code |
|
| `bridge.enabled` | false | Start automatically with VS Code |
|
||||||
| `bridge.port` | 0 | Ephemeral port |
|
| `bridge.port` | 0 | Ephemeral port |
|
||||||
| `bridge.token` | "" | Optional bearer token |
|
| `bridge.token` | "" | Bearer token required for every request (leave empty to block API access) |
|
||||||
| `bridge.historyWindow` | 3 | Retained conversation turns |
|
| `bridge.historyWindow` | 3 | Retained conversation turns |
|
||||||
| `bridge.maxConcurrent` | 1 | Max concurrent requests |
|
| `bridge.maxConcurrent` | 1 | Max concurrent requests |
|
||||||
| `bridge.verbose` | false | Enable verbose logging |
|
| `bridge.verbose` | false | Enable verbose logging |
|
||||||
|
|
||||||
> ℹ️ The bridge always binds to `127.0.0.1` and cannot be exposed to other interfaces.
|
> ℹ️ The bridge always binds to `127.0.0.1` and cannot be exposed to other interfaces.
|
||||||
|
|
||||||
|
> 💡 Hover the status bar item to confirm the token status; missing tokens show a warning link that opens the relevant setting.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🪶 Logging & Diagnostics
|
## 🪶 Logging & Diagnostics
|
||||||
|
|
@ -151,14 +169,15 @@ Responses stream back via SSE with concurrency controls for editor stability.
|
||||||
> Never expose the endpoint to external networks.
|
> Never expose the endpoint to external networks.
|
||||||
|
|
||||||
- Loopback-only binding (non-configurable)
|
- Loopback-only binding (non-configurable)
|
||||||
- Optional bearer token enforcement
|
- Mandatory bearer token gating (requests rejected without the correct header)
|
||||||
- No persistent storage or telemetry
|
- No persistent storage or telemetry
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧾 Changelog
|
## 🧾 Changelog
|
||||||
|
|
||||||
- **v1.2.0** – Locked the HTTP server to localhost for improved safety
|
- **v1.2.0** – Authentication token now mandatory; status bar hover warns when missing
|
||||||
|
- **v1.1.1** – Locked the HTTP server to localhost for improved safety
|
||||||
- **v1.1.0** – Performance improvements (~30%)
|
- **v1.1.0** – Performance improvements (~30%)
|
||||||
- **v1.0.0** – Modular core, OpenAI typings, tool-calling support
|
- **v1.0.0** – Modular core, OpenAI typings, tool-calling support
|
||||||
- **v0.2.2** – Polka integration, improved model family selection
|
- **v0.2.2** – Polka integration, improved model family selection
|
||||||
|
|
@ -179,4 +198,4 @@ Apache 2.0 © 2025 [Lars Baunwall]
|
||||||
Independent project — not affiliated with GitHub or Microsoft.
|
Independent project — not affiliated with GitHub or Microsoft.
|
||||||
For compliance or takedown inquiries, please open a GitHub issue.
|
For compliance or takedown inquiries, please open a GitHub issue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -3,7 +3,7 @@
|
||||||
"icon": "images/icon.png",
|
"icon": "images/icon.png",
|
||||||
"name": "copilot-bridge",
|
"name": "copilot-bridge",
|
||||||
"displayName": "Copilot Bridge",
|
"displayName": "Copilot Bridge",
|
||||||
"description": "Local OpenAI-compatible chat endpoint (inference) bridging to GitHub Copilot via the VS Code Language Model API.",
|
"description": "Local OpenAI-compatible interface built on the public VS Code Language Model API (vscode.lm).",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"publisher": "thinkability",
|
"publisher": "thinkability",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -18,6 +18,12 @@
|
||||||
"extensionKind": [
|
"extensionKind": [
|
||||||
"ui"
|
"ui"
|
||||||
],
|
],
|
||||||
|
"capabilities": {
|
||||||
|
"untrustedWorkspaces": {
|
||||||
|
"supported": false
|
||||||
|
},
|
||||||
|
"virtualWorkspaces": false
|
||||||
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"AI"
|
"AI"
|
||||||
],
|
],
|
||||||
|
|
@ -46,7 +52,7 @@
|
||||||
"bridge.enabled": {
|
"bridge.enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "Start the Copilot Bridge automatically when VS Code starts."
|
"description": "Start the Copilot Bridge automatically when VS Code starts. Uses only the public `vscode.lm` Language Model API."
|
||||||
},
|
},
|
||||||
"bridge.port": {
|
"bridge.port": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
|
@ -56,18 +62,18 @@
|
||||||
"bridge.token": {
|
"bridge.token": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"default": "",
|
||||||
"description": "Optional bearer token required in Authorization header. Leave empty to disable."
|
"description": "Bearer token required in every Authorization header. Leave empty to block access."
|
||||||
},
|
},
|
||||||
"bridge.historyWindow": {
|
"bridge.historyWindow": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 3,
|
"default": 3,
|
||||||
"minimum": 0,
|
|
||||||
"description": "Number of user/assistant turns to include (system message is kept separately)."
|
"description": "Number of user/assistant turns to include (system message is kept separately)."
|
||||||
},
|
},
|
||||||
"bridge.maxConcurrent": {
|
"bridge.maxConcurrent": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
|
"maximum": 4,
|
||||||
"description": "Maximum concurrent /v1/chat/completions requests. Excess requests return 429."
|
"description": "Maximum concurrent /v1/chat/completions requests. Excess requests return 429."
|
||||||
},
|
},
|
||||||
"bridge.verbose": {
|
"bridge.verbose": {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { AddressInfo } from 'net';
|
||||||
import { getBridgeConfig } from './config';
|
import { getBridgeConfig } from './config';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { ensureOutput, verbose } from './log';
|
import { ensureOutput, verbose } from './log';
|
||||||
import { ensureStatusBar } from './status';
|
import { ensureStatusBar, updateStatus } from './status';
|
||||||
import { startServer, stopServer } from './http/server';
|
import { startServer, stopServer } from './http/server';
|
||||||
import { getModel } from './models';
|
import { getModel } from './models';
|
||||||
|
|
||||||
|
|
@ -31,10 +31,27 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
|
||||||
const config = getBridgeConfig();
|
const config = getBridgeConfig();
|
||||||
const hasToken = config.token.length > 0;
|
const hasToken = config.token.length > 0;
|
||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
`Copilot Bridge: ${state.running ? 'Enabled' : 'Disabled'} | Bound: ${bound} | Token: ${hasToken ? 'Set' : 'None'}`
|
`Copilot Bridge: ${state.running ? 'Enabled' : 'Disabled'} | Bound: ${bound} | Token: ${hasToken ? 'Set (required)' : 'Missing (requests will 401)'}`
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration((event) => {
|
||||||
|
if (!event.affectsConfiguration('bridge.token')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.statusBarItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const kind: 'start' | 'error' | 'success' | 'disabled' = !state.running
|
||||||
|
? 'disabled'
|
||||||
|
: state.modelCache
|
||||||
|
? 'success'
|
||||||
|
: state.modelAttempted
|
||||||
|
? 'error'
|
||||||
|
: 'start';
|
||||||
|
updateStatus(kind, { suppressLog: true });
|
||||||
|
}));
|
||||||
|
|
||||||
const config = getBridgeConfig();
|
const config = getBridgeConfig();
|
||||||
if (config.enabled) {
|
if (config.enabled) {
|
||||||
await startBridge();
|
await startBridge();
|
||||||
|
|
@ -52,7 +69,8 @@ async function startBridge(): Promise<void> {
|
||||||
await startServer();
|
await startServer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.running = false;
|
state.running = false;
|
||||||
state.statusBarItem!.text = 'Copilot Bridge: Error';
|
state.lastReason = 'startup_failed';
|
||||||
|
updateStatus('error', { suppressLog: true });
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
verbose(error.stack || error.message);
|
verbose(error.stack || error.message);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -70,9 +88,7 @@ async function stopBridge(): Promise<void> {
|
||||||
} finally {
|
} finally {
|
||||||
state.server = undefined;
|
state.server = undefined;
|
||||||
state.modelCache = undefined;
|
state.modelCache = undefined;
|
||||||
if (state.statusBarItem) {
|
updateStatus('disabled');
|
||||||
state.statusBarItem.text = 'Copilot Bridge: Disabled';
|
|
||||||
}
|
|
||||||
verbose('Stopped');
|
verbose('Stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ let cachedAuthHeader = '';
|
||||||
* Caches the full "Bearer <token>" header to optimize hot path.
|
* Caches the full "Bearer <token>" header to optimize hot path.
|
||||||
*/
|
*/
|
||||||
export const isAuthorized = (req: IncomingMessage, token: string): boolean => {
|
export const isAuthorized = (req: IncomingMessage, token: string): boolean => {
|
||||||
if (!token) return true;
|
if (!token) {
|
||||||
|
cachedToken = '';
|
||||||
|
cachedAuthHeader = '';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Update cache if token changed
|
// Update cache if token changed
|
||||||
if (token !== cachedToken) {
|
if (token !== cachedToken) {
|
||||||
cachedToken = token;
|
cachedToken = token;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { isAuthorized } from './auth';
|
||||||
import { handleHealthCheck } from './routes/health';
|
import { handleHealthCheck } from './routes/health';
|
||||||
import { handleModelsRequest } from './routes/models';
|
import { handleModelsRequest } from './routes/models';
|
||||||
import { handleChatCompletion } from './routes/chat';
|
import { handleChatCompletion } from './routes/chat';
|
||||||
import { writeErrorResponse, writeNotFound, writeRateLimit, writeUnauthorized } from './utils';
|
import { writeErrorResponse, writeNotFound, writeRateLimit, writeTokenRequired, writeUnauthorized } from './utils';
|
||||||
import { ensureOutput, verbose } from '../log';
|
import { ensureOutput, verbose } from '../log';
|
||||||
import { updateStatus } from '../status';
|
import { updateStatus } from '../status';
|
||||||
|
|
||||||
|
|
@ -36,7 +36,15 @@ export const startServer = async (): Promise<void> => {
|
||||||
if (path === '/health') {
|
if (path === '/health') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
if (!isAuthorized(req, config.token)) {
|
const token = getBridgeConfig().token;
|
||||||
|
if (!token) {
|
||||||
|
if (config.verbose) {
|
||||||
|
verbose('401 unauthorized: missing auth token');
|
||||||
|
}
|
||||||
|
writeTokenRequired(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isAuthorized(req, token)) {
|
||||||
writeUnauthorized(res);
|
writeUnauthorized(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ const UNAUTHORIZED_ERROR = JSON.stringify({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const TOKEN_REQUIRED_ERROR = JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: 'auth token required',
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
code: 'auth_token_required',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const NOT_FOUND_ERROR = JSON.stringify({
|
const NOT_FOUND_ERROR = JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: 'not found',
|
message: 'not found',
|
||||||
|
|
@ -49,6 +57,11 @@ export const writeUnauthorized = (res: ServerResponse): void => {
|
||||||
res.end(UNAUTHORIZED_ERROR);
|
res.end(UNAUTHORIZED_ERROR);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const writeTokenRequired = (res: ServerResponse): void => {
|
||||||
|
res.writeHead(401, JSON_HEADERS);
|
||||||
|
res.end(TOKEN_REQUIRED_ERROR);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fast-path not found response (pre-serialized).
|
* Fast-path not found response (pre-serialized).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,92 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { AddressInfo } from 'net';
|
import { AddressInfo } from 'net';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { getBridgeConfig } from './config';
|
import { LOOPBACK_HOST, getBridgeConfig } from './config';
|
||||||
import { info } from './log';
|
import { info } from './log';
|
||||||
|
|
||||||
|
const formatEndpoint = (addr: AddressInfo | null, port: number): string => {
|
||||||
|
if (addr) {
|
||||||
|
const address = addr.address === '::' ? LOOPBACK_HOST : addr.address;
|
||||||
|
return `${address}:${addr.port}`;
|
||||||
|
}
|
||||||
|
const normalizedPort = port === 0 ? 'auto' : port;
|
||||||
|
return `${LOOPBACK_HOST}:${normalizedPort}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTooltip = (status: string, endpoint: string, tokenConfigured: boolean, reason?: string): vscode.MarkdownString => {
|
||||||
|
const tooltip = new vscode.MarkdownString();
|
||||||
|
tooltip.supportThemeIcons = true;
|
||||||
|
tooltip.isTrusted = true;
|
||||||
|
tooltip.appendMarkdown(`**Copilot Bridge**\n\n`);
|
||||||
|
tooltip.appendMarkdown(`Status: ${status}\n\n`);
|
||||||
|
tooltip.appendMarkdown(`Endpoint: \`http://${endpoint}\`\n\n`);
|
||||||
|
|
||||||
|
if (tokenConfigured) {
|
||||||
|
tooltip.appendMarkdown('Auth token: ✅ configured. Requests must include `Authorization: Bearer <token>`.');
|
||||||
|
} else {
|
||||||
|
tooltip.appendMarkdown('Auth token: ⚠️ not configured — all API requests return **401 Unauthorized** until you set `bridge.token`.');
|
||||||
|
tooltip.appendMarkdown('\n\n[Configure token](command:workbench.action.openSettings?%22bridge.token%22)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
tooltip.appendMarkdown(`\n\nLast reason: \`${reason}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltip;
|
||||||
|
};
|
||||||
|
|
||||||
export const ensureStatusBar = (): void => {
|
export const ensureStatusBar = (): void => {
|
||||||
if (!state.statusBarItem) {
|
if (!state.statusBarItem) {
|
||||||
state.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
state.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
||||||
state.statusBarItem.text = 'Copilot Bridge: Disabled';
|
state.statusBarItem.text = 'Copilot Bridge: Disabled';
|
||||||
|
state.statusBarItem.command = 'bridge.status';
|
||||||
state.statusBarItem.show();
|
state.statusBarItem.show();
|
||||||
|
updateStatus('disabled');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BridgeStatusKind = 'start' | 'error' | 'success';
|
export type BridgeStatusKind = 'start' | 'error' | 'success' | 'disabled';
|
||||||
|
|
||||||
export const updateStatus = (kind: BridgeStatusKind): void => {
|
interface UpdateStatusOptions {
|
||||||
|
readonly suppressLog?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateStatus = (kind: BridgeStatusKind, options: UpdateStatusOptions = {}): void => {
|
||||||
const cfg = getBridgeConfig();
|
const cfg = getBridgeConfig();
|
||||||
const addr = state.server?.address() as AddressInfo | null;
|
const addr = state.server?.address() as AddressInfo | null;
|
||||||
const shown = addr ? `${addr.address}:${addr.port}` : `${cfg.host}:${cfg.port}`;
|
const shown = formatEndpoint(addr, cfg.port);
|
||||||
|
const tokenConfigured = cfg.token.length > 0;
|
||||||
|
|
||||||
if (!state.statusBarItem) return;
|
if (!state.statusBarItem) return;
|
||||||
|
|
||||||
|
let statusLabel: string;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'start': {
|
case 'start': {
|
||||||
const availability = state.modelCache ? 'OK' : (state.modelAttempted ? 'Unavailable' : 'Pending');
|
const availability = state.modelCache ? 'OK' : (state.modelAttempted ? 'Unavailable' : 'Pending');
|
||||||
state.statusBarItem.text = `Copilot Bridge: ${availability} @ ${shown}`;
|
state.statusBarItem.text = `Copilot Bridge: ${availability} @ ${shown}`;
|
||||||
info(`Started at http://${shown} | Copilot: ${state.modelCache ? 'ok' : (state.modelAttempted ? 'unavailable' : 'pending')}`);
|
if (!options.suppressLog) {
|
||||||
|
info(`Started at http://${shown} | Copilot: ${state.modelCache ? 'ok' : (state.modelAttempted ? 'unavailable' : 'pending')}`);
|
||||||
|
}
|
||||||
|
statusLabel = availability;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'error':
|
case 'error':
|
||||||
state.statusBarItem.text = `Copilot Bridge: Unavailable @ ${shown}`;
|
state.statusBarItem.text = `Copilot Bridge: Unavailable @ ${shown}`;
|
||||||
|
statusLabel = 'Unavailable';
|
||||||
break;
|
break;
|
||||||
case 'success':
|
case 'success':
|
||||||
state.statusBarItem.text = `Copilot Bridge: OK @ ${shown}`;
|
state.statusBarItem.text = `Copilot Bridge: OK @ ${shown}`;
|
||||||
|
statusLabel = 'OK';
|
||||||
|
break;
|
||||||
|
case 'disabled':
|
||||||
|
state.statusBarItem.text = 'Copilot Bridge: Disabled';
|
||||||
|
statusLabel = 'Disabled';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Exhaustive check in case of future extension
|
// Exhaustive check in case of future extension
|
||||||
const _never: never = kind;
|
const _never: never = kind;
|
||||||
return _never;
|
return _never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.statusBarItem.tooltip = buildTooltip(statusLabel, shown, tokenConfigured, state.lastReason);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue