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:
Bohdan Triapitsyn
2026-01-16 14:43:53 +02:00
parent 5389bae465
commit 976018dd32
9 changed files with 234 additions and 332 deletions
+43 -30
View File
@@ -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<ReadyResult> {
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<void> | 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<void> {
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);