diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 789b733a..9d9bd050 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -1411,3 +1411,38 @@ background: rgba(255, 255, 255, 0.04); 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; + } +} diff --git a/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte index 4a9acbf3..526776d7 100644 --- a/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte +++ b/web/workspace/src/lib/workspace-sidebar/WorkersNavSection.svelte @@ -5,6 +5,7 @@ import type { BrowserCreateWorkerResponse, BrowserWorkingDirectoryCreateResponse, + Diagnostic, ListResponse, Worker, WorkerLaunchOptionsResponse, @@ -17,6 +18,17 @@ 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(); function workerApiPath(path: string): string { @@ -31,7 +43,7 @@ let optionsError = $state(null); let showNewWorker = $state(false); let submitting = $state(false); - let submitError = $state(null); + let submitError = $state(null); let displayName = $state('Coding Worker'); let runtimeId = $state(''); let profile = $state('builtin:coder'); @@ -124,7 +136,7 @@ async function createWorkingDirectory() { if (!workingDirectoryRepositoryId) { - submitError = 'select a repository before creating a working directory'; + submitError = { message: 'select a repository before creating a working directory', diagnostics: [] }; return; } creatingWorkingDirectory = true; @@ -140,7 +152,8 @@ }), }); 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 items = options?.working_directories ?? []; @@ -149,7 +162,7 @@ : options; workingDirectoryAllocationId = payload.item.allocation_id; } catch (err) { - submitError = err instanceof Error ? err.message : 'working directory create failed'; + submitError = exceptionDisplayError(err, 'working directory create failed'); } finally { creatingWorkingDirectory = false; } @@ -157,7 +170,7 @@ async function createWorker() { if (!workspaceId) { - submitError = 'workspace id is unavailable'; + submitError = { message: 'workspace id is unavailable', diagnostics: [] }; return; } @@ -179,32 +192,44 @@ })), }); 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; await loadWorkers(); window.location.href = payload.console_href; } catch (err) { - submitError = err instanceof Error ? err.message : 'worker create failed'; + submitError = exceptionDisplayError(err, 'worker create failed'); } finally { submitting = false; } } - async function responseErrorMessage(response: Response, fallback: string): Promise { + async function responseDisplayError(response: Response, fallback: string): Promise { 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) { - return `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`; + return { + message: `${payload.error.code ?? 'request_failed'}: ${payload.error.message}`, + diagnostics, + }; } if (payload.message) { const code = typeof payload.error === 'string' ? payload.error : 'request_failed'; - return `${code}: ${payload.message}`; + return { message: `${code}: ${payload.message}`, diagnostics }; } } catch { // 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: [], + }; } @@ -296,7 +321,19 @@

{optionsError}

{/if} {#if submitError} -

{submitError}

+
+

{submitError.message}

+ {#if submitError.diagnostics.length > 0} +
    + {#each submitError.diagnostics as diagnostic} +
  • + {diagnostic.code} + {diagnostic.message} +
  • + {/each} +
+ {/if} +
{/if}