diff --git a/src/config.ts b/src/config.ts index b0cf558..a562608 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,13 +12,14 @@ export interface BridgeConfig { export const getBridgeConfig = (): BridgeConfig => { const cfg = vscode.workspace.getConfiguration('bridge'); - return { - enabled: cfg.get('enabled') ?? false, - host: cfg.get('host') ?? '127.0.0.1', - port: cfg.get('port') ?? 0, - token: (cfg.get('token') ?? '').trim(), - historyWindow: cfg.get('historyWindow') ?? 3, - verbose: cfg.get('verbose') ?? false, - maxConcurrent: cfg.get('maxConcurrent') ?? 1, - }; + const resolved = { + enabled: cfg.get('enabled', false), + host: cfg.get('host', '127.0.0.1'), + port: cfg.get('port', 0), + token: cfg.get('token', '').trim(), + historyWindow: cfg.get('historyWindow', 3), + verbose: cfg.get('verbose', false), + maxConcurrent: cfg.get('maxConcurrent', 1), + } satisfies BridgeConfig; + return resolved; }; diff --git a/src/extension.ts b/src/extension.ts index 695e652..11cf00b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import type { AddressInfo } from 'net'; import { getBridgeConfig } from './config'; import { state } from './state'; import { ensureOutput, verbose } from './log'; @@ -9,9 +10,9 @@ import { getModel } from './models'; export async function activate(ctx: vscode.ExtensionContext): Promise { ensureOutput(); ensureStatusBar(); - state.statusItem!.text = 'Copilot Bridge: Disabled'; - state.statusItem!.show(); - ctx.subscriptions.push(state.statusItem!, state.output!); + state.statusBarItem!.text = 'Copilot Bridge: Disabled'; + state.statusBarItem!.show(); + ctx.subscriptions.push(state.statusBarItem!, state.output!); ctx.subscriptions.push(vscode.commands.registerCommand('bridge.enable', async () => { await startBridge(); @@ -24,7 +25,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { ctx.subscriptions.push(vscode.commands.registerCommand('bridge.status', async () => { const info = state.server?.address(); - const bound = info && typeof info === 'object' ? `${(info as any).address}:${(info as any).port}` : 'n/a'; + const bound = (info && typeof info === 'object' && 'address' in info && 'port' in info) + ? `${(info as AddressInfo).address}:${(info as AddressInfo).port}` + : 'n/a'; const config = getBridgeConfig(); const hasToken = config.token.length > 0; vscode.window.showInformationMessage( @@ -47,10 +50,14 @@ async function startBridge(): Promise { state.running = true; try { await startServer(); - } catch (error: any) { + } catch (error) { state.running = false; - state.statusItem!.text = 'Copilot Bridge: Error'; - verbose(error?.stack || String(error)); + state.statusBarItem!.text = 'Copilot Bridge: Error'; + if (error instanceof Error) { + verbose(error.stack || error.message); + } else { + verbose(String(error)); + } throw error; } } @@ -63,8 +70,8 @@ async function stopBridge(): Promise { } finally { state.server = undefined; state.modelCache = undefined; - if (state.statusItem) { - state.statusItem.text = 'Copilot Bridge: Disabled'; + if (state.statusBarItem) { + state.statusBarItem.text = 'Copilot Bridge: Disabled'; } verbose('Stopped'); } diff --git a/src/http/routes/chat.ts b/src/http/routes/chat.ts index 0f494a3..c1dae65 100644 --- a/src/http/routes/chat.ts +++ b/src/http/routes/chat.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import type { IncomingMessage, ServerResponse } from 'http'; import { state } from '../../state'; import { getBridgeConfig } from '../../config'; -import { extractModelFamily, isChatCompletionRequest, normalizeMessagesLM } from '../../messages'; +import { isChatCompletionRequest, normalizeMessagesLM } from '../../messages'; import { getModel, hasLMApi } from '../../models'; import { readJson, writeErrorResponse, writeJson } from '../utils'; import { verbose } from '../../log'; @@ -11,67 +11,66 @@ export const handleChatCompletion = async (req: IncomingMessage, res: ServerResp const config = getBridgeConfig(); state.activeRequests++; verbose(`Request started (active=${state.activeRequests})`); + try { const body = await readJson(req); if (!isChatCompletionRequest(body)) { - writeErrorResponse(res, 400, 'invalid request', 'invalid_request_error', 'invalid_payload'); - return; + return writeErrorResponse(res, 400, 'invalid request', 'invalid_request_error', 'invalid_payload'); } - const { model: requestedModel, stream = true } = body; - const familyOverride = extractModelFamily(requestedModel); - const model = await getModel(false, familyOverride); + + const requestedModel = body.model; + const stream = body.stream !== false; // default true + const model = await getModel(false, requestedModel); + if (!model) { const hasLM = hasLMApi(); - if (familyOverride && hasLM) { + if (requestedModel && hasLM) { state.lastReason = 'not_found'; - writeErrorResponse(res, 404, 'model not found', 'invalid_request_error', 'model_not_found', 'not_found'); - return; + return writeErrorResponse(res, 404, 'model not found', 'invalid_request_error', 'model_not_found', 'not_found'); } const reason = !hasLM ? 'missing_language_model_api' : (state.lastReason || 'copilot_model_unavailable'); - writeErrorResponse(res, 503, 'Copilot unavailable', 'server_error', 'copilot_unavailable', reason); - return; + return writeErrorResponse(res, 503, 'Copilot unavailable', 'server_error', 'copilot_unavailable', reason); } - const lmMessages = normalizeMessagesLM(body.messages, config.historyWindow); - verbose(`Sending request to Copilot via Language Model API... ${model.family || model.modelFamily || model.name || 'unknown'}`); + + const lmMessages = normalizeMessagesLM(body.messages, config.historyWindow) as vscode.LanguageModelChatMessage[]; + verbose(`LM request via API model=${model.family || model.id || model.name || 'unknown'}`); + const cts = new vscode.CancellationTokenSource(); const response = await model.sendRequest(lmMessages, {}, cts.token); - if (stream) { - await handleStreamResponse(res, response); - } else { - await handleNonStreamResponse(res, response); - } + await sendResponse(res, response, stream); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + writeErrorResponse(res, 500, msg || 'internal_error', 'server_error', 'internal_error'); } finally { state.activeRequests--; verbose(`Request complete (active=${state.activeRequests})`); } }; -const handleStreamResponse = async (res: ServerResponse, response: any): Promise => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }); - const id = `cmp_${Math.random().toString(36).slice(2)}`; - verbose(`SSE start id=${id}`); - for await (const fragment of response.text) { - const payload = { - id, - object: 'chat.completion.chunk', - choices: [{ index: 0, delta: { content: fragment } }], - }; - res.write(`data: ${JSON.stringify(payload)}\n\n`); +const sendResponse = async (res: ServerResponse, response: vscode.LanguageModelChatResponse, stream: boolean): Promise => { + if (stream) { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }); + const id = `cmp_${Math.random().toString(36).slice(2)}`; + verbose(`SSE start id=${id}`); + for await (const fragment of response.text) { + res.write(`data: ${JSON.stringify({ + id, + object: 'chat.completion.chunk', + choices: [{ index: 0, delta: { content: fragment } }], + })}\n\n`); + } + verbose(`SSE end id=${id}`); + res.write('data: [DONE]\n\n'); + res.end(); + return; } - verbose(`SSE end id=${id}`); - res.write('data: [DONE]\n\n'); - res.end(); -}; -const handleNonStreamResponse = async (res: ServerResponse, response: any): Promise => { let content = ''; - for await (const fragment of response.text) { - content += fragment; - } + for await (const fragment of response.text) content += fragment; verbose(`Non-stream complete len=${content.length}`); writeJson(res, 200, { id: `cmpl_${Math.random().toString(36).slice(2)}`, diff --git a/src/http/routes/models.ts b/src/http/routes/models.ts index fdd89ff..10ea4e9 100644 --- a/src/http/routes/models.ts +++ b/src/http/routes/models.ts @@ -1,7 +1,8 @@ import { writeJson } from '../utils'; import { listCopilotModels } from '../../models'; +import type { ServerResponse } from 'http'; -export const handleModelsRequest = async (res: any): Promise => { +export const handleModelsRequest = async (res: ServerResponse): Promise => { try { const models = await listCopilotModels(); writeJson(res, 200, { @@ -13,13 +14,7 @@ export const handleModelsRequest = async (res: any): Promise => { }); } catch { writeJson(res, 200, { - data: [ - { - id: 'copilot', - object: 'model', - owned_by: 'vscode-bridge', - }, - ], + data: [], }); } }; diff --git a/src/http/server.ts b/src/http/server.ts index ad8094c..ffda4da 100644 --- a/src/http/server.ts +++ b/src/http/server.ts @@ -1,5 +1,5 @@ -const polka = require('polka'); -import type { Server } from 'http'; +import polka from 'polka'; +import type { Server, IncomingMessage, ServerResponse } from 'http'; import { getBridgeConfig } from '../config'; import { state } from '../state'; import { isAuthorized } from './auth'; @@ -8,16 +8,30 @@ import { handleModelsRequest } from './routes/models'; import { handleChatCompletion } from './routes/chat'; import { writeErrorResponse } from './utils'; import { ensureOutput, verbose } from '../log'; -import { updateStatusAfterStart } from '../status'; +import { updateStatus } from '../status'; export const startServer = async (): Promise => { if (state.server) return; const config = getBridgeConfig(); ensureOutput(); - const app = polka(); + const app = polka({ + onError: (err, req, res) => { + const msg = err instanceof Error ? err.message : String(err); + verbose(`HTTP error: ${msg}`); + if (!res.headersSent) { + writeErrorResponse(res, 500, msg || 'internal_error', 'server_error', 'internal_error'); + } else { + try { res.end(); } catch {/* ignore */} + } + }, + onNoMatch: (_req, res) => { + writeErrorResponse(res, 404, 'not found', 'invalid_request_error', 'route_not_found'); + }, + }); - app.use((req: any, res: any, next: any) => { + // Logging + auth middleware + app.use((req: IncomingMessage & { method?: string; url?: string }, res: ServerResponse, next: () => void) => { verbose(`HTTP ${req.method} ${req.url}`); if (!isAuthorized(req, config.token)) { writeErrorResponse(res, 401, 'unauthorized', 'invalid_request_error', 'unauthorized'); @@ -26,15 +40,15 @@ export const startServer = async (): Promise => { next(); }); - app.get('/healthz', async (_req: any, res: any) => { + app.get('/health', async (_req: IncomingMessage, res: ServerResponse) => { await handleHealthCheck(res, config.verbose); }); - app.get('/v1/models', async (_req: any, res: any) => { + app.get('/v1/models', async (_req: IncomingMessage, res: ServerResponse) => { await handleModelsRequest(res); }); - app.post('/v1/chat/completions', async (req: any, res: any) => { + app.post('/v1/chat/completions', async (req: IncomingMessage, res: ServerResponse) => { if (state.activeRequests >= config.maxConcurrent) { res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '1' }); res.end(JSON.stringify({ @@ -49,36 +63,25 @@ export const startServer = async (): Promise => { } try { await handleChatCompletion(req, res); - } catch (e: any) { + } catch (e) { const msg = e instanceof Error ? e.message : String(e); writeErrorResponse(res, 500, msg || 'internal_error', 'server_error', 'internal_error'); } }); await new Promise((resolve, reject) => { - let resolved = false; try { app.listen(config.port, config.host, () => { - const srv: Server | undefined = app.server; - if (!srv) { - reject(new Error('Server failed to start')); - return; - } + const srv = app.server as Server | undefined; + if (!srv) return reject(new Error('Server failed to start')); state.server = srv; - updateStatusAfterStart(); - resolved = true; + updateStatus('start'); resolve(); }); + const srv = app.server as Server | undefined; + srv?.on('error', reject); } catch (err) { reject(err); - return; - } - const srv: Server | undefined = app.server; - if (srv && typeof (srv as any).on === 'function') { - srv.on('error', reject); - } - if (!resolved && app.server && typeof (app.server as any).on === 'function') { - app.server.on('error', reject); } }); }; diff --git a/src/http/utils.ts b/src/http/utils.ts index ba87ef3..4c81c63 100644 --- a/src/http/utils.ts +++ b/src/http/utils.ts @@ -9,31 +9,44 @@ export interface ErrorResponse { }; } -export const writeJson = (res: ServerResponse, status: number, body: any): void => { +export const writeJson = (res: ServerResponse, status: number, body: T): void => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); }; -export const writeErrorResponse = ( +export function writeErrorResponse( + res: ServerResponse, + status: number, + message: string, + type: string, + code: string +): void; +export function writeErrorResponse( + res: ServerResponse, + status: number, + message: string, + type: string, + code: string, + reason: string +): void; +export function writeErrorResponse( res: ServerResponse, status: number, message: string, type: string, code: string, reason?: string -): void => { - writeJson(res, status, { - error: { message, type, code, ...(reason && { reason }) }, - }); -}; +): void { + writeJson(res, status, { error: { message, type, code, ...(reason ? { reason } : {}) } }); +} -export const readJson = (req: IncomingMessage): Promise => - new Promise((resolve, reject) => { +export const readJson = (req: IncomingMessage): Promise => + new Promise((resolve, reject) => { let data = ''; req.on('data', (c) => (data += c)); req.on('end', () => { try { - resolve(data ? JSON.parse(data) : {}); + resolve((data ? JSON.parse(data) : {}) as T); } catch (e) { reject(e); } diff --git a/src/messages.ts b/src/messages.ts index 733039e..ac9d2c8 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -18,30 +18,31 @@ export interface ChatCompletionRequest { readonly [key: string]: unknown; } -const isValidRole = (role: unknown): role is 'system' | 'user' | 'assistant' => - typeof role === 'string' && ['system', 'user', 'assistant'].includes(role); +const VALID_ROLES = ['system', 'user', 'assistant'] as const; +type Role = typeof VALID_ROLES[number]; +const isValidRole = (role: unknown): role is Role => typeof role === 'string' && VALID_ROLES.includes(role as Role); -export const isChatMessage = (msg: unknown): msg is ChatMessage => - typeof msg === 'object' && - msg !== null && - 'role' in msg && - 'content' in msg && - isValidRole((msg as any).role) && - ((msg as any).content !== undefined && (msg as any).content !== null); +export const isChatMessage = (msg: unknown): msg is ChatMessage => { + if (typeof msg !== 'object' || msg === null) return false; + const candidate = msg as Record; + if (!('role' in candidate) || !('content' in candidate)) return false; + return isValidRole(candidate.role) && candidate.content !== undefined && candidate.content !== null; +}; -export const isChatCompletionRequest = (body: unknown): body is ChatCompletionRequest => - typeof body === 'object' && - body !== null && - 'messages' in body && - Array.isArray((body as any).messages) && - (body as any).messages.length > 0 && - (body as any).messages.every(isChatMessage); +export const isChatCompletionRequest = (body: unknown): body is ChatCompletionRequest => { + if (typeof body !== 'object' || body === null) return false; + const candidate = body as Record; + if (!('messages' in candidate)) return false; + const messages = candidate.messages; + return Array.isArray(messages) && messages.length > 0 && messages.every(isChatMessage); +}; const toText = (content: unknown): string => { if (typeof content === 'string') return content; if (Array.isArray(content)) return content.map(toText).join('\n'); - if (content && typeof content === 'object' && 'text' in content && typeof (content as any).text === 'string') { - return (content as any).text; + if (content && typeof content === 'object' && 'text' in content) { + const textVal = (content as { text?: unknown }).text; + if (typeof textVal === 'string') return textVal; } try { return JSON.stringify(content); @@ -50,52 +51,41 @@ const toText = (content: unknown): string => { } }; -export const normalizeMessagesLM = (messages: ChatMessage[], histWindow: number): unknown[] => { +export const normalizeMessagesLM = ( + messages: readonly ChatMessage[], + histWindow: number +): (vscode.LanguageModelChatMessage | { role: 'user' | 'assistant'; content: string })[] => { const systemMessages = messages.filter((m) => m.role === 'system'); const systemMessage = systemMessages[systemMessages.length - 1]; - const conversationMessages = messages - .filter((m) => m.role === 'user' || m.role === 'assistant') - .slice(-histWindow * 2); + const conversationMessages = messages.filter((m) => m.role === 'user' || m.role === 'assistant').slice(-histWindow * 2); - const User = (vscode as any).LanguageModelChatMessage?.User; - const Assistant = (vscode as any).LanguageModelChatMessage?.Assistant; + const lmMsg = (vscode as unknown as { LanguageModelChatMessage?: typeof vscode.LanguageModelChatMessage }).LanguageModelChatMessage; + const UserFactory = lmMsg?.User; + const AssistantFactory = lmMsg?.Assistant; - const result: unknown[] = []; + const result: (vscode.LanguageModelChatMessage | { role: 'user' | 'assistant'; content: string })[] = []; let firstUserSeen = false; - for (const message of conversationMessages) { - if (message.role === 'user') { - let text = toText(message.content); + for (const m of conversationMessages) { + if (m.role === 'user') { + let text = toText(m.content); if (!firstUserSeen && systemMessage) { text = `[SYSTEM]\n${toText(systemMessage.content)}\n\n[DIALOG]\nuser: ${text}`; firstUserSeen = true; } - result.push(User ? User(text) : { role: 'user', content: text }); - } else if (message.role === 'assistant') { - const text = toText(message.content); - result.push(Assistant ? Assistant(text) : { role: 'assistant', content: text }); + result.push(UserFactory ? UserFactory(text) : { role: 'user', content: text }); + } else { + const text = toText(m.content); + result.push(AssistantFactory ? AssistantFactory(text) : { role: 'assistant', content: text }); } } if (!firstUserSeen && systemMessage) { const text = `[SYSTEM]\n${toText(systemMessage.content)}`; - result.unshift(User ? User(text) : { role: 'user', content: text }); + result.unshift(UserFactory ? UserFactory(text) : { role: 'user', content: text }); } - if (result.length === 0) { - result.push(User ? User('') : { role: 'user', content: '' }); - } + if (result.length === 0) result.push(UserFactory ? UserFactory('') : { role: 'user', content: '' }); return result; }; - -export const extractModelFamily = (requestedModel?: string): string | undefined => { - if (!requestedModel) return undefined; - if (/-copilot$/i.test(requestedModel)) { - return requestedModel.replace(/-copilot$/i, ''); - } - if (requestedModel.toLowerCase() === 'copilot') { - return undefined; - } - return undefined; -}; diff --git a/src/models.ts b/src/models.ts index aaa68a9..40d78d4 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,46 +1,49 @@ import * as vscode from 'vscode'; -import { state, LanguageModel } from './state'; -import { updateStatusWithError, updateStatusWithSuccess } from './status'; +import { state } from './state'; +import { updateStatus } from './status'; import { verbose } from './log'; -const hasLanguageModelAPI = (): boolean => - !!(vscode as any).lm && typeof (vscode as any).lm.selectChatModels === 'function'; +// VS Code Language Model API (see selectChatModels docs in latest VS Code API reference) +const hasLanguageModelAPI = (): boolean => typeof vscode.lm?.selectChatModels === 'function'; -export const selectChatModels = async (family?: string): Promise => { - const lm = (vscode as any).lm; - const selector = family ? { family } : undefined; - const models = await lm.selectChatModels(selector); - return models as unknown as LanguageModel[]; +export const selectChatModels = async (family?: string): Promise => { + const selector: vscode.LanguageModelChatSelector | undefined = family ? { family } : undefined; + return vscode.lm.selectChatModels(selector); }; -export const getModel = async (force = false, family?: string): Promise => { +export const getModel = async (force = false, family?: string): Promise => { if (!force && state.modelCache && !family) return state.modelCache; + // Mark that we've attempted at least one model fetch (affects status bar messaging) + state.modelAttempted = true; + const hasLM = hasLanguageModelAPI(); if (!hasLM) { if (!family) state.modelCache = undefined; state.lastReason = 'missing_language_model_api'; - updateStatusWithError(); + updateStatus('error'); verbose('VS Code Language Model API not available; update VS Code or enable proposed API (Insiders/F5/--enable-proposed-api).'); return undefined; } try { - const models = await selectChatModels(family); - if (!models || models.length === 0) { + // Prefer selecting by vendor 'copilot' if no family specified to reduce unrelated models + const models: vscode.LanguageModelChat[] = family + ? await selectChatModels(family) + : await vscode.lm.selectChatModels({ vendor: 'copilot' }); + if (models.length === 0) { if (!family) state.modelCache = undefined; state.lastReason = family ? 'not_found' : 'copilot_model_unavailable'; - updateStatusWithError(); - const m = family ? `no models for family ${family}` : 'no copilot models available'; - verbose(m); + updateStatus('error'); + verbose(family ? `no models for family ${family}` : 'no copilot models available'); return undefined; } - state.modelCache = models[0]; + state.modelCache = models[0]; // keep first for now; future: choose by quality or family preference state.lastReason = undefined; - updateStatusWithSuccess(); + updateStatus('success'); return state.modelCache; - } catch (e: any) { + } catch (e: unknown) { handleModelSelectionError(e, family); return undefined; } @@ -55,7 +58,7 @@ export const handleModelSelectionError = (error: unknown, family?: string): void } else { state.lastReason = 'copilot_model_unavailable'; } - updateStatusWithError(); + updateStatus('error'); const fam = family ? ` family=${family}` : ''; verbose(`Model selection failed: ${msg}${fam}`); }; @@ -63,9 +66,9 @@ export const handleModelSelectionError = (error: unknown, family?: string): void export const listCopilotModels = async (): Promise => { try { const models = await selectChatModels(); - const ids = models.map((m: any) => { - const normalized = m.family || m.modelFamily || m.name || 'copilot'; - return `${normalized}-copilot`; + const ids = models.map((m: vscode.LanguageModelChat) => { + const normalized = m.family || m.id || m.name || 'copilot'; + return `${normalized}`; }); return ids.length ? ids : ['copilot']; } catch { diff --git a/src/state.ts b/src/state.ts index b356fba..a5be4cc 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,28 +1,19 @@ import * as vscode from 'vscode'; import type { Server } from 'http'; -export interface LanguageModel { - readonly family?: string; - readonly modelFamily?: string; - readonly name?: string; - readonly sendRequest: (messages: unknown[], options: unknown, token: vscode.CancellationToken) => Promise; -} - -export interface LanguageModelResponse { - readonly text: AsyncIterable; -} - export interface BridgeState { server?: Server; - modelCache?: LanguageModel; - statusItem?: vscode.StatusBarItem; + modelCache?: vscode.LanguageModelChat; // official API type + statusBarItem?: vscode.StatusBarItem; output?: vscode.OutputChannel; running: boolean; activeRequests: number; lastReason?: string; + modelAttempted?: boolean; // whether we've attempted to resolve a model yet } export const state: BridgeState = { running: false, activeRequests: 0, + modelAttempted: false, }; diff --git a/src/status.ts b/src/status.ts index 67bb58b..2726b41 100644 --- a/src/status.ts +++ b/src/status.ts @@ -5,37 +5,38 @@ import { getBridgeConfig } from './config'; import { info } from './log'; export const ensureStatusBar = (): void => { - if (!state.statusItem) { - state.statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); - state.statusItem.text = 'Copilot Bridge: Disabled'; - state.statusItem.show(); + if (!state.statusBarItem) { + state.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + state.statusBarItem.text = 'Copilot Bridge: Disabled'; + state.statusBarItem.show(); } }; -export const updateStatusAfterStart = (): void => { - const cfg = getBridgeConfig(); - const addr = state.server?.address() as AddressInfo | null; - const shown = addr ? `${addr.address}:${addr.port}` : `${cfg.host}:${cfg.port}`; - if (state.statusItem) { - state.statusItem.text = `Copilot Bridge: ${state.modelCache ? 'OK' : 'Unavailable'} @ ${shown}`; - } - info(`Started at http://${shown} | Copilot: ${state.modelCache ? 'ok' : 'unavailable'}`); -}; +export type BridgeStatusKind = 'start' | 'error' | 'success'; -export const updateStatusWithError = (): void => { +export const updateStatus = (kind: BridgeStatusKind): void => { const cfg = getBridgeConfig(); const addr = state.server?.address() as AddressInfo | null; const shown = addr ? `${addr.address}:${addr.port}` : `${cfg.host}:${cfg.port}`; - if (state.statusItem) { - state.statusItem.text = `Copilot Bridge: Unavailable @ ${shown}`; - } -}; -export const updateStatusWithSuccess = (): void => { - const cfg = getBridgeConfig(); - const addr = state.server?.address() as AddressInfo | null; - const shown = addr ? `${addr.address}:${addr.port}` : `${cfg.host}:${cfg.port}`; - if (state.statusItem) { - state.statusItem.text = `Copilot Bridge: OK @ ${shown}`; + if (!state.statusBarItem) return; + + switch (kind) { + case 'start': { + const availability = state.modelCache ? 'OK' : (state.modelAttempted ? 'Unavailable' : 'Pending'); + state.statusBarItem.text = `Copilot Bridge: ${availability} @ ${shown}`; + info(`Started at http://${shown} | Copilot: ${state.modelCache ? 'ok' : (state.modelAttempted ? 'unavailable' : 'pending')}`); + break; + } + case 'error': + state.statusBarItem.text = `Copilot Bridge: Unavailable @ ${shown}`; + break; + case 'success': + state.statusBarItem.text = `Copilot Bridge: OK @ ${shown}`; + break; + default: + // Exhaustive check in case of future extension + const _never: never = kind; + return _never; } }; diff --git a/src/types/polka.d.ts b/src/types/polka.d.ts index 5a3bccc..1716b73 100644 --- a/src/types/polka.d.ts +++ b/src/types/polka.d.ts @@ -1,2 +1,30 @@ -declare function polka(...args: any[]): any; -export = polka; +declare module 'polka' { + import { IncomingMessage, ServerResponse } from 'http'; + + export interface PolkaRequest extends IncomingMessage { + params?: Record; + } + + export type Next = () => void; + export type Middleware = (req: PolkaRequest, res: ServerResponse, next: Next) => void; + export type Handler = (req: PolkaRequest, res: ServerResponse) => void; + + export interface PolkaOptions { + onError?: (err: Error, req: PolkaRequest, res: ServerResponse, next: Next) => void; + onNoMatch?: (req: PolkaRequest, res: ServerResponse) => void; + } + + export interface PolkaInstance { + use(mw: Middleware): PolkaInstance; + use(path: string, mw: Middleware): PolkaInstance; + get(path: string, handler: Handler): PolkaInstance; + post(path: string, handler: Handler): PolkaInstance; + put(path: string, handler: Handler): PolkaInstance; + delete(path: string, handler: Handler): PolkaInstance; + listen(port: number, host: string, cb: () => void): void; + server?: import('http').Server; + } + + function polka(options?: PolkaOptions): PolkaInstance; + export default polka; +}