ui: show worker spawn diagnostics

This commit is contained in:
Keisuke Hirata 2026-07-08 02:27:19 +09:00
parent a6fe68301c
commit 9745bf041c
No known key found for this signature in database
2 changed files with 85 additions and 13 deletions

View File

@ -1411,3 +1411,38 @@
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
padding: 0.75rem; padding: 0.75rem;
} }
@layer components {
.worker-submit-error {
display: grid;
gap: var(--space-2);
}
.worker-submit-error p {
margin: 0;
}
.worker-error-diagnostics {
display: grid;
gap: var(--space-1);
margin: 0;
padding-left: var(--space-4);
}
.worker-error-diagnostics li {
color: var(--text-muted);
}
.worker-error-diagnostics li.error {
color: var(--danger);
}
.worker-error-diagnostics strong {
color: inherit;
font-weight: 750;
}
.worker-error-diagnostics span {
display: block;
}
}

View File

@ -5,6 +5,7 @@
import type { import type {
BrowserCreateWorkerResponse, BrowserCreateWorkerResponse,
BrowserWorkingDirectoryCreateResponse, BrowserWorkingDirectoryCreateResponse,
Diagnostic,
ListResponse, ListResponse,
Worker, Worker,
WorkerLaunchOptionsResponse, WorkerLaunchOptionsResponse,
@ -17,6 +18,17 @@
workspaceId: string; workspaceId: string;
}; };
type DisplayError = {
message: string;
diagnostics: Diagnostic[];
};
type ErrorPayload = {
error?: { message?: string; code?: string } | string;
message?: string;
diagnostics?: Diagnostic[];
};
let { currentPath = '/', workspaceId }: Props = $props(); let { currentPath = '/', workspaceId }: Props = $props();
function workerApiPath(path: string): string { function workerApiPath(path: string): string {
@ -31,7 +43,7 @@
let optionsError = $state<string | null>(null); let optionsError = $state<string | null>(null);
let showNewWorker = $state(false); let showNewWorker = $state(false);
let submitting = $state(false); let submitting = $state(false);
let submitError = $state<string | null>(null); let submitError = $state<DisplayError | null>(null);
let displayName = $state('Coding Worker'); let displayName = $state('Coding Worker');
let runtimeId = $state(''); let runtimeId = $state('');
let profile = $state('builtin:coder'); let profile = $state('builtin:coder');
@ -124,7 +136,7 @@
async function createWorkingDirectory() { async function createWorkingDirectory() {
if (!workingDirectoryRepositoryId) { if (!workingDirectoryRepositoryId) {
submitError = 'select a repository before creating a working directory'; submitError = { message: 'select a repository before creating a working directory', diagnostics: [] };
return; return;
} }
creatingWorkingDirectory = true; creatingWorkingDirectory = true;
@ -140,7 +152,8 @@
}), }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(await responseErrorMessage(response, 'working directory create failed')); submitError = await responseDisplayError(response, 'working directory create failed');
return;
} }
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse; const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
const items = options?.working_directories ?? []; const items = options?.working_directories ?? [];
@ -149,7 +162,7 @@
: options; : options;
workingDirectoryAllocationId = payload.item.allocation_id; workingDirectoryAllocationId = payload.item.allocation_id;
} catch (err) { } catch (err) {
submitError = err instanceof Error ? err.message : 'working directory create failed'; submitError = exceptionDisplayError(err, 'working directory create failed');
} finally { } finally {
creatingWorkingDirectory = false; creatingWorkingDirectory = false;
} }
@ -157,7 +170,7 @@
async function createWorker() { async function createWorker() {
if (!workspaceId) { if (!workspaceId) {
submitError = 'workspace id is unavailable'; submitError = { message: 'workspace id is unavailable', diagnostics: [] };
return; return;
} }
@ -179,32 +192,44 @@
})), })),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(await responseErrorMessage(response, 'worker create failed')); submitError = await responseDisplayError(response, 'worker create failed');
return;
} }
const payload = (await response.json()) as BrowserCreateWorkerResponse; const payload = (await response.json()) as BrowserCreateWorkerResponse;
await loadWorkers(); await loadWorkers();
window.location.href = payload.console_href; window.location.href = payload.console_href;
} catch (err) { } catch (err) {
submitError = err instanceof Error ? err.message : 'worker create failed'; submitError = exceptionDisplayError(err, 'worker create failed');
} finally { } finally {
submitting = false; submitting = false;
} }
} }
async function responseErrorMessage(response: Response, fallback: string): Promise<string> { async function responseDisplayError(response: Response, fallback: string): Promise<DisplayError> {
try { try {
const payload = (await response.json()) as { error?: { message?: string; code?: string } | string; message?: string }; const payload = (await response.json()) as ErrorPayload;
const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
if (typeof payload.error === 'object' && payload.error?.message) { if (typeof payload.error === 'object' && payload.error?.message) {
return `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`; return {
message: `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`,
diagnostics,
};
} }
if (payload.message) { if (payload.message) {
const code = typeof payload.error === 'string' ? payload.error : 'request_failed'; const code = typeof payload.error === 'string' ? payload.error : 'request_failed';
return `${code}: ${payload.message}`; return { message: `${code}: ${payload.message}`, diagnostics };
} }
} catch { } catch {
// fall through // fall through
} }
return `${fallback} (${response.status})`; return { message: `${fallback} (${response.status})`, diagnostics: [] };
}
function exceptionDisplayError(err: unknown, fallback: string): DisplayError {
return {
message: err instanceof Error ? err.message : fallback,
diagnostics: [],
};
} }
</script> </script>
@ -296,7 +321,19 @@
<p class="section-state error">{optionsError}</p> <p class="section-state error">{optionsError}</p>
{/if} {/if}
{#if submitError} {#if submitError}
<p class="section-state error">{submitError}</p> <div class="section-state error worker-submit-error">
<p>{submitError.message}</p>
{#if submitError.diagnostics.length > 0}
<ul class="worker-error-diagnostics">
{#each submitError.diagnostics as diagnostic}
<li class={diagnostic.severity}>
<strong>{diagnostic.code}</strong>
<span>{diagnostic.message}</span>
</li>
{/each}
</ul>
{/if}
</div>
{/if} {/if}
<button type="submit" disabled={submitting || !runtimeId || !profile || !workingDirectoryAllocationId}> <button type="submit" disabled={submitting || !runtimeId || !profile || !workingDirectoryAllocationId}>
{submitting ? 'Starting…' : 'Start Coding Worker'} {submitting ? 'Starting…' : 'Start Coding Worker'}