From 3b6ed2764033efcd6628d76e416276919d38ac2e Mon Sep 17 00:00:00 2001
From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Date: Thu, 23 Jul 2026 20:55:14 -0400
Subject: [PATCH 1/3] fix(onboarding): show real install errors and fix
 concurrent install state
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Two bugs in the onboarding runtime setup screen and the Doctor settings
panel:

Bug A: RuntimeCard received the detailed install error string (step,
stderr, exit code, actionable hint) via installResults state but rendered
a hardcoded 'Installation couldn't be completed. Try again.' tooltip
instead. The rich string from getInstallErrorMessage was computed and
stored, then thrown away at render. Fix: pass installError as detail to
RuntimeErrorTooltip. Add whitespace-pre-line to the tooltip content span
so hint + step blocks render on separate lines.

Bug B: RuntimeProvidersSection (onboarding) and DoctorSettingsPanel
shared one useInstallAcpRuntimeMutation instance across all runtime
cards/rows. react-query v5 per-mutate callbacks (onSuccess/onError) only
fire for the latest mutate() call on a shared instance — starting install
B silently drops install A's result callback. Additionally, isPending and
mutation.variables tracked only the latest call, so card A's spinner
vanished as soon as card B's install started. Fix: move
useInstallAcpRuntimeMutation into each RuntimeCard / RuntimeRow so every
card owns its own mutation instance with independent isPending and
callbacks. RuntimeProvidersSection's shared installMutation and
handleInstall are removed; DoctorSettingsPanel's lifted installResults /
installingIds / handleInstall are replaced by per-row state.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
---
 .../onboarding/ui/RuntimeErrorTooltip.tsx     |   2 +-
 .../src/features/onboarding/ui/SetupStep.tsx  |  89 +++++++-------
 .../settings/ui/DoctorSettingsPanel.tsx       | 114 ++++++------------
 desktop/src/shared/lib/installError.test.mjs  |  44 +++++++
 4 files changed, 129 insertions(+), 120 deletions(-)

