From 30300be52d69aa509e9583577edfbb2f767bfa72 Mon Sep 17 00:00:00 2001
From: Samuel K <skevetter@pm.me>
Date: Sat, 6 Jun 2026 08:33:34 -0500
Subject: [PATCH] fix: honor platform override on image pull and clarify wizard
 compat UX
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The desktop wizard's image-compatibility check ran but its result was
invisible unless the image was incompatible, and the emulation override
never worked: EnsureImage pulled without a platform flag, so an
amd64-only image on an arm64 host failed at `docker pull` before
`docker run --platform` was ever reached.

- pkg/docker: refactor Pull to take a PullOptions config struct and
  thread Platform through to `docker pull --platform`
- pkg/driver/docker: pass RunOptions.Platform into the pull
- desktop wizard: review step now always resolves to a definite compat
  state (compatible / incompatible / unknown) instead of flashing
  "Checking…" and vanishing; list supported platforms
- desktop wizard: drop confusing "Auto-suggested / Resolved id" lines;
  align Ref Type dropdown height with the adjacent input
---
 .../WorkspaceWizard.platform.test.ts          | 36 ++++++++++-
 .../workspace/WorkspaceWizard.svelte          | 61 ++++++++++++++++---
 e2e/tests/up-docker-compose/helper.go         |  6 +-
 pkg/docker/helper.go                          | 33 ++++++----
 pkg/docker/helper_test.go                     | 38 ++++++++++++
 pkg/driver/docker/docker.go                   |  7 ++-
 6 files changed, 156 insertions(+), 25 deletions(-)

diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.platform.test.ts b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.platform.test.ts
index 86374b5a3..dc58080cd 100644
--- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.platform.test.ts
+++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.platform.test.ts
@@ -196,7 +196,22 @@ describe("WorkspaceWizard platform compatibility", () => {
     unmount()
   })
 
