diff --git a/packages/ui/src/components/sections/openchamber/AboutSettings.tsx b/packages/ui/src/components/sections/openchamber/AboutSettings.tsx index 8477dffe..0d3357ca 100644 --- a/packages/ui/src/components/sections/openchamber/AboutSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/AboutSettings.tsx @@ -138,6 +138,7 @@ export const AboutSettings: React.FC = () => { ); } + // Desktop layout (unchanged) return (
@@ -200,6 +201,7 @@ export const AboutSettings: React.FC = () => {
+ {/* Links */} {/* Links */}
= ({
)} + ); }; diff --git a/packages/ui/src/components/ui/AboutDialog.tsx b/packages/ui/src/components/ui/AboutDialog.tsx index e491bff7..5cf17a2a 100644 --- a/packages/ui/src/components/ui/AboutDialog.tsx +++ b/packages/ui/src/components/ui/AboutDialog.tsx @@ -5,6 +5,9 @@ import { } from '@/components/ui/dialog'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; import { RiDiscordFill, RiGithubFill, RiTwitterXFill } from '@remixicon/react'; +import { debugUtils } from '@/lib/debug'; +import { cn } from '@/lib/utils'; +import { toast } from 'sonner'; declare const __APP_VERSION__: string | undefined; @@ -18,6 +21,28 @@ export const AboutDialog: React.FC = ({ onOpenChange, }) => { const [version, setVersion] = React.useState(null); + const [isCopyingDiagnostics, setIsCopyingDiagnostics] = React.useState(false); + const [copiedDiagnostics, setCopiedDiagnostics] = React.useState(false); + + const handleCopyDiagnostics = React.useCallback(async () => { + if (isCopyingDiagnostics) return; + setIsCopyingDiagnostics(true); + setCopiedDiagnostics(false); + try { + const result = await debugUtils.copyDiagnosticsReport(); + if (result.ok) { + setCopiedDiagnostics(true); + toast.success('Diagnostics copied'); + } else { + toast.error('Copy failed'); + } + } catch (error) { + toast.error('Copy failed'); + console.error('Failed to copy diagnostics:', error); + } finally { + setIsCopyingDiagnostics(false); + } + }, [isCopyingDiagnostics]); React.useEffect(() => { if (!open) return; @@ -70,6 +95,23 @@ export const AboutDialog: React.FC = ({ agent

+
+ +

+ Includes OpenChamber state, OpenCode health, directories, and projects. +

+
+
({ + id: project.id, + path: project.path, + label: project.label, + })); + const report = { runtime: { + platform: runtimeApis?.runtime?.platform ?? null, isDesktop: isDesktopRuntime, + isVSCode: Boolean(runtimeApis?.runtime?.isVSCode), hasRuntimeApis: Boolean(runtimeApis), desktopServerOrigin: desktopServer?.origin ?? null, }, @@ -313,6 +323,11 @@ export const debugUtils = { hasPersistedDirectory: directoryState.hasPersistedDirectory, isSwitchingDirectory: directoryState.isSwitchingDirectory, }, + projects: { + total: projectsState.projects.length, + activeProjectId: projectsState.activeProjectId, + samples: projectSamples, + }, sessions: { total: sessions.length, currentSessionId: sessionState.currentSessionId, @@ -341,6 +356,20 @@ export const debugUtils = { return report; }, + async buildDiagnosticsReport() { + const report = await this.getAppStatus(); + return JSON.stringify(report, null, 2); + }, + + async copyDiagnosticsReport() { + const report = await this.buildDiagnosticsReport(); + if (typeof navigator !== 'undefined' && navigator.clipboard) { + await navigator.clipboard.writeText(report); + return { ok: true, report } as const; + } + return { ok: false, report } as const; + }, + checkLastMessage() { const info = this.getLastAssistantMessage(); if (!info) return false; diff --git a/packages/ui/src/lib/directoryPersistence.ts b/packages/ui/src/lib/directoryPersistence.ts index 5c318b86..826208d5 100644 --- a/packages/ui/src/lib/directoryPersistence.ts +++ b/packages/ui/src/lib/directoryPersistence.ts @@ -1,3 +1,4 @@ +import { isVSCodeRuntime } from '@/lib/desktop'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; export const applyPersistedDirectoryPreferences = async (): Promise => { @@ -21,7 +22,7 @@ export const applyPersistedDirectoryPreferences = async (): Promise => { directoryStore.synchronizeHomeDirectory(savedHome); } - if (savedDirectory) { + if (savedDirectory && !isVSCodeRuntime()) { directoryStore.setDirectory(savedDirectory, { showOverlay: false }); } }; diff --git a/packages/ui/src/stores/useDirectoryStore.ts b/packages/ui/src/stores/useDirectoryStore.ts index f9fd8ac6..9bb00773 100644 --- a/packages/ui/src/stores/useDirectoryStore.ts +++ b/packages/ui/src/stores/useDirectoryStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { opencodeClient } from '@/lib/opencode/client'; -import { getDesktopHomeDirectory } from '@/lib/desktop'; +import { getDesktopHomeDirectory, isVSCodeRuntime } from '@/lib/desktop'; import { updateDesktopSettings } from '@/lib/persistence'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { streamDebugEnabled } from '@/stores/utils/streamDebug'; @@ -76,7 +76,7 @@ const getHomeDirectory = () => { if (typeof window !== 'undefined') { const storedHome = safeStorage.getItem('homeDirectory') || cachedHomeDirectory || null; const saved = safeStorage.getItem('lastDirectory'); - if (saved) { + if (saved && !isVSCodeRuntime()) { return resolveDirectoryPath(saved, storedHome); } @@ -95,7 +95,7 @@ const getHomeDirectory = () => { return desktopHome; } - if (storedHome) { + if (storedHome && !isVSCodeRuntime()) { cachedHomeDirectory = storedHome; return storedHome; } @@ -187,7 +187,19 @@ const initializeHomeDirectory = async () => { return fallback; }; -const initialHomeDirectory = getHomeDirectory(); +const getVsCodeWorkspaceFolder = (): string | null => { + if (!isVSCodeRuntime()) { + return null; + } + const workspaceFolder = (window as unknown as { __VSCODE_CONFIG__?: { workspaceFolder?: unknown } }).__VSCODE_CONFIG__?.workspaceFolder; + if (typeof workspaceFolder !== 'string' || workspaceFolder.trim().length === 0) { + return null; + } + const normalized = normalizeDirectoryPath(workspaceFolder); + return normalized.length > 0 ? normalized : null; +}; + +const initialHomeDirectory = getVsCodeWorkspaceFolder() || getHomeDirectory(); if (initialHomeDirectory) { opencodeClient.setDirectory(initialHomeDirectory); } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 1455d4b8..db022360 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -342,12 +342,22 @@ export async function activate(context: vscode.ExtensionContext) { const extensionVersion = String(context.extension?.packageJSON?.version || ''); const workspaceFolders = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri.fsPath); + const primaryWorkspace = workspaceFolders[0] || ''; const debug = openCodeManager?.getDebugInfo(); const resolvedApiUrl = openCodeManager?.getApiUrl(); const workingDirectory = openCodeManager?.getWorkingDirectory() ?? ''; + const workingDirectoryMatchesWorkspace = Boolean(primaryWorkspace && workingDirectory === primaryWorkspace); + let resolvedApiPath = ''; + if (resolvedApiUrl) { + try { + resolvedApiPath = new URL(resolvedApiUrl).pathname || '/'; + } catch { + resolvedApiPath = '(invalid url)'; + } + } - const safeFetch = async (input: string, timeoutMs = 2500) => { + const safeFetch = async (input: string, timeoutMs = 6000) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); const startedAt = Date.now(); @@ -407,12 +417,12 @@ export async function activate(context: vscode.ExtensionContext) { { label: 'config', path: '/config', includeDirectory: true }, { label: 'providers', path: '/config/providers', includeDirectory: true }, // Can be slower on large configs; keep the probe from producing false negatives. - { label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 8000 }, - { label: 'commands', path: '/command', includeDirectory: true }, + { label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 }, + { label: 'commands', path: '/command', includeDirectory: true, timeoutMs: 10000 }, { label: 'project', path: '/project/current', includeDirectory: true }, { label: 'path', path: '/path', includeDirectory: true }, // Session listing is what powers the sidebar. This helps diagnose "no sessions shown" bugs. - { label: 'sessions', path: '/session', includeDirectory: true, timeoutMs: 8000 }, + { label: 'sessions', path: '/session', includeDirectory: true, timeoutMs: 12000 }, { label: 'sessionStatus', path: '/session/status', includeDirectory: true }, ]; @@ -439,9 +449,11 @@ export async function activate(context: vscode.ExtensionContext) { `Platform: ${process.platform} ${process.arch}`, `Workspace folders: ${workspaceFolders.length}${workspaceFolders.length ? ` (${workspaceFolders.join(', ')})` : ''}`, `Status: ${openCodeManager?.getStatus() ?? 'unknown'}`, - `Working directory: ${openCodeManager?.getWorkingDirectory() ?? ''}`, + `Working directory: ${workingDirectory}`, + `Working dir matches workspace: ${workingDirectoryMatchesWorkspace ? 'yes' : 'no'}`, `API URL (configured): ${configuredApiUrl || '(none)'}`, `API URL (resolved): ${openCodeManager?.getApiUrl() ?? '(none)'}`, + `API URL path: ${resolvedApiPath || '(none)'}`, debug ? `OpenCode server URL: ${debug.serverUrl ?? '(none)'}` : `OpenCode server URL: (unknown)`, @@ -460,6 +472,15 @@ export async function activate(context: vscode.ExtensionContext) { debug ? `Last start: ${formatIso(debug.lastStartAt)}` : `Last start: (unknown)`, + debug + ? `Last ready: ${debug.lastReadyElapsedMs !== null ? `${debug.lastReadyElapsedMs}ms` : '(unknown)'}` + : `Last ready: (unknown)`, + debug + ? `Ready attempts: ${debug.lastReadyAttempts ?? '(unknown)'}` + : `Ready attempts: (unknown)`, + debug + ? `Start attempts: ${debug.lastStartAttempts ?? '(unknown)'}` + : `Start attempts: (unknown)`, debug ? `Last connected: ${formatIso(debug.lastConnectedAt)}` : `Last connected: (unknown)`, diff --git a/packages/vscode/src/opencode.ts b/packages/vscode/src/opencode.ts index 2966759f..ff02939d 100644 --- a/packages/vscode/src/opencode.ts +++ b/packages/vscode/src/opencode.ts @@ -25,6 +25,9 @@ export type OpenCodeDebugInfo = { lastConnectedAt: number | null; lastExitCode: number | null; serverUrl: string | null; + lastReadyElapsedMs: number | null; + lastReadyAttempts: number | null; + lastStartAttempts: number | null; }; export interface OpenCodeManager { @@ -49,7 +52,9 @@ function resolvePortFromUrl(url: string): number | null { } } -type ReadyResult = { ok: true; baseUrl: string } | { ok: false }; +type ReadyResult = + | { ok: true; baseUrl: string; elapsedMs: number; attempts: number } + | { ok: false; elapsedMs: number; attempts: number }; function normalizeBaseUrl(value: string): string { return value.replace(/\/+$/, ''); @@ -83,9 +88,11 @@ function getCandidateBaseUrls(serverUrl: string): string[] { async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirectory = ''): Promise { const start = Date.now(); const candidates = getCandidateBaseUrls(serverUrl); + let attempts = 0; while (Date.now() - start < timeoutMs) { for (const baseUrl of candidates) { + attempts += 1; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 1500); @@ -102,7 +109,9 @@ async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirector }); clearTimeout(timeout); - if (res.ok) return { ok: true, baseUrl }; + if (res.ok) { + return { ok: true, baseUrl, elapsedMs: Date.now() - start, attempts }; + } } catch { // ignore } @@ -111,20 +120,11 @@ async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirector await new Promise(r => setTimeout(r, 100)); } - return { ok: false }; + return { ok: false, elapsedMs: Date.now() - start, attempts }; } -function inferApiPrefixFromUrl(url: string): string { - try { - const parsed = new URL(url); - const pathname = parsed.pathname; - if (pathname === '/' || pathname === '') { - return ''; - } - return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; - } catch { - return ''; - } +function inferApiPrefixFromUrl(): string { + return ''; } export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCodeManager { @@ -135,16 +135,19 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo let status: ConnectionStatus = 'disconnected'; let lastError: string | undefined; const listeners = new Set<(status: ConnectionStatus, error?: string) => void>(); - let workingDirectory: string = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir(); + const workspaceDirectory = (): string => + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir(); + let workingDirectory: string = workspaceDirectory(); let startCount = 0; let restartCount = 0; let lastStartAt: number | null = null; let lastConnectedAt: number | null = null; let lastExitCode: number | null = null; + let lastReadyElapsedMs: number | null = null; + let lastReadyAttempts: number | null = null; + let lastStartAttempts: number | null = null; let detectedPort: number | null = null; - let apiPrefix: string = ''; - let apiPrefixDetected = false; let cliMissing = false; let pendingOperation: Promise | null = null; @@ -187,17 +190,21 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo return server.url.replace(/\/+$/, ''); } if (detectedPort) { - return `http://127.0.0.1:${detectedPort}${apiPrefix}`; + return `http://127.0.0.1:${detectedPort}`; } return null; }; async function startInternal(workdir?: string): Promise { startCount += 1; + setStatus('connecting'); lastStartAt = Date.now(); + lastStartAttempts = startCount; if (typeof workdir === 'string' && workdir.trim().length > 0) { workingDirectory = workdir.trim(); + } else { + workingDirectory = workspaceDirectory(); } if (useConfiguredUrl && configuredApiUrl) { @@ -218,8 +225,6 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo cliMissing = false; detectedPort = null; - apiPrefix = ''; - apiPrefixDetected = false; lastExitCode = null; managedApiUrlOverride = null; @@ -247,11 +252,11 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo if (server && server.url) { // Validate readiness for the current workspace context. const ready = await waitForReady(server.url, 10000, workingDirectory); + lastReadyElapsedMs = ready.elapsedMs; + lastReadyAttempts = ready.attempts; if (ready.ok) { managedApiUrlOverride = ready.baseUrl; detectedPort = resolvePortFromUrl(ready.baseUrl); - apiPrefix = inferApiPrefixFromUrl(ready.baseUrl); - apiPrefixDetected = apiPrefix.length > 0; setStatus('connected'); } else { try { @@ -341,6 +346,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo return; } } + lastStartAttempts = 1; pendingOperation = startInternal(workdir); try { await pendingOperation; @@ -369,6 +375,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo if (pendingOperation) { await pendingOperation; } + lastStartAttempts = 1; pendingOperation = restartInternal(); try { await pendingOperation; @@ -378,18 +385,21 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo } async function setWorkingDirectory(newPath: string): Promise<{ success: boolean; restarted: boolean; path: string }> { - const target = typeof newPath === 'string' && newPath.trim().length > 0 ? newPath.trim() : workingDirectory; - if (target === workingDirectory) { - return { success: true, restarted: false, path: target }; + const target = typeof newPath === 'string' && newPath.trim().length > 0 ? newPath.trim() : workspaceDirectory(); + const workspacePath = workspaceDirectory(); + const nextDirectory = workspacePath; + + if (workingDirectory === nextDirectory) { + return { success: true, restarted: false, path: nextDirectory }; } - workingDirectory = target; + workingDirectory = nextDirectory; if (useConfiguredUrl && configuredApiUrl) { - return { success: true, restarted: false, path: target }; + return { success: true, restarted: false, path: nextDirectory }; } - return { success: true, restarted: false, path: target }; + return { success: true, restarted: false, path: nextDirectory }; } return { @@ -411,14 +421,17 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null, configuredPort, detectedPort, - apiPrefix, - apiPrefixDetected, + apiPrefix: '', + apiPrefixDetected: true, startCount, restartCount, lastStartAt, lastConnectedAt, lastExitCode, serverUrl: getApiUrl(), + lastReadyElapsedMs, + lastReadyAttempts, + lastStartAttempts, }), onStatusChange(callback) { listeners.add(callback); diff --git a/packages/web/server/index.js b/packages/web/server/index.js index f90a1922..e88be96d 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -875,10 +875,8 @@ let expressApp = null; let currentRestartPromise = null; let isRestartingOpenCode = false; let openCodeApiPrefix = ''; -let openCodeApiPrefixDetected = false; +let openCodeApiPrefixDetected = true; let openCodeApiDetectionTimer = null; -let isDetectingApiPrefix = false; -let openCodeApiDetectionPromise = null; let lastOpenCodeError = null; let isOpenCodeReady = false; let openCodeNotReadySince = 0; @@ -949,10 +947,8 @@ const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix( process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || '' ); -if (ENV_CONFIGURED_API_PREFIX) { - openCodeApiPrefix = ENV_CONFIGURED_API_PREFIX; - openCodeApiPrefixDetected = true; - console.log(`Using OpenCode API prefix from environment: ${openCodeApiPrefix}`); +if (ENV_CONFIGURED_API_PREFIX && ENV_CONFIGURED_API_PREFIX !== '') { + console.warn('Ignoring configured OpenCode API prefix; API runs at root.'); } function setOpenCodePort(port) { @@ -1041,7 +1037,7 @@ function buildAugmentedPath() { return Array.from(augmented).join(path.delimiter); } -const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api +const API_PREFIX_CANDIDATES = ['']; async function waitForReady(url, timeoutMs = 10000) { const start = Date.now(); @@ -1086,36 +1082,17 @@ function normalizeApiPrefix(prefix) { return withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading; } -function setDetectedOpenCodeApiPrefix(prefix) { - const normalized = normalizeApiPrefix(prefix); - if (!openCodeApiPrefixDetected || openCodeApiPrefix !== normalized) { - openCodeApiPrefix = normalized; - openCodeApiPrefixDetected = true; - if (openCodeApiDetectionTimer) { - clearTimeout(openCodeApiDetectionTimer); - openCodeApiDetectionTimer = null; - } - console.log(`Detected OpenCode API prefix: ${normalized || '(root)'}`); +function setDetectedOpenCodeApiPrefix() { + openCodeApiPrefix = ''; + openCodeApiPrefixDetected = true; + if (openCodeApiDetectionTimer) { + clearTimeout(openCodeApiDetectionTimer); + openCodeApiDetectionTimer = null; } } function getCandidateApiPrefixes() { - if (openCodeApiPrefixDetected) { - return [openCodeApiPrefix]; - } - - const candidates = []; - if (openCodeApiPrefix && !candidates.includes(openCodeApiPrefix)) { - candidates.push(openCodeApiPrefix); - } - - for (const candidate of API_PREFIX_CANDIDATES) { - if (!candidates.includes(candidate)) { - candidates.push(candidate); - } - } - - return candidates; + return API_PREFIX_CANDIDATES; } function buildOpenCodeUrl(path, prefixOverride) { @@ -1123,9 +1100,7 @@ function buildOpenCodeUrl(path, prefixOverride) { throw new Error('OpenCode port is not available'); } const normalizedPath = path.startsWith('/') ? path : `/${path}`; - const prefix = normalizeApiPrefix( - prefixOverride !== undefined ? prefixOverride : openCodeApiPrefixDetected ? openCodeApiPrefix : '' - ); + const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : ''); const fullPath = `${prefix}${normalizedPath}`; return `http://localhost:${openCodePort}${fullPath}`; } @@ -1214,165 +1189,22 @@ function writeSseEvent(res, payload) { res.write(`data: ${JSON.stringify(payload)}\n\n`); } -function extractApiPrefixFromUrl(urlString, expectedSuffix) { - if (!urlString) { - return null; - } - try { - const parsed = new URL(urlString); - const pathname = parsed.pathname || ''; - if (expectedSuffix && pathname.endsWith(expectedSuffix)) { - const prefix = pathname.slice(0, pathname.length - expectedSuffix.length); - return normalizeApiPrefix(prefix); - } - } catch (error) { - console.warn(`Failed to parse OpenCode URL "${urlString}": ${error.message}`); - } - return null; +function extractApiPrefixFromUrl() { + return ''; } -async function tryDetectOpenCodeApiPrefix() { - if (!openCodePort) { - return false; - } - - const docPrefix = await detectPrefixFromDocumentation(); - if (docPrefix !== null) { - setDetectedOpenCodeApiPrefix(docPrefix); - return true; - } - - const candidates = getCandidateApiPrefixes(); - - for (const candidate of candidates) { - try { - const response = await fetch(buildOpenCodeUrl('/config', candidate), { - method: 'GET', - headers: { Accept: 'application/json' } - }); - - if (response.ok) { - await response.json().catch(() => null); - setDetectedOpenCodeApiPrefix(candidate); - return true; - } - } catch (error) { - - } - } - - return false; +function detectOpenCodeApiPrefix() { + openCodeApiPrefixDetected = true; + openCodeApiPrefix = ''; + return true; } -async function detectOpenCodeApiPrefix() { - if (openCodeApiPrefixDetected) { - return true; - } - - if (!openCodePort) { - return false; - } - - if (isDetectingApiPrefix) { - try { - await openCodeApiDetectionPromise; - } catch (error) { - - } - return openCodeApiPrefixDetected; - } - - isDetectingApiPrefix = true; - openCodeApiDetectionPromise = (async () => { - const success = await tryDetectOpenCodeApiPrefix(); - if (!success) { - console.warn('Failed to detect OpenCode API prefix via documentation or known candidates'); - } - return success; - })(); - - try { - const result = await openCodeApiDetectionPromise; - return result; - } finally { - isDetectingApiPrefix = false; - openCodeApiDetectionPromise = null; - } +function ensureOpenCodeApiPrefix() { + return detectOpenCodeApiPrefix(); } -async function ensureOpenCodeApiPrefix() { - if (openCodeApiPrefixDetected) { - return true; - } - - const result = await detectOpenCodeApiPrefix(); - if (!result) { - scheduleOpenCodeApiDetection(); - } - return result; -} - -function scheduleOpenCodeApiDetection(delayMs = 500) { - if (openCodeApiPrefixDetected) { - return; - } - - if (openCodeApiDetectionTimer) { - clearTimeout(openCodeApiDetectionTimer); - } - - openCodeApiDetectionTimer = setTimeout(async () => { - openCodeApiDetectionTimer = null; - const success = await detectOpenCodeApiPrefix(); - if (!success) { - const nextDelay = Math.min(delayMs * 2, 8000); - scheduleOpenCodeApiDetection(nextDelay); - } - }, delayMs); -} - -const OPENAPI_DOC_PATHS = ['/doc']; - -function extractPrefixFromOpenApiDocument(content) { - - const globalMatch = content.match(/__OPENCODE_API_BASE__\s*=\s*['"]([^'"]+)['"]/); - if (globalMatch && globalMatch[1]) { - return normalizeApiPrefix(globalMatch[1]); - } - return null; -} - -async function detectPrefixFromDocumentation() { - if (!openCodePort) { - return null; - } - - const prefixesToTry = [...new Set(['', ...API_PREFIX_CANDIDATES])]; - - for (const prefix of prefixesToTry) { - for (const docPath of OPENAPI_DOC_PATHS) { - try { - const response = await fetch(buildOpenCodeUrl(docPath, prefix), { - method: 'GET', - headers: { Accept: '*/*' } - }); - - if (!response.ok) { - continue; - } - - const text = await response.text(); - const extracted = extractPrefixFromOpenApiDocument(text); - if (extracted !== null) { - return extracted; - } - } catch (error) { - - } - } - } - - return null; +function scheduleOpenCodeApiDetection() { + return; } function parseArgs(argv = process.argv.slice(2)) { @@ -1545,13 +1377,12 @@ async function restartOpenCode() { syncToHmrState(); } - // Reset detection state - openCodeApiPrefixDetected = false; + openCodeApiPrefixDetected = true; + openCodeApiPrefix = ''; if (openCodeApiDetectionTimer) { clearTimeout(openCodeApiDetectionTimer); openCodeApiDetectionTimer = null; } - openCodeApiDetectionPromise = null; lastOpenCodeError = null; openCodeProcess = await startOpenCode(); @@ -1573,7 +1404,8 @@ async function restartOpenCode() { openCodePort = null; syncToHmrState(); } - openCodeApiPrefixDetected = false; + openCodeApiPrefixDetected = true; + openCodeApiPrefix = ''; throw error; } finally { currentRestartPromise = null; @@ -1590,74 +1422,51 @@ async function waitForOpenCodeReady(timeoutMs = 20000, intervalMs = 400) { let lastError = null; while (Date.now() < deadline) { - const prefixes = getCandidateApiPrefixes(); - - for (const prefix of prefixes) { - try { - const normalizedPrefix = normalizeApiPrefix(prefix); - - const configPromise = fetch(buildOpenCodeUrl('/config', normalizedPrefix), { + try { + const [configResult, agentResult] = await Promise.all([ + fetch(buildOpenCodeUrl('/config', ''), { method: 'GET', headers: { Accept: 'application/json' } - }).catch((error) => error); - - const agentPromise = fetch(buildOpenCodeUrl('/agent', normalizedPrefix), { + }).catch((error) => error), + fetch(buildOpenCodeUrl('/agent', ''), { method: 'GET', headers: { Accept: 'application/json' } - }).catch((error) => error); + }).catch((error) => error) + ]); - const [configResult, agentResult] = await Promise.all([configPromise, agentPromise]); - - if (configResult instanceof Error) { - lastError = configResult; - continue; - } - - if (!configResult.ok) { - if (configResult.status === 404 && !openCodeApiPrefixDetected && normalizedPrefix === '') { - lastError = new Error('OpenCode config endpoint returned 404 on root prefix'); - } else { - lastError = new Error(`OpenCode config endpoint responded with status ${configResult.status}`); - } - continue; - } - - await configResult.json().catch(() => null); - const detectedPrefix = extractApiPrefixFromUrl(configResult.url, '/config'); - if (detectedPrefix !== null) { - setDetectedOpenCodeApiPrefix(detectedPrefix); - } else if (normalizedPrefix) { - setDetectedOpenCodeApiPrefix(normalizedPrefix); - } - - if (agentResult instanceof Error) { - lastError = agentResult; - continue; - } - - if (!agentResult.ok) { - lastError = new Error(`Agent endpoint responded with status ${agentResult.status}`); - continue; - } - - await agentResult.json().catch(() => []); - - const effectivePrefix = detectedPrefix !== null ? detectedPrefix : normalizedPrefix; - if (detectedPrefix === null) { - const agentPrefix = extractApiPrefixFromUrl(agentResult.url, '/agent'); - if (agentPrefix !== null) { - setDetectedOpenCodeApiPrefix(agentPrefix); - } else if (normalizedPrefix) { - setDetectedOpenCodeApiPrefix(normalizedPrefix); - } - } - - isOpenCodeReady = true; - lastOpenCodeError = null; - return; - } catch (error) { - lastError = error; + if (configResult instanceof Error) { + lastError = configResult; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + continue; } + + if (!configResult.ok) { + lastError = new Error(`OpenCode config endpoint responded with status ${configResult.status}`); + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + continue; + } + + await configResult.json().catch(() => null); + + if (agentResult instanceof Error) { + lastError = agentResult; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + continue; + } + + if (!agentResult.ok) { + lastError = new Error(`Agent endpoint responded with status ${agentResult.status}`); + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + continue; + } + + await agentResult.json().catch(() => []); + + isOpenCodeReady = true; + lastOpenCodeError = null; + return; + } catch (error) { + lastError = error; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); @@ -1829,12 +1638,8 @@ function setupProxy(app) { next(); }); - app.use('/api', async (req, res, next) => { - try { - await ensureOpenCodeApiPrefix(); - } catch (error) { - console.warn(`OpenCode API prefix detection failed for ${req.method} ${req.path}: ${error.message}`); - } + app.use('/api', (_req, _res, next) => { + ensureOpenCodeApiPrefix(); next(); }); @@ -1867,11 +1672,7 @@ function setupProxy(app) { const suffix = path.slice(4) || '/'; - if (!openCodeApiPrefixDetected || openCodeApiPrefix === '') { - return suffix; - } - - return `${openCodeApiPrefix}${suffix}`; + return suffix; }, ws: true, onError: (err, req, res) => { @@ -1903,9 +1704,7 @@ function setupProxy(app) { proxyRes.headers['X-Content-Type-Options'] = 'nosniff'; } - if (proxyRes.statusCode === 404 && !openCodeApiPrefixDetected) { - scheduleOpenCodeApiDetection(); - } + } }); @@ -2013,8 +1812,8 @@ async function main(options = {}) { timestamp: new Date().toISOString(), openCodePort: openCodePort, openCodeRunning: Boolean(openCodePort && isOpenCodeReady && !isRestartingOpenCode), - openCodeApiPrefix, - openCodeApiPrefixDetected, + openCodeApiPrefix: '', + openCodeApiPrefixDetected: true, isOpenCodeReady, lastOpenCodeError }); @@ -2213,18 +2012,9 @@ async function main(options = {}) { }); app.get('/api/global/event', async (req, res) => { - if (!openCodeApiPrefixDetected) { - try { - await detectOpenCodeApiPrefix(); - } catch { - // ignore detection failures - } - } - let targetUrl; try { - const prefix = openCodeApiPrefixDetected ? openCodeApiPrefix : ''; - targetUrl = new URL(buildOpenCodeUrl('/global/event', prefix)); + targetUrl = new URL(buildOpenCodeUrl('/global/event', '')); } catch (error) { return res.status(503).json({ error: 'OpenCode service unavailable' }); } @@ -2330,18 +2120,9 @@ async function main(options = {}) { }); app.get('/api/event', async (req, res) => { - if (!openCodeApiPrefixDetected) { - try { - await detectOpenCodeApiPrefix(); - } catch { - // ignore detection failures - } - } - let targetUrl; try { - const prefix = openCodeApiPrefixDetected ? openCodeApiPrefix : ''; - targetUrl = new URL(buildOpenCodeUrl('/event', prefix)); + targetUrl = new URL(buildOpenCodeUrl('/event', '')); } catch (error) { return res.status(503).json({ error: 'OpenCode service unavailable' }); }