diff --git a/bun.lock b/bun.lock index a7a6cd95..e918628a 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@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-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -105,7 +105,7 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", - "@opencode-ai/sdk": "^1.1.8", + "@opencode-ai/sdk": "^1.1.19", "@pierre/diffs": "^1.0.5", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", @@ -172,7 +172,7 @@ "version": "1.4.8", "dependencies": { "@openchamber/ui": "workspace:*", - "@opencode-ai/sdk": "^1.1.8", + "@opencode-ai/sdk": "^1.1.19", "adm-zip": "^0.5.16", "jsonc-parser": "^3.3.1", "react": "^19.1.1", @@ -200,7 +200,7 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.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-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -537,7 +537,7 @@ "@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=="], diff --git a/package.json b/package.json index 0dcf6e33..b1a4f482 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@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-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/packages/ui/package.json b/packages/ui/package.json index 8053d808..f4742002 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,7 +17,7 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", - "@opencode-ai/sdk": "^1.1.8", + "@opencode-ai/sdk": "^1.1.19", "@pierre/diffs": "^1.0.5", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 1bb9a0ae..13b719e9 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -198,7 +198,7 @@ }, "dependencies": { "@openchamber/ui": "workspace:*", - "@opencode-ai/sdk": "^1.1.8", + "@opencode-ai/sdk": "^1.1.19", "adm-zip": "^0.5.16", "jsonc-parser": "^3.3.1", "react": "^19.1.1", diff --git a/packages/vscode/src/opencode.ts b/packages/vscode/src/opencode.ts index 555dafed..a8537098 100644 --- a/packages/vscode/src/opencode.ts +++ b/packages/vscode/src/opencode.ts @@ -48,27 +48,65 @@ function resolvePortFromUrl(url: string): number | null { } } -async function waitForReady(url: string, timeoutMs = 5000): Promise { +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 { const start = Date.now(); + const candidates = getCandidateBaseUrls(serverUrl); + 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 + for (const baseUrl of candidates) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 1500); + + // Keep using /config since the UI proxies to it (via /api -> strip prefix). + const res = await fetch(`${baseUrl}/config`, { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: controller.signal, + }); + + clearTimeout(timeout); + if (res.ok) return { ok: true, baseUrl }; + } catch { + // ignore + } } - // Retry faster + await new Promise(r => setTimeout(r, 100)); } - return false; + + return { ok: false }; } 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) void _context; let server: { url: string; close: () => void } | null = null; + let managedApiUrlOverride: string | null = null; let status: ConnectionStatus = 'disconnected'; let lastError: string | undefined; const listeners = new Set<(status: ConnectionStatus, error?: string) => void>(); @@ -134,6 +173,9 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo if (useConfiguredUrl && configuredApiUrl) { return configuredApiUrl.replace(/\/+$/, ''); } + if (managedApiUrlOverride) { + return managedApiUrlOverride.replace(/\/+$/, ''); + } if (server?.url) { return server.url.replace(/\/+$/, ''); } @@ -164,7 +206,8 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo apiPrefix = ''; apiPrefixDetected = false; lastExitCode = null; - + managedApiUrlOverride = null; + try { // Let the SDK/OS choose a random available port (port: 0) server = await createOpencodeServer({ @@ -173,16 +216,18 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo timeout: READY_CHECK_TIMEOUT_MS, signal: undefined, }); - + if (server && server.url) { // Wait for actual HTTP readiness (stdout "listening" isn't always enough on Windows) - if (await waitForReady(server.url, 10000)) { - detectedPort = resolvePortFromUrl(server.url); - apiPrefix = inferApiPrefixFromUrl(server.url); + const ready = await waitForReady(server.url, 10000); + if (ready.ok) { + managedApiUrlOverride = ready.baseUrl; + detectedPort = resolvePortFromUrl(ready.baseUrl); + apiPrefix = inferApiPrefixFromUrl(ready.baseUrl); apiPrefixDetected = apiPrefix.length > 0; setStatus('connected'); } else { - // Cleanup zombie process if health check fails + // Cleanup zombie process if readiness check fails try { server.close(); } catch { @@ -224,7 +269,8 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo } server = null; } - + + managedApiUrlOverride = null; detectedPort = null; setStatus('disconnected'); } diff --git a/packages/web/package.json b/packages/web/package.json index 6dedc7f8..b0b25910 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -25,7 +25,7 @@ "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.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-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",