diff --git a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
index 15708d3087..afaf475ec6 100644
--- a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
+++ b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
@@ -42,7 +42,7 @@ export function RuntimeErrorTooltip({
         side="bottom"
         sideOffset={12}
       >
-        <span className="leading-4">{detail}</span>
+        <span className="whitespace-pre-line leading-4">{detail}</span>
       </TooltipContent>
     </Tooltip>
   );
diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx
index 96e7322d14..aa02486706 100644
--- a/desktop/src/features/onboarding/ui/SetupStep.tsx
+++ b/desktop/src/features/onboarding/ui/SetupStep.tsx
@@ -463,19 +463,56 @@ function RuntimeAuthError({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
 }
 
 function RuntimeCard({
-  installError,
-  isInstalling,
-  onInstall,
+  installResults,
+  onInstallResultsChange,
   runtime,
 }: {
-  installError: string | null;
-  isInstalling: boolean;
-  onInstall: () => void;
+  installResults: InstallResultsState;
+  onInstallResultsChange: React.Dispatch<
+    React.SetStateAction<InstallResultsState>
+  >;
   runtime: AcpRuntimeCatalogEntry;
 }) {
+  // Each card owns its own mutation instance so concurrent installs on
+  // different cards each track their own isPending state and callbacks
+  // independently (react-query v5 per-mutate callbacks only fire for the
+  // latest mutate() call on a shared instance, silently dropping earlier ones).
+  const installMutation = useInstallAcpRuntimeMutation();
+  const installError = installResults[runtime.id]?.error ?? null;
+  const isInstalling = installMutation.isPending;
   const isAvailable = runtime.availability === "available";
   const isReady = runtimeIsReadyForOnboarding(runtime);
 
+  function handleInstall() {
+    onInstallResultsChange((current) => ({
+      ...current,
+      [runtime.id]: { error: null, success: false },
+    }));
+
+    installMutation.mutate(runtime.id, {
+      onSuccess: (result) => {
+        onInstallResultsChange((current) => ({
+          ...current,
+          [runtime.id]: result.success
+            ? { error: null, success: true }
+            : {
+                error: getInstallErrorMessage(result.steps),
+                success: false,
+              },
+        }));
+      },
+      onError: (error) => {
+        onInstallResultsChange((current) => ({
+          ...current,
+          [runtime.id]: {
+            error: error instanceof Error ? error.message : "Install failed.",
+            success: false,
+          },
+        }));
+      },
+    });
+  }
+
   return (
     <Card
       className={cn(
@@ -499,7 +536,7 @@ function RuntimeCard({
         <RuntimeStatus
           installError={installError}
           isInstalling={isInstalling}
-          onInstall={onInstall}
+          onInstall={handleInstall}
           runtime={runtime}
         />
         {!isAvailable && runtimeDetailText(runtime) ? (
@@ -517,7 +554,7 @@ function RuntimeCard({
       {installError ? (
         <RuntimeErrorTooltip
           className="absolute inset-x-3 bottom-2 flex min-w-0 items-center justify-center gap-1.5 overflow-hidden whitespace-nowrap text-xs leading-4 text-destructive"
-          detail="Installation couldn’t be completed. Try again."
+          detail={installError}
           label="Installation failed"
           showIcon
           testId={`onboarding-runtime-error-${runtime.id}`}
@@ -560,34 +597,6 @@ function RuntimeProvidersSection({
 }) {
   const { errorMessage, isChecking, items } = runtimeProviders;
   const orderedItems = getVisibleOnboardingRuntimes(items);
-  const installMutation = useInstallAcpRuntimeMutation();
-
-  function handleInstall(runtimeId: string) {
-    onInstallResultsChange((current) => ({
-      ...current,
-      [runtimeId]: { error: null, success: false },
-    }));
-
-    installMutation.mutate(runtimeId, {
-      onSuccess: (result) => {
-        onInstallResultsChange((current) => ({
-          ...current,
-          [runtimeId]: result.success
-            ? { error: null, success: true }
-            : { error: getInstallErrorMessage(result.steps), success: false },
-        }));
-      },
-      onError: (error) => {
-        onInstallResultsChange((current) => ({
-          ...current,
-          [runtimeId]: {
-            error: error instanceof Error ? error.message : "Install failed.",
-            success: false,
-          },
-        }));
-      },
-    });
-  }
 
   return (
     <section className="flex min-h-full w-full flex-col items-center">
@@ -606,13 +615,9 @@ function RuntimeProvidersSection({
           <div className="grid min-w-0 w-full max-w-[592px] grid-cols-1 gap-4 md:grid-cols-2">
             {orderedItems.map((runtime) => (
               <RuntimeCard
-                installError={installResults[runtime.id]?.error ?? null}
-                isInstalling={
-                  installMutation.isPending &&
-                  installMutation.variables === runtime.id
-                }
+                installResults={installResults}
                 key={runtime.id}
-                onInstall={() => handleInstall(runtime.id)}
+                onInstallResultsChange={onInstallResultsChange}
                 runtime={runtime}
               />
             ))}
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index cee156ece7..f7a6b2141b 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -278,23 +278,44 @@ function RuntimeHeader({
   );
 }
 
-function RuntimeRow({
-  installError,
-  installSuccess,
-  isInstalling,
-  onInstall,
-  runtime,
-}: {
-  installError: string | null;
-  installSuccess: boolean;
-  isInstalling: boolean;
-  onInstall: () => void;
-  runtime: AcpRuntimeCatalogEntry;
-}) {
+function RuntimeRow({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
   const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState<
     string | null
   >(null);
   const [isUpdateWarningOpen, setIsUpdateWarningOpen] = React.useState(false);
+  // Each row owns its mutation instance so concurrent installs each track
+  // their own isPending / result state independently.
+  const installMutation = useInstallAcpRuntimeMutation();
+  const [installResult, setInstallResult] = React.useState<{
+    success: boolean;
+    error: string | null;
+  } | null>(null);
+  const isInstalling = installMutation.isPending;
+  const installError = installResult?.error ?? null;
+  const installSuccess = installResult?.success ?? false;
+
+  function handleInstall() {
+    setInstallResult(null);
+    installMutation.mutate(runtime.id, {
+      onSuccess: (result) => {
+        if (result.success) {
+          setInstallResult({ success: true, error: null });
+        } else {
+          setInstallResult({
+            success: false,
+            error: getInstallErrorMessage(result.steps),
+          });
+        }
+      },
+      onError: (error) => {
+        setInstallResult({
+          success: false,
+          error: error instanceof Error ? error.message : "Install failed.",
+        });
+      },
+    });
+  }
+
   const canConnectAccount =
     runtime.availability === "available" &&
     runtime.authStatus.status === "logged_out";
@@ -352,7 +373,7 @@ function RuntimeRow({
               setIsUpdateWarningOpen(true);
               return;
             }
-            onInstall();
+            handleInstall();
           }}
           runtime={runtime}
         />
@@ -410,7 +431,7 @@ function RuntimeRow({
           <AlertDialogFooter>
             <AlertDialogCancel>Cancel</AlertDialogCancel>
             <AlertDialogAction
-              onClick={onInstall}
+              onClick={handleInstall}
               data-testid={`doctor-runtime-confirm-update-${runtime.id}`}
             >
               Update
@@ -491,59 +512,6 @@ export function DoctorSettingsPanel() {
     [runtimesQuery.data],
   );
   const isRefreshing = runtimesQuery.isFetching;
-  const installMutation = useInstallAcpRuntimeMutation();
-  const [installResults, setInstallResults] = React.useState<
-    Record<string, { success: boolean; error: string | null }>
-  >({});
-  // Per-runtime installing state: tracks which runtime IDs have an in-flight
-  // install so concurrent installs each show their own spinner correctly.
-  const [installingIds, setInstallingIds] = React.useState<Set<string>>(
-    new Set(),
-  );
-
-  function handleInstall(runtimeId: string) {
-    // Clear any previous result for this runtime before retrying.
-    setInstallResults((prev) => ({
-      ...prev,
-      [runtimeId]: { success: false, error: null },
-    }));
-    setInstallingIds((prev) => new Set(prev).add(runtimeId));
-
-    installMutation.mutate(runtimeId, {
-      onSuccess: (result) => {
-        if (result.success) {
-          setInstallResults((prev) => ({
-            ...prev,
-            [runtimeId]: { success: true, error: null },
-          }));
-        } else {
-          setInstallResults((prev) => ({
-            ...prev,
-            [runtimeId]: {
-              success: false,
-              error: getInstallErrorMessage(result.steps),
-            },
-          }));
-        }
-      },
-      onError: (error) => {
-        setInstallResults((prev) => ({
-          ...prev,
-          [runtimeId]: {
-            success: false,
-            error: error instanceof Error ? error.message : "Install failed.",
-          },
-        }));
-      },
-      onSettled: () => {
-        setInstallingIds((prev) => {
-          const next = new Set(prev);
-          next.delete(runtimeId);
-          return next;
-        });
-      },
-    });
-  }
 
   return (
     <section
@@ -558,7 +526,6 @@ export function DoctorSettingsPanel() {
           <Button
             disabled={isRefreshing}
             onClick={() => {
-              setInstallResults({});
               void runtimesQuery.refetch();
               void gitBashQuery.refetch();
             }}
@@ -597,14 +564,7 @@ export function DoctorSettingsPanel() {
           ) : runtimes.length > 0 ? (
             <div className="space-y-3" data-testid="doctor-runtime-list">
               {runtimes.map((runtime) => (
-                <RuntimeRow
-                  installError={installResults[runtime.id]?.error ?? null}
-                  installSuccess={installResults[runtime.id]?.success ?? false}
-                  isInstalling={installingIds.has(runtime.id)}
-                  key={runtime.id}
-                  onInstall={() => handleInstall(runtime.id)}
-                  runtime={runtime}
-                />
+                <RuntimeRow key={runtime.id} runtime={runtime} />
               ))}
             </div>
           ) : (
diff --git a/desktop/src/shared/lib/installError.test.mjs b/desktop/src/shared/lib/installError.test.mjs
index 232ac178cc..c6b51186a8 100644
--- a/desktop/src/shared/lib/installError.test.mjs
+++ b/desktop/src/shared/lib/installError.test.mjs
@@ -67,3 +67,47 @@ test("getInstallErrorMessage: failed step with empty stderr falls back to stdout
   ]);
   assert.match(message, /some stdout output/);
 });
+
+test("getInstallErrorMessage: hint and step detail are separated by double newline for whitespace-pre-line rendering", () => {
+  const hint = "Git Bash is required. Install it from git-scm.com.";
+  const message = getInstallErrorMessage([
+    {
+      step: "shell",
+      command: "bash -l -c 'npm install'",
+      success: false,
+      stdout: "",
+      stderr: "bash: command not found",
+      exitCode: 127,
+      hint,
+    },
+  ]);
+  assert.ok(
+    message.includes("\n\n"),
+    "hint and step detail should be separated by a blank line",
+  );
+  assert.ok(message.startsWith(hint));
+});
+
+test("getInstallErrorMessage: only reports the last (failing) step when multiple steps present", () => {
+  const message = getInstallErrorMessage([
+    {
+      step: "node",
+      command: "node --version",
+      success: true,
+      stdout: "v20.0.0",
+      stderr: "",
+      exitCode: 0,
+    },
+    {
+      step: "adapter",
+      command: "npm install -g @agentclientprotocol/claude-code-acp",
+      success: false,
+      stdout: "",
+      stderr: "npm ERR! code E404",
+      exitCode: 1,
+    },
+  ]);
+  assert.match(message, /Step "adapter" failed:/);
+  assert.match(message, /npm ERR! code E404/);
+  assert.doesNotMatch(message, /Step "node"/);
+});

From 6a90f30cf429051565406122278f16a55a079a4d Mon Sep 17 00:00:00 2001
From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Date: Thu, 23 Jul 2026 21:42:03 -0400
Subject: [PATCH 2/3] fix(onboarding): address Thufir pass-1 findings
 (F1/F2/F3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

F1 (Doctor stale error): add resetEpoch prop to RuntimeRow; DoctorSettingsPanel
bumps epoch on Check again; RuntimeRow useEffect([resetEpoch]) clears local
installResult so stale failures are gone after a catalog refetch.

F2 (no concurrent regression test): extend e2eBridge with installAcpRuntimeByRuntime
config supporting per-runtime delay/result/call-sequences with independent
counters. Add Doctor concurrent + stale-clear E2E test (08) and onboarding
concurrent E2E test (both-fail-one-succeeds with rich multiline tooltip).

F3 (tooltip visually clipped): RuntimeErrorTooltip content now has
max-h-48 overflow-y-auto break-words in addition to whitespace-pre-line,
bounding tall/wide stderr output.

Doctor test assertion: codex success banner (installSuccess && availability
!= 'available') races with the catalog refresh that immediately returns
availability 'available'. Assert codexToggle.toBeChecked() instead — stable
whether isOn is driven by installSuccess or isAvailable.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
---
 .../onboarding/ui/RuntimeErrorTooltip.tsx     |   4 +-
 .../settings/ui/DoctorSettingsPanel.tsx       |  30 +++-
 desktop/src/testing/e2eBridge.ts              |  39 ++++++
 desktop/tests/e2e/doctor-states.spec.ts       | 129 ++++++++++++++++++
 .../e2e/onboarding-agent-defaults.spec.ts     | 113 +++++++++++++++
 5 files changed, 311 insertions(+), 4 deletions(-)

diff --git a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
index afaf475ec6..34d7035f5f 100644
--- a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
+++ b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
@@ -42,7 +42,9 @@ export function RuntimeErrorTooltip({
         side="bottom"
         sideOffset={12}
       >
-        <span className="whitespace-pre-line leading-4">{detail}</span>
+        <span className="block max-h-48 overflow-y-auto break-words whitespace-pre-line leading-4">
+          {detail}
+        </span>
       </TooltipContent>
     </Tooltip>
   );
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index f7a6b2141b..7a0186a7ba 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -278,7 +278,13 @@ function RuntimeHeader({
   );
 }
 
-function RuntimeRow({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
+function RuntimeRow({
+  resetEpoch,
+  runtime,
+}: {
+  resetEpoch: number;
+  runtime: AcpRuntimeCatalogEntry;
+}) {
   const [terminalLaunchMethodId, setTerminalLaunchMethodId] = React.useState<
     string | null
   >(null);
@@ -290,6 +296,13 @@ function RuntimeRow({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
     success: boolean;
     error: string | null;
   } | null>(null);
+  // Clear stale install results when the parent triggers a catalog refresh
+  // (Check again) — the runtime may now be healthy and stale failure state
+  // would linger because keyed rows don't remount on refetch.
+  // biome-ignore lint/correctness/useExhaustiveDependencies: resetEpoch is an intentional trigger only; its value is not consumed in the effect body
+  React.useEffect(() => {
+    setInstallResult(null);
+  }, [resetEpoch]);
   const isInstalling = installMutation.isPending;
   const installError = installResult?.error ?? null;
   const installSuccess = installResult?.success ?? false;
@@ -393,7 +406,10 @@ function RuntimeRow({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
           </p>
         ) : null}
         {installError ? (
-          <p className="mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive">
+          <p
+            className="mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive"
+            data-testid={`doctor-runtime-install-error-${runtime.id}`}
+          >
             {installError}
           </p>
         ) : null}
@@ -512,6 +528,9 @@ export function DoctorSettingsPanel() {
     [runtimesQuery.data],
   );
   const isRefreshing = runtimesQuery.isFetching;
+  // Incremented each time the user clicks "Check again" so RuntimeRow
+  // useEffect clears stale install results from before the refresh.
+  const [resetEpoch, setResetEpoch] = React.useState(0);
 
   return (
     <section
@@ -526,6 +545,7 @@ export function DoctorSettingsPanel() {
           <Button
             disabled={isRefreshing}
             onClick={() => {
+              setResetEpoch((e) => e + 1);
               void runtimesQuery.refetch();
               void gitBashQuery.refetch();
             }}
@@ -564,7 +584,11 @@ export function DoctorSettingsPanel() {
           ) : runtimes.length > 0 ? (
             <div className="space-y-3" data-testid="doctor-runtime-list">
               {runtimes.map((runtime) => (
-                <RuntimeRow key={runtime.id} runtime={runtime} />
+                <RuntimeRow
+                  key={runtime.id}
+                  resetEpoch={resetEpoch}
+                  runtime={runtime}
+                />
               ))}
             </div>
           ) : (
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 9ad4059a05..aa841d2765 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -185,6 +185,18 @@ type E2eConfig = {
      *  Call N returns results[N]; when exhausted the last entry repeats.
      *  Takes precedence over `installAcpRuntimeResult`. */
     installAcpRuntimeResults?: RawInstallRuntimeResult[];
+    /** Per-runtime install configuration keyed by runtimeId.
+     *  When a runtimeId matches, its entry overrides the global
+     *  installAcpRuntime* fields for that specific runtime. */
+    installAcpRuntimeByRuntime?: Record<
+      string,
+      {
+        delayMs?: number;
+        result?: RawInstallRuntimeResult;
+        /** Call-order sequence — same semantics as installAcpRuntimeResults. */
+        results?: RawInstallRuntimeResult[];
+      }
+    >;
     managedAgentPrereqs?: {
       acp?: MockCommandAvailability;
       mcp?: MockCommandAvailability;
@@ -7011,6 +7023,8 @@ async function handleConnectAcpRuntime(
 // Per-page install call counter. Reset each test run because this module is
 // re-evaluated via addInitScript, so the counter starts at 0 for every test.
 let installCallCount = 0;
+/** Per-runtime call counters for `installAcpRuntimeByRuntime` sequences. */
+const installCallCountByRuntime: Record<string, number> = {};
 let addChannelMembersCallCount = 0;
 let mockGlobalAgentConfig: {
   env_vars: Record<string, string>;
@@ -7031,6 +7045,31 @@ async function handleInstallAcpRuntime(
   },
   config: E2eConfig | undefined,
 ): Promise<RawInstallRuntimeResult> {
+  const runtimeId = args.runtimeId ?? "";
+  const perRuntime = config?.mock?.installAcpRuntimeByRuntime?.[runtimeId];
+
+  if (perRuntime) {
+    const delayMs = perRuntime.delayMs ?? 0;
+    if (delayMs > 0) {
+      await new Promise((resolve) => window.setTimeout(resolve, delayMs));
+    }
+    const seq = perRuntime.results;
+    if (seq && seq.length > 0) {
+      const idx = Math.min(
+        installCallCountByRuntime[runtimeId] ?? 0,
+        seq.length - 1,
+      );
+      installCallCountByRuntime[runtimeId] = idx + 1;
+      const result = seq[idx];
+      if (result.success) mockInstallCompleted = true;
+      return result;
+    }
+    if (perRuntime.result) {
+      if (perRuntime.result.success) mockInstallCompleted = true;
+      return perRuntime.result;
+    }
+  }
+
   const delayMs = config?.mock?.installAcpRuntimeDelayMs ?? 0;
   if (delayMs > 0) {
     await new Promise((resolve) => window.setTimeout(resolve, delayMs));
diff --git a/desktop/tests/e2e/doctor-states.spec.ts b/desktop/tests/e2e/doctor-states.spec.ts
index 1a724f719a..dd2cf83286 100644
--- a/desktop/tests/e2e/doctor-states.spec.ts
+++ b/desktop/tests/e2e/doctor-states.spec.ts
@@ -735,4 +735,133 @@ test.describe("Doctor panel state screenshots", () => {
     await expect(loading).toBeVisible();
     await expect(loading).toContainText("Codex installing");
   });
+
+  /**
+   * 08 — concurrent installs each keep their own spinner/result state;
+   *      stale install failure is cleared when Check again fires (F1 fix).
+   *
+   * Flow:
+   *  - Claude (400ms delay) → failure
+   *  - Codex  (100ms delay) → success
+   *  Both started before either settles.
+   *  After both settle: claude shows failure, codex shows success banner.
+   *  Click Check again → both rows lose stale state (claude error gone).
+   */
+  test("08-concurrent-installs-and-stale-clear", async ({ page }) => {
+    await installMockBridge(page, {
+      acpRuntimesCatalog: [
+        {
+          ...CLAUDE_AVAILABLE_LOGGED_IN,
+          availability: "adapter_missing",
+          command: null,
+          binary_path: null,
+          can_auto_install: true,
+          auth_status: { status: "unknown" },
+        },
+        {
+          ...CODEX_NOT_INSTALLED,
+          can_auto_install: true,
+          node_required: false,
+        },
+        GOOSE_AVAILABLE,
+        BUZZ_AGENT_AVAILABLE,
+      ],
+      installAcpRuntimeByRuntime: {
+        claude: {
+          delayMs: 400,
+          result: {
+            success: false,
+            steps: [
+              {
+                step: "adapter",
+                command: "npm install -g @agentclientprotocol/claude-agent-acp",
+                success: false,
+                stdout: "",
+                stderr:
+                  "npm ERR! code EACCES\nnpm ERR! syscall mkdir\nnpm ERR! path /usr/local\n\nHint: Check prefix permissions.",
+                exit_code: 1,
+              },
+            ],
+          },
+        },
+        codex: {
+          delayMs: 100,
+          result: {
+            success: true,
+            steps: [
+              {
+                step: "adapter",
+                command: "npm install -g @zed-industries/codex-acp",
+                success: true,
+                stdout: "added 1 package",
+                stderr: "",
+                exit_code: 0,
+              },
+            ],
+          },
+        },
+      },
+      // After the catalog refresh (triggered by a successful install or Check
+      // again), all runtimes report healthy so stale errors must clear.
+      acpRuntimesCatalogAfterInstall: [
+        {
+          ...CLAUDE_AVAILABLE_LOGGED_IN,
+          availability: "available",
+        },
+        {
+          ...CODEX_NOT_INSTALLED,
+          availability: "available",
+          command: "codex-acp",
+          binary_path: "/usr/local/bin/codex-acp",
+          auth_status: { status: "logged_in" },
+        },
+        GOOSE_AVAILABLE,
+        BUZZ_AGENT_AVAILABLE,
+      ],
+    });
+
+    await page.goto("/", { waitUntil: "domcontentloaded" });
+    await openSettings(page, "agents");
+
+    const claudeRow = page.getByTestId("doctor-runtime-claude");
+    const codexRow = page.getByTestId("doctor-runtime-codex");
+    await expect(claudeRow).toBeVisible({ timeout: 10_000 });
+    await expect(codexRow).toBeVisible();
+
+    const claudeToggle = page.getByTestId("doctor-runtime-toggle-claude");
+    const codexToggle = page.getByTestId("doctor-runtime-toggle-codex");
+
+    // Start both installs before either settles.
+    await claudeToggle.click();
+    await codexToggle.click();
+
+    // Codex settles first (shorter delay): toggle flips on, no error on codex.
+    // The catalog refresh triggered by codex's success immediately returns
+    // availability === "available", so the transient "installed. Checking..."
+    // banner is replaced by the stable isOn state — assert the toggle instead.
+    await expect(codexToggle).toBeChecked({ timeout: 3_000 });
+    await expect(
+      page.getByTestId("doctor-runtime-install-error-codex"),
+    ).toHaveCount(0);
+
+    // Claude settles (after its longer delay): failure error visible with
+    // multiline stderr. Codex toggle must still be on — unaffected by claude.
+    const claudeError = page.getByTestId("doctor-runtime-install-error-claude");
+    await expect(claudeError).toBeVisible({ timeout: 3_000 });
+    await expect(claudeError).toContainText("npm ERR!");
+    await expect(codexToggle).toBeChecked();
+
+    // Click Check again — epoch increments, RuntimeRow useEffect clears
+    // local installResult state, so the stale claude error disappears.
+    await page.getByRole("button", { name: "Check again" }).click();
+    await expect(claudeError).toHaveCount(0, { timeout: 5_000 });
+    // Codex toggle stays on (catalog still reports available after refresh).
+    await expect(codexToggle).toBeChecked({ timeout: 5_000 });
+
+    await claudeRow.scrollIntoViewIfNeeded();
+    await waitForAnimations(page);
+    await claudeRow.screenshot({
+      path: `${SHOTS}/08-concurrent-installs-and-stale-clear.png`,
+    });
+  });
 });
diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
index f8e6322ed3..503c93721c 100644
--- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
+++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
@@ -635,3 +635,116 @@ test("defaults requires a choice when multiple visible harnesses are ready", asy
   await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
   await expect.poll(() => readSavedRuntime(page)).toBe("codex");
 });
+
+/**
+ * Two installs started concurrently — claude fails with a multiline error
+ * (rich hint+stderr in the tooltip) while codex succeeds. Each card must
+ * keep its own independent spinner and its own terminal result; neither card
+ * may show the other's outcome.
+ *
+ * This is the behavioral regression test for the per-card mutation fix
+ * (Bug B) and the multiline tooltip fix (Bug A / F3 from Thufir pass 1).
+ */
+test("concurrent installs each keep their own state — one fails, one succeeds", async ({
+  page,
+}) => {
+  const multilineError =
+    "npm ERR! code EACCES\nnpm ERR! syscall mkdir\nnpm ERR! path /usr/local/lib/node_modules\n\nHint: Check your npm prefix permissions and try again.";
+  const claudeNotInstalled = runtime("claude", "adapter_missing", {
+    status: "unknown",
+  });
+  const codexNotInstalled = runtime("codex", "adapter_missing", {
+    status: "unknown",
+  });
+  await installMockBridge(
+    page,
+    {
+      acpRuntimesCatalog: [claudeNotInstalled, codexNotInstalled],
+      // Claude: long delay then failure with multiline stderr + hint.
+      // Codex: short delay then success.
+      // Per-runtime config lets both be in flight simultaneously.
+      installAcpRuntimeByRuntime: {
+        claude: {
+          delayMs: 600,
+          result: {
+            success: false,
+            steps: [
+              {
+                step: "adapter",
+                command: "npm install -g @agentclientprotocol/claude-agent-acp",
+                success: false,
+                stdout: "",
+                stderr: multilineError,
+                exit_code: 1,
+              },
+            ],
+          },
+        },
+        codex: {
+          delayMs: 200,
+          result: {
+            success: true,
+            steps: [
+              {
+                step: "adapter",
+                command: "npm install -g @zed-industries/codex-acp",
+                success: true,
+                stdout: "added 1 package",
+                stderr: "",
+                exit_code: 0,
+              },
+            ],
+          },
+        },
+      },
+      acpRuntimesCatalogAfterInstall: [
+        runtime("claude", "adapter_missing", { status: "unknown" }),
+        runtime("codex", "available", { status: "logged_in" }),
+      ],
+    },
+    { skipCommunitySeed: true, skipOnboardingSeed: true },
+  );
+  await page.goto("/");
+  await navigateToSetupPage(page);
+
+  const claudeInstall = page.getByTestId("onboarding-runtime-install-claude");
+  const codexInstall = page.getByTestId("onboarding-runtime-install-codex");
+
+  // Start both installs before either settles.
+  await claudeInstall.click();
+  await codexInstall.click();
+
+  // While in flight: both install buttons must be absent (no duplicate clicks).
+  await expect(claudeInstall).toHaveCount(0);
+  await expect(codexInstall).toHaveCount(0);
+
+  // Codex settles first (shorter delay): success indicator, no error.
+  await expect(page.getByTestId("onboarding-runtime-ready-codex")).toBeVisible({
+    timeout: 3_000,
+  });
+  await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
+    0,
+  );
+
+  // Claude still in flight: its install button must still be absent.
+  await expect(claudeInstall).toHaveCount(0);
+
+  // Claude settles: failure error visible; codex still shows ready (not reset).
+  const claudeError = page.getByTestId("onboarding-runtime-error-claude");
+  await expect(claudeError).toBeVisible({ timeout: 3_000 });
+  await expect(
+    page.getByTestId("onboarding-runtime-ready-codex"),
+  ).toBeVisible();
+  await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
+    0,
+  );
+
+  // The error trigger has the full aria-label (label + detail).
+  await expect(claudeError).toHaveAttribute("aria-label", /npm ERR!/);
+  // Open the tooltip and verify multiline content is visible (F3 fix).
+  await claudeError.focus();
+  const tooltip = page.getByRole("tooltip");
+  await expect(tooltip).toBeVisible({ timeout: 2_000 });
+  await expect(tooltip).toContainText("npm ERR! code EACCES");
+  await expect(tooltip).toContainText("Hint: Check your npm prefix");
+});

From 771f3f099bfb92ffac4d950ca3493e9805a0a7cd Mon Sep 17 00:00:00 2001
From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Date: Thu, 23 Jul 2026 22:36:39 -0400
Subject: [PATCH 3/3] fix(onboarding): make F3 overflow test discriminating
 against pre-fix CSS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The previous tooltip E2E test fed a short 5-line payload and used
toContainText() — passes with or without max-h-48/break-words.

Replace fixture with a 19-line realistic Windows EACCES error including
a long unbroken path (C:\Users\...\claude-agent-acp.exe) that exercises
both overflow axes. Add overflow dimension assertions on the detail span:

  • scrollHeight > clientHeight: proves max-h-48 + overflow-y-auto are active
    (without them, tall content has no scroll container; assertion fails)
  • scrollTop can advance: proves the span actually scrolls
  • scrollWidth <= clientWidth: proves break-words prevents horizontal overflow
    (verified red-on-old: removing break-words causes this assertion to fail)

Also add overflow-x-hidden to RuntimeErrorTooltip's detail span so that
overflow-y: auto and overflow-x: hidden together make the scroll container
unambiguous on both axes. Locator uses page.locator().first() because
Radix tooltip portals place a visible copy and a hidden duplicate in the DOM.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
---
 .../onboarding/ui/RuntimeErrorTooltip.tsx     |  2 +-
 .../e2e/onboarding-agent-defaults.spec.ts     | 62 +++++++++++++++++--
 2 files changed, 59 insertions(+), 5 deletions(-)

diff --git a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
index 34d7035f5f..6a3d8ef319 100644
--- a/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
+++ b/desktop/src/features/onboarding/ui/RuntimeErrorTooltip.tsx
@@ -42,7 +42,7 @@ export function RuntimeErrorTooltip({
         side="bottom"
         sideOffset={12}
       >
-        <span className="block max-h-48 overflow-y-auto break-words whitespace-pre-line leading-4">
+        <span className="block max-h-48 overflow-y-auto overflow-x-hidden break-words whitespace-pre-line leading-4">
           {detail}
         </span>
       </TooltipContent>
diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
index 503c93721c..e1d8b11b6f 100644
--- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
+++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
@@ -648,8 +648,34 @@ test("defaults requires a choice when multiple visible harnesses are ready", asy
 test("concurrent installs each keep their own state — one fails, one succeeds", async ({
   page,
 }) => {
-  const multilineError =
-    "npm ERR! code EACCES\nnpm ERR! syscall mkdir\nnpm ERR! path /usr/local/lib/node_modules\n\nHint: Check your npm prefix permissions and try again.";
+  // Realistic 512-head + 1024-tail shape: many short lines followed by one
+  // long unbroken Windows path.  This exercises both overflow axes:
+  //   • vertical: enough lines to exceed max-h-48 (192px at ~16px/line)
+  //   • horizontal: the long path has no spaces, so only break-words prevents
+  //     scrollWidth > clientWidth.
+  const longWindowsPath =
+    "C:\\Users\\willp\\AppData\\Roaming\\npm\\node_modules\\@agentclientprotocol\\claude-agent-acp\\dist\\bin\\claude-agent-acp.exe";
+  const multilineError = [
+    "npm ERR! code EACCES",
+    "npm ERR! syscall mkdir",
+    "npm ERR! path C:\\Users\\willp\\AppData\\Roaming\\npm",
+    "npm ERR! errno -4048",
+    "npm ERR! Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm'",
+    "npm ERR!  { [Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm']",
+    "npm ERR!   errno: -4048,",
+    "npm ERR!   code: 'EACCES',",
+    "npm ERR!   syscall: 'mkdir',",
+    "npm ERR!   path: 'C:\\\\Users\\\\willp\\\\AppData\\\\Roaming\\\\npm' }",
+    "npm ERR!",
+    "npm ERR! The operation was rejected by your operating system.",
+    "npm ERR! It is likely you do not have the permissions to access this file as the current user",
+    "npm ERR!",
+    `npm ERR! If you believe this might be a permissions issue, please double-check the`,
+    `npm ERR! permissions of the file and its containing directories, or try running`,
+    `npm ERR! the command again as root/Administrator.`,
+    "",
+    `Hint: Run as Administrator or change npm prefix: npm config set prefix ${longWindowsPath}`,
+  ].join("\n");
   const claudeNotInstalled = runtime("claude", "adapter_missing", {
     status: "unknown",
   });
@@ -741,10 +767,38 @@ test("concurrent installs each keep their own state — one fails, one succeeds"
 
   // The error trigger has the full aria-label (label + detail).
   await expect(claudeError).toHaveAttribute("aria-label", /npm ERR!/);
-  // Open the tooltip and verify multiline content is visible (F3 fix).
+  // Open the tooltip and verify the detail span handles overflow correctly:
+  //   • vertical overflow exists and is scrollable (max-h-48 + overflow-y-auto)
+  //   • no horizontal overflow (break-words forces the long unbroken path to wrap)
   await claudeError.focus();
   const tooltip = page.getByRole("tooltip");
   await expect(tooltip).toBeVisible({ timeout: 2_000 });
   await expect(tooltip).toContainText("npm ERR! code EACCES");
-  await expect(tooltip).toContainText("Hint: Check your npm prefix");
+  await expect(tooltip).toContainText("Hint: Run as Administrator");
+
+  // Locate the scroll container using page-level locator since Radix portals
+  // can place content outside the tooltip role element's subtree in the DOM.
+  // Use .first() because Radix keeps a hidden duplicate in the light DOM.
+  const detailSpan = page.locator("span.overflow-y-auto").first();
+  await expect(detailSpan).toBeVisible();
+
+  // Vertical: scrollHeight must exceed clientHeight (content taller than max-h-48).
+  // Scroll position must advance when set, proving scrollability.
+  const isVerticallyScrollable = await detailSpan.evaluate((el) => {
+    return el.scrollHeight > el.clientHeight;
+  });
+  expect(isVerticallyScrollable).toBe(true);
+
+  // Confirm scroll position can actually advance.
+  await detailSpan.evaluate((el) => {
+    el.scrollTop = 9999;
+  });
+  const scrolledDown = await detailSpan.evaluate((el) => el.scrollTop > 0);
+  expect(scrolledDown).toBe(true);
+
+  // Horizontal: break-words must prevent horizontal overflow.
+  const hasHorizontalOverflow = await detailSpan.evaluate((el) => {
+    return el.scrollWidth > el.clientWidth;
+  });
+  expect(hasHorizontalOverflow).toBe(false);
 });
