feat: enhance OpenCodeManager with improved server readiness check and CLI availability detection

This commit is contained in:
Bohdan Triapitsyn
2026-01-09 18:03:12 +02:00
parent f1889d73af
commit 4db0d01fd9
+45 -5
View File
@@ -34,6 +34,7 @@ export interface OpenCodeManager {
getStatus(): ConnectionStatus; getStatus(): ConnectionStatus;
getApiUrl(): string | null; getApiUrl(): string | null;
getWorkingDirectory(): string; getWorkingDirectory(): string;
isCliAvailable(): boolean;
getDebugInfo(): OpenCodeDebugInfo; getDebugInfo(): OpenCodeDebugInfo;
onStatusChange(callback: (status: ConnectionStatus, error?: string) => void): vscode.Disposable; onStatusChange(callback: (status: ConnectionStatus, error?: string) => void): vscode.Disposable;
} }
@@ -47,6 +48,29 @@ function resolvePortFromUrl(url: string): number | null {
} }
} }
async function waitForReady(url: string, timeoutMs = 5000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const controller = new AbortController();
// Increase per-request timeout to 3s to allow for cold start latency without aborting
const timeout = setTimeout(() => controller.abort(), 3000);
const res = await fetch(`${url.replace(/\/+$/, '')}/config`, {
method: 'GET',
headers: { Accept: 'application/json' },
signal: controller.signal
});
clearTimeout(timeout);
if (res.ok) return true;
} catch {
// ignore
}
// Retry faster
await new Promise(r => setTimeout(r, 100));
}
return false;
}
function inferApiPrefixFromUrl(url: string): string { function inferApiPrefixFromUrl(url: string): string {
try { try {
const parsed = new URL(url); const parsed = new URL(url);
@@ -77,6 +101,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
let detectedPort: number | null = null; let detectedPort: number | null = null;
let apiPrefix: string = ''; let apiPrefix: string = '';
let apiPrefixDetected = false; let apiPrefixDetected = false;
let cliMissing = false;
const config = vscode.workspace.getConfiguration('openchamber'); const config = vscode.workspace.getConfiguration('openchamber');
const configuredApiUrl = config.get<string>('apiUrl') || ''; const configuredApiUrl = config.get<string>('apiUrl') || '';
@@ -133,6 +158,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
} }
setStatus('connecting'); setStatus('connecting');
cliMissing = false; // Reset assumption on retry
detectedPort = null; detectedPort = null;
apiPrefix = ''; apiPrefix = '';
@@ -149,10 +175,22 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
}); });
if (server && server.url) { if (server && server.url) {
detectedPort = resolvePortFromUrl(server.url); // Wait for actual HTTP readiness (stdout "listening" isn't always enough on Windows)
apiPrefix = inferApiPrefixFromUrl(server.url); if (await waitForReady(server.url, 10000)) {
apiPrefixDetected = apiPrefix.length > 0; detectedPort = resolvePortFromUrl(server.url);
setStatus('connected'); apiPrefix = inferApiPrefixFromUrl(server.url);
apiPrefixDetected = apiPrefix.length > 0;
setStatus('connected');
} else {
// Cleanup zombie process if health check fails
try {
server.close();
} catch {
// ignore
}
server = null;
throw new Error('Server started but health check failed');
}
} else { } else {
throw new Error('Server started but URL is missing'); throw new Error('Server started but URL is missing');
} }
@@ -161,6 +199,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
// Check for ENOENT or generic spawn failure which implies CLI missing // Check for ENOENT or generic spawn failure which implies CLI missing
if (message.includes('ENOENT') || message.includes('spawn opencode')) { if (message.includes('ENOENT') || message.includes('spawn opencode')) {
cliMissing = true;
setStatus('error', 'OpenCode CLI not found. Install it or ensure it\'s in PATH.'); setStatus('error', 'OpenCode CLI not found. Install it or ensure it\'s in PATH.');
vscode.window.showErrorMessage( vscode.window.showErrorMessage(
'OpenCode CLI not found. Please install it or ensure it\'s in PATH.', 'OpenCode CLI not found. Please install it or ensure it\'s in PATH.',
@@ -220,12 +259,13 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
getStatus: () => status, getStatus: () => status,
getApiUrl, getApiUrl,
getWorkingDirectory: () => workingDirectory, getWorkingDirectory: () => workingDirectory,
isCliAvailable: () => !cliMissing,
getDebugInfo: () => ({ getDebugInfo: () => ({
mode: useConfiguredUrl && configuredApiUrl ? 'external' : 'managed', mode: useConfiguredUrl && configuredApiUrl ? 'external' : 'managed',
status, status,
lastError, lastError,
workingDirectory, workingDirectory,
cliAvailable: status !== 'error' || (lastError ? !lastError.includes('CLI not found') : true), // Infer availability from status cliAvailable: !cliMissing,
cliPath: null, cliPath: null,
configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null, configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null,
configuredPort, configuredPort,