Copilot Chat API guard + reason fields: detect missing proposed API, add reason in /healthz and 503 errors, verbose diagnostics, docs; manifest opts into chat proposals

Co-Authored-By: Lars Baunwall <larslb@thinkability.dk>
This commit is contained in:
Devin AI 2025-08-12 18:34:19 +00:00
parent e61d191ebf
commit 71495c6812
3 changed files with 52 additions and 4 deletions

View file

@ -49,6 +49,27 @@ Optional: Packaging a VSIX
npm i -g @vscode/vsce
vsce package
- Then install the generated .vsix via “Extensions: Install from VSIX…”
## Enabling the VS Code Chat proposed API
The `vscode.chat.requestChatAccess` API is currently proposed. To use this extension at runtime, enable proposed APIs for this extension:
- Stable VS Code:
- Start with: `code --enable-proposed-api thinkability.copilot-bridge`
- VS Code Insiders:
- Proposed APIs can be used when running the extension from source (F5) or with the flag above
- Run from source:
- Open this folder in VS Code and press F5 (Extension Development Host)
When the proposed API is not enabled, the Output (“Copilot Bridge”) will show:
“VS Code Chat proposed API not enabled; start VS Code with: code --enable-proposed-api thinkability.copilot-bridge, or run via F5/Insiders.”
## Troubleshooting
- /healthz shows `copilot: "unavailable"` with a `reason`:
- `missing_chat_api`: VS Code Chat proposed API not enabled (use the flag above)
- `copilot_unavailable`: Copilot access not granted (sign in to GitHub Copilot)
- POST /v1/chat/completions returns 503 with `reason` giving the same codes as above.
## Configuration (bridge.*)
@ -68,6 +89,7 @@ To see verbose logs:
- Access acquisition attempts (“Copilot access missing; attempting to acquire…”, “Copilot access acquired.”)
- SSE lifecycle (“SSE start …”, “SSE end …”)
- Health checks (best-effort access check when verbose is on)
- Proposed API diagnostics (e.g., “VS Code Chat proposed API not enabled…”)
- bridge.verbose (boolean; default false): verbose logs to “Copilot Bridge” output channel
## Manual Testing (curl)

View file

@ -7,6 +7,13 @@
"engines": {
"vscode": "^1.90.0"
},
"enabledApiProposals": [
"chat",
"chatProvider"
],
"extensionKind": [
"ui"
],
"categories": [
"Other"
],

View file

@ -8,6 +8,7 @@ let statusItem: vscode.StatusBarItem | undefined;
let output: vscode.OutputChannel | undefined;
let running = false;
let activeRequests = 0;
let lastReason: string | undefined;
export async function activate(ctx: vscode.ExtensionContext) {
output = vscode.window.createOutputChannel('Copilot Bridge');
@ -77,11 +78,13 @@ async function startBridge() {
if (req.method === 'GET' && req.url === '/healthz') {
const cfgNow = vscode.workspace.getConfiguration('bridge');
const verboseNow = cfgNow.get<boolean>('verbose') ?? false;
const hasProposal = !!((vscode as any).chat && typeof (vscode as any).chat.requestChatAccess === 'function');
if (!access && verboseNow) {
if (verboseNow) output?.appendLine(`Healthz: access=${access ? 'present' : 'missing'}`);
if (verboseNow) output?.appendLine(`Healthz: access=${access ? 'present' : 'missing'} proposal=${hasProposal ? 'ok' : 'missing'}`);
await getAccess();
}
writeJson(res, 200, { ok: true, copilot: access ? 'ok' : 'unavailable', version: vscode.version });
const unavailableReason = access ? undefined : (!hasProposal ? 'missing_chat_api' : (lastReason || 'copilot_unavailable'));
writeJson(res, 200, { ok: true, copilot: access ? 'ok' : 'unavailable', reason: unavailableReason, version: vscode.version });
return;
}
@ -100,7 +103,9 @@ async function startBridge() {
await getAccess();
}
if (!access) {
writeJson(res, 503, { error: { message: 'Copilot unavailable', type: 'server_error', code: 'copilot_unavailable' } });
const hasProposal = !!((vscode as any).chat && typeof (vscode as any).chat.requestChatAccess === 'function');
const reason = !hasProposal ? 'missing_chat_api' : (lastReason || 'copilot_unavailable');
writeJson(res, 503, { error: { message: 'Copilot unavailable', type: 'server_error', code: 'copilot_unavailable', reason } });
return;
}
@ -236,9 +241,22 @@ async function getAccess(force = false): Promise<vscode.ChatAccess | undefined>
if (!force && access) return access;
const cfg = vscode.workspace.getConfiguration('bridge');
const verbose = cfg.get<boolean>('verbose') ?? false;
const hasProposal = !!((vscode as any).chat && typeof (vscode as any).chat.requestChatAccess === 'function');
if (!hasProposal) {
access = undefined;
lastReason = 'missing_chat_api';
const info = server ? server.address() : undefined;
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
statusItem && (statusItem.text = `Copilot Bridge: Unavailable ${bound ? `@ ${bound}` : ''}`);
if (verbose) output?.appendLine('VS Code Chat proposed API not enabled; start VS Code with: code --enable-proposed-api thinkability.copilot-bridge, or run via F5/Insiders.');
return undefined;
}
try {
const newAccess = await vscode.chat.requestChatAccess('copilot');
const newAccess = await (vscode as any).chat.requestChatAccess('copilot');
access = newAccess;
lastReason = undefined;
const info = server ? server.address() : undefined;
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
statusItem && (statusItem.text = `Copilot Bridge: OK ${bound ? `@ ${bound}` : ''}`);
@ -246,6 +264,7 @@ async function getAccess(force = false): Promise<vscode.ChatAccess | undefined>
return access;
} catch (e: any) {
access = undefined;
lastReason = 'copilot_unavailable';
const info = server ? server.address() : undefined;
const bound = info && typeof info === 'object' ? `${info.address}:${info.port}` : '';
statusItem && (statusItem.text = `Copilot Bridge: Unavailable ${bound ? `@ ${bound}` : ''}`);