feat(vscode): enhance server readiness with multiple URL candidates (#145)
Add support for multiple candidate base URLs in readiness check. Automatically set API URL override upon successful server connection.
This commit is contained in:
committed by
GitHub
parent
be5a12af6e
commit
c52a5f27a2
@@ -11,7 +11,7 @@
|
|||||||
"@heroui/system": "^2.4.23",
|
"@heroui/system": "^2.4.23",
|
||||||
"@heroui/theme": "^2.4.23",
|
"@heroui/theme": "^2.4.23",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@pierre/diffs": "^1.0.5",
|
"@pierre/diffs": "^1.0.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -537,7 +537,7 @@
|
|||||||
|
|
||||||
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
|
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.8", "", {}, "sha512-N/02945tafTFN1FjlGCWO++dErtNyWaYOdARPifuL4nSsxTk2XtqZ2naZ28rj8kekjYR8JX1MA5upNEqwztOZA=="],
|
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||||
|
|
||||||
"@pierre/diffs": ["@pierre/diffs@1.0.5", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-QcFhO6BW1Zz3BP+WFuH1tO2DjFJY5Sb6NmRjmEoVeEu1AVOd3HoUEFTnztxgxn+2c2ZFyFZP+6T4X/g8LDuZLw=="],
|
"@pierre/diffs": ["@pierre/diffs@1.0.5", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-QcFhO6BW1Zz3BP+WFuH1tO2DjFJY5Sb6NmRjmEoVeEu1AVOd3HoUEFTnztxgxn+2c2ZFyFZP+6T4X/g8LDuZLw=="],
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@
|
|||||||
"@heroui/system": "^2.4.23",
|
"@heroui/system": "^2.4.23",
|
||||||
"@heroui/theme": "^2.4.23",
|
"@heroui/theme": "^2.4.23",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@pierre/diffs": "^1.0.5",
|
"@pierre/diffs": "^1.0.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
|||||||
@@ -48,27 +48,65 @@ function resolvePortFromUrl(url: string): number | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForReady(url: string, timeoutMs = 5000): Promise<boolean> {
|
type ReadyResult = { ok: true; baseUrl: string } | { ok: false };
|
||||||
|
|
||||||
|
function normalizeBaseUrl(value: string): string {
|
||||||
|
return value.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCandidateBaseUrls(serverUrl: string): string[] {
|
||||||
|
const normalized = normalizeBaseUrl(serverUrl);
|
||||||
|
try {
|
||||||
|
const parsed = new URL(normalized);
|
||||||
|
const origin = parsed.origin;
|
||||||
|
|
||||||
|
const candidates: string[] = [];
|
||||||
|
const add = (url: string) => {
|
||||||
|
const v = normalizeBaseUrl(url);
|
||||||
|
if (!candidates.includes(v)) candidates.push(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefer SDK-provided url first.
|
||||||
|
add(normalized);
|
||||||
|
// Newer OpenCode servers may mount HTTP API under /api.
|
||||||
|
add(`${origin}/api`);
|
||||||
|
// Fallback to plain origin (in case SDK url includes path not accepted).
|
||||||
|
add(origin);
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
} catch {
|
||||||
|
return [normalized];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForReady(serverUrl: string, timeoutMs = 5000): Promise<ReadyResult> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
const candidates = getCandidateBaseUrls(serverUrl);
|
||||||
|
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
try {
|
for (const baseUrl of candidates) {
|
||||||
const controller = new AbortController();
|
try {
|
||||||
// Increase per-request timeout to 3s to allow for cold start latency without aborting
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||||
const res = await fetch(`${url.replace(/\/+$/, '')}/config`, {
|
|
||||||
method: 'GET',
|
// Keep using /config since the UI proxies to it (via /api -> strip prefix).
|
||||||
headers: { Accept: 'application/json' },
|
const res = await fetch(`${baseUrl}/config`, {
|
||||||
signal: controller.signal
|
method: 'GET',
|
||||||
});
|
headers: { Accept: 'application/json' },
|
||||||
clearTimeout(timeout);
|
signal: controller.signal,
|
||||||
if (res.ok) return true;
|
});
|
||||||
} catch {
|
|
||||||
// ignore
|
clearTimeout(timeout);
|
||||||
|
if (res.ok) return { ok: true, baseUrl };
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Retry faster
|
|
||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise(r => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferApiPrefixFromUrl(url: string): string {
|
function inferApiPrefixFromUrl(url: string): string {
|
||||||
@@ -88,6 +126,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
// Discard unused parameter - reserved for future use (state persistence, subscriptions)
|
// Discard unused parameter - reserved for future use (state persistence, subscriptions)
|
||||||
void _context;
|
void _context;
|
||||||
let server: { url: string; close: () => void } | null = null;
|
let server: { url: string; close: () => void } | null = null;
|
||||||
|
let managedApiUrlOverride: string | null = null;
|
||||||
let status: ConnectionStatus = 'disconnected';
|
let status: ConnectionStatus = 'disconnected';
|
||||||
let lastError: string | undefined;
|
let lastError: string | undefined;
|
||||||
const listeners = new Set<(status: ConnectionStatus, error?: string) => void>();
|
const listeners = new Set<(status: ConnectionStatus, error?: string) => void>();
|
||||||
@@ -134,6 +173,9 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
if (useConfiguredUrl && configuredApiUrl) {
|
if (useConfiguredUrl && configuredApiUrl) {
|
||||||
return configuredApiUrl.replace(/\/+$/, '');
|
return configuredApiUrl.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
if (managedApiUrlOverride) {
|
||||||
|
return managedApiUrlOverride.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
if (server?.url) {
|
if (server?.url) {
|
||||||
return server.url.replace(/\/+$/, '');
|
return server.url.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
@@ -164,6 +206,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
apiPrefix = '';
|
apiPrefix = '';
|
||||||
apiPrefixDetected = false;
|
apiPrefixDetected = false;
|
||||||
lastExitCode = null;
|
lastExitCode = null;
|
||||||
|
managedApiUrlOverride = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Let the SDK/OS choose a random available port (port: 0)
|
// Let the SDK/OS choose a random available port (port: 0)
|
||||||
@@ -176,13 +219,15 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
|
|
||||||
if (server && server.url) {
|
if (server && server.url) {
|
||||||
// Wait for actual HTTP readiness (stdout "listening" isn't always enough on Windows)
|
// Wait for actual HTTP readiness (stdout "listening" isn't always enough on Windows)
|
||||||
if (await waitForReady(server.url, 10000)) {
|
const ready = await waitForReady(server.url, 10000);
|
||||||
detectedPort = resolvePortFromUrl(server.url);
|
if (ready.ok) {
|
||||||
apiPrefix = inferApiPrefixFromUrl(server.url);
|
managedApiUrlOverride = ready.baseUrl;
|
||||||
|
detectedPort = resolvePortFromUrl(ready.baseUrl);
|
||||||
|
apiPrefix = inferApiPrefixFromUrl(ready.baseUrl);
|
||||||
apiPrefixDetected = apiPrefix.length > 0;
|
apiPrefixDetected = apiPrefix.length > 0;
|
||||||
setStatus('connected');
|
setStatus('connected');
|
||||||
} else {
|
} else {
|
||||||
// Cleanup zombie process if health check fails
|
// Cleanup zombie process if readiness check fails
|
||||||
try {
|
try {
|
||||||
server.close();
|
server.close();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -225,6 +270,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
server = null;
|
server = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
managedApiUrlOverride = null;
|
||||||
detectedPort = null;
|
detectedPort = null;
|
||||||
setStatus('disconnected');
|
setStatus('disconnected');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@opencode-ai/sdk": "^1.1.8",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
Reference in New Issue
Block a user