-  it("stays silent and still launches when the platform lookup rejects", async () => {
+  it("confirms compatibility and lists platforms for a multi-arch image", async () => {
+    getImagePlatforms.mockResolvedValue(["linux/amd64", "linux/arm64"])
+    const { getByText, unmount } = render(WorkspaceWizard, {
+      props: { open: true },
+    })
+    await flushAsync()
+    await gotoReviewWithImage(getByText, "ubuntu:22.04")
+
+    await waitFor(() =>
+      expect(getByText(/Compatible with your machine \(linux\/arm64\)/i)).toBeTruthy(),
+    )
+    expect(getByText(/linux\/amd64, linux\/arm64/i)).toBeTruthy()
+    unmount()
+  })
+
+  it("shows a neutral note and still launches when the platform lookup rejects", async () => {
     getImagePlatforms.mockRejectedValue(new Error("registry unreachable"))
     const { getByText, queryByText, unmount } = render(WorkspaceWizard, {
       props: { open: true },
@@ -204,7 +219,9 @@ describe("WorkspaceWizard platform compatibility", () => {
     await flushAsync()
     await gotoReviewWithImage(getByText, "ubuntu:22.04")
 
-    await flushAsync()
+    await waitFor(() =>
+      expect(getByText(/Couldn't verify compatibility/i)).toBeTruthy(),
+    )
     expect(queryByText(/no build for your machine/i)).toBeNull()
     expect(document.querySelector('input[type="checkbox"]')).toBeNull()
 
@@ -217,4 +234,19 @@ describe("WorkspaceWizard platform compatibility", () => {
     )
     unmount()
   })
+
+  it("shows a neutral note when the registry returns no platform data", async () => {
+    getImagePlatforms.mockResolvedValue([])
+    const { getByText, queryByText, unmount } = render(WorkspaceWizard, {
+      props: { open: true },
+    })
+    await flushAsync()
+    await gotoReviewWithImage(getByText, "ubuntu:22.04")
+
+    await waitFor(() =>
+      expect(getByText(/Couldn't verify compatibility/i)).toBeTruthy(),
+    )
+    expect(queryByText(/no build for your machine/i)).toBeNull()
+    unmount()
+  })
 })
diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
index 626b869b4..4a35ae28b 100644
--- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
+++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
@@ -212,6 +212,8 @@ let hostPlatform = $state("")
 let imagePlatforms = $state<string[]>([])
 let checkedRef = $state("")
 let compatLoading = $state(false)
+let compatChecked = $state(false)
+let compatLookupFailed = $state(false)
 let emulationEnabled = $state(false)
 
 let initializedProviders = $derived(
@@ -261,6 +263,22 @@ let emulationTarget = $derived(
   imagePlatforms.includes("linux/amd64") ? "linux/amd64" : imagePlatforms[0] ?? "",
 )
 
+let imageCompatible = $derived(
+  sourceType === "image" &&
+    compatChecked &&
+    hostPlatform !== "" &&
+    imagePlatforms.length > 0 &&
+    isImageCompatible(imagePlatforms, hostPlatform),
+)
+
+// Lookup resolved but produced nothing actionable: either it errored or the
+// registry returned no platform data, so we can't make a compatibility claim.
+let compatUnknown = $derived(
+  sourceType === "image" &&
+    compatChecked &&
+    (compatLookupFailed || imagePlatforms.length === 0 || hostPlatform === ""),
+)
+
 let stepStates = $derived.by(() => {
   const states: Record<Step, StepState> = {
     provider: "pending",
@@ -319,6 +337,8 @@ function reset() {
   checkedRef = ""
   imagePlatforms = []
   compatLoading = false
+  compatChecked = false
+  compatLookupFailed = false
   emulationEnabled = false
   clearWatchdog()
   unlisten?.()
@@ -342,6 +362,8 @@ $effect(() => {
     const ref = imageRef.trim()
     checkedRef = ref
     compatLoading = true
+    compatChecked = false
+    compatLookupFailed = false
     imagePlatforms = []
     emulationEnabled = false
     getImagePlatforms(ref)
@@ -353,9 +375,11 @@ $effect(() => {
         // renderer defect isn't silently masked as "image compatible".
         console.warn(`Image platform lookup failed for ${ref}:`, err)
         imagePlatforms = []
+        compatLookupFailed = true
       })
       .finally(() => {
         compatLoading = false
+        compatChecked = true
       })
   }
 })
@@ -577,7 +601,7 @@ function selectTemplate(t: { name: string; source: string }) {
             <div class="space-y-1.5">
               <Label class="text-sm">Ref Type</Label>
               <Select.Root type="single" bind:value={refType}>
-                <Select.Trigger class="w-full">
+                <Select.Trigger class="h-8 w-full rounded-lg">
                   {refType === "branch" ? "Branch" : refType === "commit" ? "Commit" : "Pull Request"}
                 </Select.Trigger>
                 <Select.Content>
@@ -897,12 +921,6 @@ function selectTemplate(t: { name: string; source: string }) {
               value={workspaceName}
               oninput={(e) => (workspaceName = e.currentTarget.value)}
             />
-            <p class="text-xs text-muted-foreground">
-              Auto-suggested to avoid conflicts. Edit to use a custom name.
-            </p>
-            <p class="text-xs text-muted-foreground">
-              Resolved id: <span class="font-mono">{resolvedId || "—"}</span>
-            </p>
           </div>
 
           {#if resolvedIdInvalid}
@@ -984,7 +1002,29 @@ function selectTemplate(t: { name: string; source: string }) {
           </div>
 
           {#if sourceType === "image" && compatLoading}
-            <p class="text-sm text-muted-foreground">Checking image compatibility…</p>
+            <p class="flex items-center gap-2 text-sm text-muted-foreground">
+              <Loader2 class="h-4 w-4 animate-spin" />
+              Checking image compatibility…
+            </p>
+          {/if}
+
+          {#if imageCompatible}
+            <p class="flex items-center gap-2 text-sm text-emerald-700 dark:text-emerald-400">
+              <Check class="h-4 w-4 shrink-0" />
+              <span>
+                Compatible with your machine ({hostPlatform})
+                <span class="block text-xs text-muted-foreground">
+                  Image supports {imagePlatforms.join(", ")}
+                </span>
+              </span>
+            </p>
+          {/if}
+
+          {#if compatUnknown}
+            <p class="text-sm text-muted-foreground">
+              Couldn't verify compatibility for your machine ({hostPlatform ||
+                "unknown"}); the image will be pulled as-is.
+            </p>
           {/if}
 
           {#if imageIncompatible}
@@ -993,6 +1033,11 @@ function selectTemplate(t: { name: string; source: string }) {
               <Alert.Description class="text-amber-700 dark:text-amber-400">
                 This image has no build for your machine ({hostPlatform}) and will
                 fail to start unless you run it under emulation below.
+                {#if imagePlatforms.length > 0}
+                  <span class="block text-xs">
+                    Image supports {imagePlatforms.join(", ")}.
+                  </span>
+                {/if}
               </Alert.Description>
             </Alert.Root>
 
diff --git a/e2e/tests/up-docker-compose/helper.go b/e2e/tests/up-docker-compose/helper.go
index 78c505cbc..257ddb4d6 100644
--- a/e2e/tests/up-docker-compose/helper.go
+++ b/e2e/tests/up-docker-compose/helper.go
@@ -69,7 +69,11 @@ func (btc *baseTestContext) resetTaggedImage(
 	ctx context.Context,
 	sourceImage, targetImage string,
 ) error {
-	if err := btc.dockerHelper.Pull(ctx, sourceImage, nil, io.Discard, io.Discard); err != nil {
+	if err := btc.dockerHelper.Pull(ctx, docker.PullOptions{
+		Image:  sourceImage,
+		Stdout: io.Discard,
+		Stderr: io.Discard,
+	}); err != nil {
 		return err
 	}
 	_ = btc.dockerHelper.Run(
diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go
index 9c8e04553..e72c3a020 100644
--- a/pkg/docker/helper.go
+++ b/pkg/docker/helper.go
@@ -140,17 +140,25 @@ func (r *DockerHelper) Stop(ctx context.Context, id string) error {
 	return nil
 }
 
-func (r *DockerHelper) Pull(
-	ctx context.Context,
-	image string,
-	stdin io.Reader,
-	stdout io.Writer,
-	stderr io.Writer,
-) error {
-	cmd := r.buildCmd(ctx, "pull", image)
-	cmd.Stdin = stdin
-	cmd.Stdout = stdout
-	cmd.Stderr = stderr
+// PullOptions configures an image pull.
+type PullOptions struct {
+	Image    string
+	Platform string
+	Stdin    io.Reader
+	Stdout   io.Writer
+	Stderr   io.Writer
+}
+
+func (r *DockerHelper) Pull(ctx context.Context, opts PullOptions) error {
+	args := []string{"pull"}
+	if opts.Platform != "" {
+		args = append(args, "--platform", opts.Platform)
+	}
+	args = append(args, opts.Image)
+	cmd := r.buildCmd(ctx, args...)
+	cmd.Stdin = opts.Stdin
+	cmd.Stdout = opts.Stdout
+	cmd.Stderr = opts.Stderr
 	return cmd.Run()
 }
 
@@ -416,8 +424,7 @@ func (r *DockerHelper) FindContainerJSON(ctx context.Context, labels []string) (
 		}
 
 		for _, label := range labels {
-			key := strings.Split(label, "=")[0]
-			value := strings.Join(strings.Split(label, "=")[1:], "=")
+			key, value, _ := strings.Cut(label, "=")
 
 			found = containers[0].Config.Labels[key] == value
 		}
diff --git a/pkg/docker/helper_test.go b/pkg/docker/helper_test.go
index 440cbcd7a..10103b5c3 100644
--- a/pkg/docker/helper_test.go
+++ b/pkg/docker/helper_test.go
@@ -1,8 +1,11 @@
 package docker
 
 import (
+	"context"
+	"io"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -81,6 +84,41 @@ esac
 	assert.False(t, got, "should not detect GPU when Podman CDI has no nvidia device")
 }
 
+func TestPull_PlatformArg(t *testing.T) {
+	tmp := t.TempDir()
+	argsFile := filepath.Join(tmp, "args.txt")
+	bin := writeScript(t, tmp, "docker-fake", `#!/bin/sh
+echo "$@" > `+argsFile+`
+`)
+
+	t.Run("includes --platform when set", func(t *testing.T) {
+		h := &DockerHelper{DockerCommand: bin}
+		require.NoError(t, h.Pull(context.Background(), PullOptions{
+			Image:    "ubuntu:22.04",
+			Platform: "linux/amd64",
+			Stdout:   io.Discard,
+			Stderr:   io.Discard,
+		}))
+		//nolint:gosec // test reads a temp file path it controls
+		got, err := os.ReadFile(argsFile)
+		require.NoError(t, err)
+		assert.Equal(t, "pull --platform linux/amd64 ubuntu:22.04", strings.TrimSpace(string(got)))
+	})
+
+	t.Run("omits --platform when empty", func(t *testing.T) {
+		h := &DockerHelper{DockerCommand: bin}
+		require.NoError(t, h.Pull(context.Background(), PullOptions{
+			Image:  "ubuntu:22.04",
+			Stdout: io.Discard,
+			Stderr: io.Discard,
+		}))
+		//nolint:gosec // test reads a temp file path it controls
+		got, err := os.ReadFile(argsFile)
+		require.NoError(t, err)
+		assert.Equal(t, "pull ubuntu:22.04", strings.TrimSpace(string(got)))
+	})
+}
+
 func TestGPUSupportEnabled_CommandFailure(t *testing.T) {
 	tmp := t.TempDir()
 	bin := writeScript(t, tmp, "bad-runtime", `#!/bin/sh
diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go
index 10af27ddd..155433d1b 100644
--- a/pkg/driver/docker/docker.go
+++ b/pkg/driver/docker/docker.go
@@ -327,7 +327,12 @@ func (d *dockerDriver) EnsureImage(
 		writer := log.Writer(log.LevelDebug)
 		defer func() { _ = writer.Close() }()
 
-		return d.Docker.Pull(ctx, options.Image, nil, writer, writer)
+		return d.Docker.Pull(ctx, docker.PullOptions{
+			Image:    options.Image,
+			Platform: options.Platform,
+			Stdout:   writer,
+			Stderr:   writer,
+		})
 	}
 	return nil
 }
