feat: add copy diagnostics button
Add copy diagnostics button in About dialog. Button copies report with OpenChamber state, OpenCode health, directories, and projects. Show success or error toasts after copying attempt.
This commit is contained in:
@@ -138,6 +138,7 @@ export const AboutSettings: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Desktop layout (unchanged)
|
// Desktop layout (unchanged)
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-6">
|
<div className="w-full space-y-6">
|
||||||
@@ -200,6 +201,7 @@ export const AboutSettings: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
{/* Links */}
|
{/* Links */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
|||||||
<AboutSettings />
|
<AboutSettings />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||||
import { RiDiscordFill, RiGithubFill, RiTwitterXFill } from '@remixicon/react';
|
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;
|
declare const __APP_VERSION__: string | undefined;
|
||||||
|
|
||||||
@@ -18,6 +21,28 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [version, setVersion] = React.useState<string | null>(null);
|
const [version, setVersion] = React.useState<string | null>(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(() => {
|
React.useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -70,6 +95,23 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({
|
|||||||
agent
|
agent
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCopyDiagnostics}
|
||||||
|
disabled={isCopyingDiagnostics}
|
||||||
|
className={cn(
|
||||||
|
'typography-meta text-muted-foreground hover:text-foreground',
|
||||||
|
'underline-offset-2 hover:underline',
|
||||||
|
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedDiagnostics ? 'Diagnostics copied' : 'Copy diagnostics'}
|
||||||
|
</button>
|
||||||
|
<p className="typography-micro text-muted-foreground">
|
||||||
|
Includes OpenChamber state, OpenCode health, directories, and projects.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 pt-2">
|
<div className="flex items-center gap-4 pt-2">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/btriapitsyn/openchamber"
|
href="https://github.com/btriapitsyn/openchamber"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { opencodeClient } from '@/lib/opencode/client';
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||||
import { streamDebugEnabled } from '@/stores/utils/streamDebug';
|
import { streamDebugEnabled } from '@/stores/utils/streamDebug';
|
||||||
@@ -176,6 +177,7 @@ export const debugUtils = {
|
|||||||
async getAppStatus() {
|
async getAppStatus() {
|
||||||
const directoryState = useDirectoryStore.getState();
|
const directoryState = useDirectoryStore.getState();
|
||||||
const sessionState = useSessionStore.getState();
|
const sessionState = useSessionStore.getState();
|
||||||
|
const projectsState = useProjectsStore.getState();
|
||||||
const currentDirectory = directoryState.currentDirectory || null;
|
const currentDirectory = directoryState.currentDirectory || null;
|
||||||
const opencodeDirectory = opencodeClient.getDirectory() ?? null;
|
const opencodeDirectory = opencodeClient.getDirectory() ?? null;
|
||||||
|
|
||||||
@@ -291,9 +293,17 @@ export const debugUtils = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectSamples = projectsState.projects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
path: project.path,
|
||||||
|
label: project.label,
|
||||||
|
}));
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
runtime: {
|
runtime: {
|
||||||
|
platform: runtimeApis?.runtime?.platform ?? null,
|
||||||
isDesktop: isDesktopRuntime,
|
isDesktop: isDesktopRuntime,
|
||||||
|
isVSCode: Boolean(runtimeApis?.runtime?.isVSCode),
|
||||||
hasRuntimeApis: Boolean(runtimeApis),
|
hasRuntimeApis: Boolean(runtimeApis),
|
||||||
desktopServerOrigin: desktopServer?.origin ?? null,
|
desktopServerOrigin: desktopServer?.origin ?? null,
|
||||||
},
|
},
|
||||||
@@ -313,6 +323,11 @@ export const debugUtils = {
|
|||||||
hasPersistedDirectory: directoryState.hasPersistedDirectory,
|
hasPersistedDirectory: directoryState.hasPersistedDirectory,
|
||||||
isSwitchingDirectory: directoryState.isSwitchingDirectory,
|
isSwitchingDirectory: directoryState.isSwitchingDirectory,
|
||||||
},
|
},
|
||||||
|
projects: {
|
||||||
|
total: projectsState.projects.length,
|
||||||
|
activeProjectId: projectsState.activeProjectId,
|
||||||
|
samples: projectSamples,
|
||||||
|
},
|
||||||
sessions: {
|
sessions: {
|
||||||
total: sessions.length,
|
total: sessions.length,
|
||||||
currentSessionId: sessionState.currentSessionId,
|
currentSessionId: sessionState.currentSessionId,
|
||||||
@@ -341,6 +356,20 @@ export const debugUtils = {
|
|||||||
return report;
|
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() {
|
checkLastMessage() {
|
||||||
const info = this.getLastAssistantMessage();
|
const info = this.getLastAssistantMessage();
|
||||||
if (!info) return false;
|
if (!info) return false;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
|
|
||||||
export const applyPersistedDirectoryPreferences = async (): Promise<void> => {
|
export const applyPersistedDirectoryPreferences = async (): Promise<void> => {
|
||||||
@@ -21,7 +22,7 @@ export const applyPersistedDirectoryPreferences = async (): Promise<void> => {
|
|||||||
directoryStore.synchronizeHomeDirectory(savedHome);
|
directoryStore.synchronizeHomeDirectory(savedHome);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedDirectory) {
|
if (savedDirectory && !isVSCodeRuntime()) {
|
||||||
directoryStore.setDirectory(savedDirectory, { showOverlay: false });
|
directoryStore.setDirectory(savedDirectory, { showOverlay: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools } from 'zustand/middleware';
|
import { devtools } from 'zustand/middleware';
|
||||||
import { opencodeClient } from '@/lib/opencode/client';
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
import { getDesktopHomeDirectory } from '@/lib/desktop';
|
import { getDesktopHomeDirectory, isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||||
import { streamDebugEnabled } from '@/stores/utils/streamDebug';
|
import { streamDebugEnabled } from '@/stores/utils/streamDebug';
|
||||||
@@ -76,7 +76,7 @@ const getHomeDirectory = () => {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const storedHome = safeStorage.getItem('homeDirectory') || cachedHomeDirectory || null;
|
const storedHome = safeStorage.getItem('homeDirectory') || cachedHomeDirectory || null;
|
||||||
const saved = safeStorage.getItem('lastDirectory');
|
const saved = safeStorage.getItem('lastDirectory');
|
||||||
if (saved) {
|
if (saved && !isVSCodeRuntime()) {
|
||||||
return resolveDirectoryPath(saved, storedHome);
|
return resolveDirectoryPath(saved, storedHome);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ const getHomeDirectory = () => {
|
|||||||
return desktopHome;
|
return desktopHome;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedHome) {
|
if (storedHome && !isVSCodeRuntime()) {
|
||||||
cachedHomeDirectory = storedHome;
|
cachedHomeDirectory = storedHome;
|
||||||
return storedHome;
|
return storedHome;
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,19 @@ const initializeHomeDirectory = async () => {
|
|||||||
return fallback;
|
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) {
|
if (initialHomeDirectory) {
|
||||||
opencodeClient.setDirectory(initialHomeDirectory);
|
opencodeClient.setDirectory(initialHomeDirectory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,12 +342,22 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
const extensionVersion = String(context.extension?.packageJSON?.version || '');
|
const extensionVersion = String(context.extension?.packageJSON?.version || '');
|
||||||
const workspaceFolders = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri.fsPath);
|
const workspaceFolders = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri.fsPath);
|
||||||
|
const primaryWorkspace = workspaceFolders[0] || '';
|
||||||
|
|
||||||
const debug = openCodeManager?.getDebugInfo();
|
const debug = openCodeManager?.getDebugInfo();
|
||||||
const resolvedApiUrl = openCodeManager?.getApiUrl();
|
const resolvedApiUrl = openCodeManager?.getApiUrl();
|
||||||
const workingDirectory = openCodeManager?.getWorkingDirectory() ?? '';
|
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 controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
@@ -407,12 +417,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
{ label: 'config', path: '/config', includeDirectory: true },
|
{ label: 'config', path: '/config', includeDirectory: true },
|
||||||
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
{ label: 'providers', path: '/config/providers', includeDirectory: true },
|
||||||
// Can be slower on large configs; keep the probe from producing false negatives.
|
// Can be slower on large configs; keep the probe from producing false negatives.
|
||||||
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 8000 },
|
{ label: 'agents', path: '/agent', includeDirectory: true, timeoutMs: 12000 },
|
||||||
{ label: 'commands', path: '/command', includeDirectory: true },
|
{ label: 'commands', path: '/command', includeDirectory: true, timeoutMs: 10000 },
|
||||||
{ label: 'project', path: '/project/current', includeDirectory: true },
|
{ label: 'project', path: '/project/current', includeDirectory: true },
|
||||||
{ label: 'path', path: '/path', includeDirectory: true },
|
{ label: 'path', path: '/path', includeDirectory: true },
|
||||||
// Session listing is what powers the sidebar. This helps diagnose "no sessions shown" bugs.
|
// 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 },
|
{ label: 'sessionStatus', path: '/session/status', includeDirectory: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -439,9 +449,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
`Platform: ${process.platform} ${process.arch}`,
|
`Platform: ${process.platform} ${process.arch}`,
|
||||||
`Workspace folders: ${workspaceFolders.length}${workspaceFolders.length ? ` (${workspaceFolders.join(', ')})` : ''}`,
|
`Workspace folders: ${workspaceFolders.length}${workspaceFolders.length ? ` (${workspaceFolders.join(', ')})` : ''}`,
|
||||||
`Status: ${openCodeManager?.getStatus() ?? 'unknown'}`,
|
`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 (configured): ${configuredApiUrl || '(none)'}`,
|
||||||
`API URL (resolved): ${openCodeManager?.getApiUrl() ?? '(none)'}`,
|
`API URL (resolved): ${openCodeManager?.getApiUrl() ?? '(none)'}`,
|
||||||
|
`API URL path: ${resolvedApiPath || '(none)'}`,
|
||||||
debug
|
debug
|
||||||
? `OpenCode server URL: ${debug.serverUrl ?? '(none)'}`
|
? `OpenCode server URL: ${debug.serverUrl ?? '(none)'}`
|
||||||
: `OpenCode server URL: (unknown)`,
|
: `OpenCode server URL: (unknown)`,
|
||||||
@@ -460,6 +472,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
debug
|
debug
|
||||||
? `Last start: ${formatIso(debug.lastStartAt)}`
|
? `Last start: ${formatIso(debug.lastStartAt)}`
|
||||||
: `Last start: (unknown)`,
|
: `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
|
debug
|
||||||
? `Last connected: ${formatIso(debug.lastConnectedAt)}`
|
? `Last connected: ${formatIso(debug.lastConnectedAt)}`
|
||||||
: `Last connected: (unknown)`,
|
: `Last connected: (unknown)`,
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type OpenCodeDebugInfo = {
|
|||||||
lastConnectedAt: number | null;
|
lastConnectedAt: number | null;
|
||||||
lastExitCode: number | null;
|
lastExitCode: number | null;
|
||||||
serverUrl: string | null;
|
serverUrl: string | null;
|
||||||
|
lastReadyElapsedMs: number | null;
|
||||||
|
lastReadyAttempts: number | null;
|
||||||
|
lastStartAttempts: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface OpenCodeManager {
|
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 {
|
function normalizeBaseUrl(value: string): string {
|
||||||
return value.replace(/\/+$/, '');
|
return value.replace(/\/+$/, '');
|
||||||
@@ -83,9 +88,11 @@ function getCandidateBaseUrls(serverUrl: string): string[] {
|
|||||||
async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirectory = ''): Promise<ReadyResult> {
|
async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirectory = ''): Promise<ReadyResult> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const candidates = getCandidateBaseUrls(serverUrl);
|
const candidates = getCandidateBaseUrls(serverUrl);
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
for (const baseUrl of candidates) {
|
for (const baseUrl of candidates) {
|
||||||
|
attempts += 1;
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||||
@@ -102,7 +109,9 @@ async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirector
|
|||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
if (res.ok) return { ok: true, baseUrl };
|
if (res.ok) {
|
||||||
|
return { ok: true, baseUrl, elapsedMs: Date.now() - start, attempts };
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -111,20 +120,11 @@ async function waitForReady(serverUrl: string, timeoutMs = 5000, workingDirector
|
|||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise(r => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: false };
|
return { ok: false, elapsedMs: Date.now() - start, attempts };
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferApiPrefixFromUrl(url: string): string {
|
function inferApiPrefixFromUrl(): string {
|
||||||
try {
|
return '';
|
||||||
const parsed = new URL(url);
|
|
||||||
const pathname = parsed.pathname;
|
|
||||||
if (pathname === '/' || pathname === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCodeManager {
|
export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCodeManager {
|
||||||
@@ -135,16 +135,19 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
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>();
|
||||||
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 startCount = 0;
|
||||||
let restartCount = 0;
|
let restartCount = 0;
|
||||||
let lastStartAt: number | null = null;
|
let lastStartAt: number | null = null;
|
||||||
let lastConnectedAt: number | null = null;
|
let lastConnectedAt: number | null = null;
|
||||||
let lastExitCode: 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 detectedPort: number | null = null;
|
||||||
let apiPrefix: string = '';
|
|
||||||
let apiPrefixDetected = false;
|
|
||||||
let cliMissing = false;
|
let cliMissing = false;
|
||||||
|
|
||||||
let pendingOperation: Promise<void> | null = null;
|
let pendingOperation: Promise<void> | null = null;
|
||||||
@@ -187,17 +190,21 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
return server.url.replace(/\/+$/, '');
|
return server.url.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
if (detectedPort) {
|
if (detectedPort) {
|
||||||
return `http://127.0.0.1:${detectedPort}${apiPrefix}`;
|
return `http://127.0.0.1:${detectedPort}`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function startInternal(workdir?: string): Promise<void> {
|
async function startInternal(workdir?: string): Promise<void> {
|
||||||
startCount += 1;
|
startCount += 1;
|
||||||
|
setStatus('connecting');
|
||||||
lastStartAt = Date.now();
|
lastStartAt = Date.now();
|
||||||
|
lastStartAttempts = startCount;
|
||||||
|
|
||||||
if (typeof workdir === 'string' && workdir.trim().length > 0) {
|
if (typeof workdir === 'string' && workdir.trim().length > 0) {
|
||||||
workingDirectory = workdir.trim();
|
workingDirectory = workdir.trim();
|
||||||
|
} else {
|
||||||
|
workingDirectory = workspaceDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useConfiguredUrl && configuredApiUrl) {
|
if (useConfiguredUrl && configuredApiUrl) {
|
||||||
@@ -218,8 +225,6 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
cliMissing = false;
|
cliMissing = false;
|
||||||
|
|
||||||
detectedPort = null;
|
detectedPort = null;
|
||||||
apiPrefix = '';
|
|
||||||
apiPrefixDetected = false;
|
|
||||||
lastExitCode = null;
|
lastExitCode = null;
|
||||||
managedApiUrlOverride = null;
|
managedApiUrlOverride = null;
|
||||||
|
|
||||||
@@ -247,11 +252,11 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
if (server && server.url) {
|
if (server && server.url) {
|
||||||
// Validate readiness for the current workspace context.
|
// Validate readiness for the current workspace context.
|
||||||
const ready = await waitForReady(server.url, 10000, workingDirectory);
|
const ready = await waitForReady(server.url, 10000, workingDirectory);
|
||||||
|
lastReadyElapsedMs = ready.elapsedMs;
|
||||||
|
lastReadyAttempts = ready.attempts;
|
||||||
if (ready.ok) {
|
if (ready.ok) {
|
||||||
managedApiUrlOverride = ready.baseUrl;
|
managedApiUrlOverride = ready.baseUrl;
|
||||||
detectedPort = resolvePortFromUrl(ready.baseUrl);
|
detectedPort = resolvePortFromUrl(ready.baseUrl);
|
||||||
apiPrefix = inferApiPrefixFromUrl(ready.baseUrl);
|
|
||||||
apiPrefixDetected = apiPrefix.length > 0;
|
|
||||||
setStatus('connected');
|
setStatus('connected');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +346,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lastStartAttempts = 1;
|
||||||
pendingOperation = startInternal(workdir);
|
pendingOperation = startInternal(workdir);
|
||||||
try {
|
try {
|
||||||
await pendingOperation;
|
await pendingOperation;
|
||||||
@@ -369,6 +375,7 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
if (pendingOperation) {
|
if (pendingOperation) {
|
||||||
await pendingOperation;
|
await pendingOperation;
|
||||||
}
|
}
|
||||||
|
lastStartAttempts = 1;
|
||||||
pendingOperation = restartInternal();
|
pendingOperation = restartInternal();
|
||||||
try {
|
try {
|
||||||
await pendingOperation;
|
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 }> {
|
async function setWorkingDirectory(newPath: string): Promise<{ success: boolean; restarted: boolean; path: string }> {
|
||||||
const target = typeof newPath === 'string' && newPath.trim().length > 0 ? newPath.trim() : workingDirectory;
|
const target = typeof newPath === 'string' && newPath.trim().length > 0 ? newPath.trim() : workspaceDirectory();
|
||||||
if (target === workingDirectory) {
|
const workspacePath = workspaceDirectory();
|
||||||
return { success: true, restarted: false, path: target };
|
const nextDirectory = workspacePath;
|
||||||
|
|
||||||
|
if (workingDirectory === nextDirectory) {
|
||||||
|
return { success: true, restarted: false, path: nextDirectory };
|
||||||
}
|
}
|
||||||
|
|
||||||
workingDirectory = target;
|
workingDirectory = nextDirectory;
|
||||||
|
|
||||||
if (useConfiguredUrl && configuredApiUrl) {
|
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 {
|
return {
|
||||||
@@ -411,14 +421,17 @@ export function createOpenCodeManager(_context: vscode.ExtensionContext): OpenCo
|
|||||||
configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null,
|
configuredApiUrl: useConfiguredUrl && configuredApiUrl ? configuredApiUrl.replace(/\/+$/, '') : null,
|
||||||
configuredPort,
|
configuredPort,
|
||||||
detectedPort,
|
detectedPort,
|
||||||
apiPrefix,
|
apiPrefix: '',
|
||||||
apiPrefixDetected,
|
apiPrefixDetected: true,
|
||||||
startCount,
|
startCount,
|
||||||
restartCount,
|
restartCount,
|
||||||
lastStartAt,
|
lastStartAt,
|
||||||
lastConnectedAt,
|
lastConnectedAt,
|
||||||
lastExitCode,
|
lastExitCode,
|
||||||
serverUrl: getApiUrl(),
|
serverUrl: getApiUrl(),
|
||||||
|
lastReadyElapsedMs,
|
||||||
|
lastReadyAttempts,
|
||||||
|
lastStartAttempts,
|
||||||
}),
|
}),
|
||||||
onStatusChange(callback) {
|
onStatusChange(callback) {
|
||||||
listeners.add(callback);
|
listeners.add(callback);
|
||||||
|
|||||||
+73
-292
@@ -875,10 +875,8 @@ let expressApp = null;
|
|||||||
let currentRestartPromise = null;
|
let currentRestartPromise = null;
|
||||||
let isRestartingOpenCode = false;
|
let isRestartingOpenCode = false;
|
||||||
let openCodeApiPrefix = '';
|
let openCodeApiPrefix = '';
|
||||||
let openCodeApiPrefixDetected = false;
|
let openCodeApiPrefixDetected = true;
|
||||||
let openCodeApiDetectionTimer = null;
|
let openCodeApiDetectionTimer = null;
|
||||||
let isDetectingApiPrefix = false;
|
|
||||||
let openCodeApiDetectionPromise = null;
|
|
||||||
let lastOpenCodeError = null;
|
let lastOpenCodeError = null;
|
||||||
let isOpenCodeReady = false;
|
let isOpenCodeReady = false;
|
||||||
let openCodeNotReadySince = 0;
|
let openCodeNotReadySince = 0;
|
||||||
@@ -949,10 +947,8 @@ const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
|
|||||||
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ENV_CONFIGURED_API_PREFIX) {
|
if (ENV_CONFIGURED_API_PREFIX && ENV_CONFIGURED_API_PREFIX !== '') {
|
||||||
openCodeApiPrefix = ENV_CONFIGURED_API_PREFIX;
|
console.warn('Ignoring configured OpenCode API prefix; API runs at root.');
|
||||||
openCodeApiPrefixDetected = true;
|
|
||||||
console.log(`Using OpenCode API prefix from environment: ${openCodeApiPrefix}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOpenCodePort(port) {
|
function setOpenCodePort(port) {
|
||||||
@@ -1041,7 +1037,7 @@ function buildAugmentedPath() {
|
|||||||
return Array.from(augmented).join(path.delimiter);
|
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) {
|
async function waitForReady(url, timeoutMs = 10000) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
@@ -1086,36 +1082,17 @@ function normalizeApiPrefix(prefix) {
|
|||||||
return withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading;
|
return withLeading.endsWith('/') ? withLeading.slice(0, -1) : withLeading;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDetectedOpenCodeApiPrefix(prefix) {
|
function setDetectedOpenCodeApiPrefix() {
|
||||||
const normalized = normalizeApiPrefix(prefix);
|
openCodeApiPrefix = '';
|
||||||
if (!openCodeApiPrefixDetected || openCodeApiPrefix !== normalized) {
|
openCodeApiPrefixDetected = true;
|
||||||
openCodeApiPrefix = normalized;
|
if (openCodeApiDetectionTimer) {
|
||||||
openCodeApiPrefixDetected = true;
|
clearTimeout(openCodeApiDetectionTimer);
|
||||||
if (openCodeApiDetectionTimer) {
|
openCodeApiDetectionTimer = null;
|
||||||
clearTimeout(openCodeApiDetectionTimer);
|
|
||||||
openCodeApiDetectionTimer = null;
|
|
||||||
}
|
|
||||||
console.log(`Detected OpenCode API prefix: ${normalized || '(root)'}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCandidateApiPrefixes() {
|
function getCandidateApiPrefixes() {
|
||||||
if (openCodeApiPrefixDetected) {
|
return API_PREFIX_CANDIDATES;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOpenCodeUrl(path, prefixOverride) {
|
function buildOpenCodeUrl(path, prefixOverride) {
|
||||||
@@ -1123,9 +1100,7 @@ function buildOpenCodeUrl(path, prefixOverride) {
|
|||||||
throw new Error('OpenCode port is not available');
|
throw new Error('OpenCode port is not available');
|
||||||
}
|
}
|
||||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
const prefix = normalizeApiPrefix(
|
const prefix = normalizeApiPrefix(prefixOverride !== undefined ? prefixOverride : '');
|
||||||
prefixOverride !== undefined ? prefixOverride : openCodeApiPrefixDetected ? openCodeApiPrefix : ''
|
|
||||||
);
|
|
||||||
const fullPath = `${prefix}${normalizedPath}`;
|
const fullPath = `${prefix}${normalizedPath}`;
|
||||||
return `http://localhost:${openCodePort}${fullPath}`;
|
return `http://localhost:${openCodePort}${fullPath}`;
|
||||||
}
|
}
|
||||||
@@ -1214,165 +1189,22 @@ function writeSseEvent(res, payload) {
|
|||||||
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractApiPrefixFromUrl(urlString, expectedSuffix) {
|
function extractApiPrefixFromUrl() {
|
||||||
if (!urlString) {
|
return '';
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryDetectOpenCodeApiPrefix() {
|
function detectOpenCodeApiPrefix() {
|
||||||
if (!openCodePort) {
|
openCodeApiPrefixDetected = true;
|
||||||
return false;
|
openCodeApiPrefix = '';
|
||||||
}
|
return true;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectOpenCodeApiPrefix() {
|
function ensureOpenCodeApiPrefix() {
|
||||||
if (openCodeApiPrefixDetected) {
|
return detectOpenCodeApiPrefix();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureOpenCodeApiPrefix() {
|
function scheduleOpenCodeApiDetection() {
|
||||||
if (openCodeApiPrefixDetected) {
|
return;
|
||||||
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 parseArgs(argv = process.argv.slice(2)) {
|
function parseArgs(argv = process.argv.slice(2)) {
|
||||||
@@ -1545,13 +1377,12 @@ async function restartOpenCode() {
|
|||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset detection state
|
openCodeApiPrefixDetected = true;
|
||||||
openCodeApiPrefixDetected = false;
|
openCodeApiPrefix = '';
|
||||||
if (openCodeApiDetectionTimer) {
|
if (openCodeApiDetectionTimer) {
|
||||||
clearTimeout(openCodeApiDetectionTimer);
|
clearTimeout(openCodeApiDetectionTimer);
|
||||||
openCodeApiDetectionTimer = null;
|
openCodeApiDetectionTimer = null;
|
||||||
}
|
}
|
||||||
openCodeApiDetectionPromise = null;
|
|
||||||
|
|
||||||
lastOpenCodeError = null;
|
lastOpenCodeError = null;
|
||||||
openCodeProcess = await startOpenCode();
|
openCodeProcess = await startOpenCode();
|
||||||
@@ -1573,7 +1404,8 @@ async function restartOpenCode() {
|
|||||||
openCodePort = null;
|
openCodePort = null;
|
||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
}
|
}
|
||||||
openCodeApiPrefixDetected = false;
|
openCodeApiPrefixDetected = true;
|
||||||
|
openCodeApiPrefix = '';
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
currentRestartPromise = null;
|
currentRestartPromise = null;
|
||||||
@@ -1590,74 +1422,51 @@ async function waitForOpenCodeReady(timeoutMs = 20000, intervalMs = 400) {
|
|||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const prefixes = getCandidateApiPrefixes();
|
try {
|
||||||
|
const [configResult, agentResult] = await Promise.all([
|
||||||
for (const prefix of prefixes) {
|
fetch(buildOpenCodeUrl('/config', ''), {
|
||||||
try {
|
|
||||||
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
||||||
|
|
||||||
const configPromise = fetch(buildOpenCodeUrl('/config', normalizedPrefix), {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' }
|
headers: { Accept: 'application/json' }
|
||||||
}).catch((error) => error);
|
}).catch((error) => error),
|
||||||
|
fetch(buildOpenCodeUrl('/agent', ''), {
|
||||||
const agentPromise = fetch(buildOpenCodeUrl('/agent', normalizedPrefix), {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' }
|
headers: { Accept: 'application/json' }
|
||||||
}).catch((error) => error);
|
}).catch((error) => error)
|
||||||
|
]);
|
||||||
|
|
||||||
const [configResult, agentResult] = await Promise.all([configPromise, agentPromise]);
|
if (configResult instanceof Error) {
|
||||||
|
lastError = configResult;
|
||||||
if (configResult instanceof Error) {
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
lastError = configResult;
|
continue;
|
||||||
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.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));
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
@@ -1829,12 +1638,8 @@ function setupProxy(app) {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api', async (req, res, next) => {
|
app.use('/api', (_req, _res, next) => {
|
||||||
try {
|
ensureOpenCodeApiPrefix();
|
||||||
await ensureOpenCodeApiPrefix();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`OpenCode API prefix detection failed for ${req.method} ${req.path}: ${error.message}`);
|
|
||||||
}
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1867,11 +1672,7 @@ function setupProxy(app) {
|
|||||||
|
|
||||||
const suffix = path.slice(4) || '/';
|
const suffix = path.slice(4) || '/';
|
||||||
|
|
||||||
if (!openCodeApiPrefixDetected || openCodeApiPrefix === '') {
|
return suffix;
|
||||||
return suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${openCodeApiPrefix}${suffix}`;
|
|
||||||
},
|
},
|
||||||
ws: true,
|
ws: true,
|
||||||
onError: (err, req, res) => {
|
onError: (err, req, res) => {
|
||||||
@@ -1903,9 +1704,7 @@ function setupProxy(app) {
|
|||||||
proxyRes.headers['X-Content-Type-Options'] = 'nosniff';
|
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(),
|
timestamp: new Date().toISOString(),
|
||||||
openCodePort: openCodePort,
|
openCodePort: openCodePort,
|
||||||
openCodeRunning: Boolean(openCodePort && isOpenCodeReady && !isRestartingOpenCode),
|
openCodeRunning: Boolean(openCodePort && isOpenCodeReady && !isRestartingOpenCode),
|
||||||
openCodeApiPrefix,
|
openCodeApiPrefix: '',
|
||||||
openCodeApiPrefixDetected,
|
openCodeApiPrefixDetected: true,
|
||||||
isOpenCodeReady,
|
isOpenCodeReady,
|
||||||
lastOpenCodeError
|
lastOpenCodeError
|
||||||
});
|
});
|
||||||
@@ -2213,18 +2012,9 @@ async function main(options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/global/event', async (req, res) => {
|
app.get('/api/global/event', async (req, res) => {
|
||||||
if (!openCodeApiPrefixDetected) {
|
|
||||||
try {
|
|
||||||
await detectOpenCodeApiPrefix();
|
|
||||||
} catch {
|
|
||||||
// ignore detection failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetUrl;
|
let targetUrl;
|
||||||
try {
|
try {
|
||||||
const prefix = openCodeApiPrefixDetected ? openCodeApiPrefix : '';
|
targetUrl = new URL(buildOpenCodeUrl('/global/event', ''));
|
||||||
targetUrl = new URL(buildOpenCodeUrl('/global/event', prefix));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
||||||
}
|
}
|
||||||
@@ -2330,18 +2120,9 @@ async function main(options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/event', async (req, res) => {
|
app.get('/api/event', async (req, res) => {
|
||||||
if (!openCodeApiPrefixDetected) {
|
|
||||||
try {
|
|
||||||
await detectOpenCodeApiPrefix();
|
|
||||||
} catch {
|
|
||||||
// ignore detection failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetUrl;
|
let targetUrl;
|
||||||
try {
|
try {
|
||||||
const prefix = openCodeApiPrefixDetected ? openCodeApiPrefix : '';
|
targetUrl = new URL(buildOpenCodeUrl('/event', ''));
|
||||||
targetUrl = new URL(buildOpenCodeUrl('/event', prefix));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
return res.status(503).json({ error: 'OpenCode service unavailable' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user