From 701156c41e06af62fa5187d8aaaaf0e45b2d50b8 Mon Sep 17 00:00:00 2001 From: Guilherme Barros Date: Mon, 17 Aug 2026 09:42:32 +0200 Subject: [PATCH 1/8] fix(preview): name hidden, disabled, and ambiguous click failures preview_click treated a hidden or disabled button as missing and leaked the locator when it failed. Agents then spent turns guessing at chrome they could already see. Clicks now report hidden, disabled, or ambiguous targets without putting the locator on the wire. --- apps/desktop/src/preview/Manager.test.ts | 29 +++++ apps/desktop/src/preview/Manager.ts | 108 ++++++++++++++-- .../preview/previewAutomationErrors.test.ts | 43 +++++++ .../preview/previewAutomationErrors.ts | 121 ++++++++++++++++++ .../previewAutomationRequestConsumer.test.ts | 47 +++++++ 5 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/components/preview/previewAutomationErrors.test.ts diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts index 75271d76386a..d79e00adc9d7 100644 --- a/apps/desktop/src/preview/Manager.test.ts +++ b/apps/desktop/src/preview/Manager.test.ts @@ -3747,4 +3747,33 @@ describe("Preview automation diagnostics", () => { expect(JSON.stringify(error)).not.toContain(selector); expect("locator" in error).toBe(false); }); + + it("names hidden, disabled, and ambiguous click failures without leaking the locator", () => { + const selector = "role=button[name='target-secret']"; + const hidden = new PreviewManager.PreviewAutomationTargetHiddenError({ + operation: "click", + tabId: "tab_1", + selectorKind: "locator", + selectorLength: selector.length, + }); + const disabled = new PreviewManager.PreviewAutomationTargetDisabledError({ + operation: "click", + tabId: "tab_1", + selectorKind: "locator", + selectorLength: selector.length, + }); + const ambiguous = new PreviewManager.PreviewAutomationTargetAmbiguousError({ + operation: "click", + tabId: "tab_1", + selectorKind: "locator", + selectorLength: selector.length, + matchCount: 3, + }); + expect(hidden.message).toContain("not visible"); + expect(disabled.message).toContain("disabled"); + expect(ambiguous.message).toContain("matched 3 elements"); + expect(hidden.message).not.toContain("secret"); + expect(disabled.message).not.toContain("secret"); + expect(ambiguous.message).not.toContain("secret"); + }); }); diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 8ee312110d86..835f5493aee5 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -3541,7 +3541,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function locator, ); const point = yield* evaluateWithDebugger< - { x: number; y: number } | { invalidSelector: true; message: string } | { notFound: true } + | { x: number; y: number } + | { invalidSelector: true; message: string } + | { + notFound: true; + failureKind: "missing" | "hidden" | "disabled" | "ambiguous"; + matchCount?: number; + } >( tabId, send, @@ -3549,11 +3555,24 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function try { const injected = globalThis.__t3PlaywrightInjected; const parsed = injected.parseSelector(${locatorJson}); - const element = injected.querySelector(parsed, document, true); - if (!element) return { notFound: true }; + let element; + try { + element = injected.querySelector(parsed, document, true); + } catch (error) { + const message = String(error); + if (message.toLowerCase().includes("strict mode")) { + const matches = injected.querySelectorAll + ? injected.querySelectorAll(parsed, document) + : []; + return { notFound: true, failureKind: "ambiguous", matchCount: matches.length || 2 }; + } + throw error; + } + if (!element) return { notFound: true, failureKind: "missing" }; const visible = injected.elementState(element, "visible"); const enabled = injected.elementState(element, "enabled"); - if (!visible.matches || !enabled.matches) return { notFound: true }; + if (!visible.matches) return { notFound: true, failureKind: "hidden" }; + if (!enabled.matches) return { notFound: true, failureKind: "disabled" }; element.scrollIntoView({ block: "center", inline: "center" }); const rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; @@ -3573,10 +3592,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function }); } if ("notFound" in point) { - return yield* new PreviewAutomationTargetNotFoundError({ + return yield* raiseAutomationTargetLookupError({ operation: "click", tabId, ...automationSelectorDiagnostics(input), + failureKind: point.failureKind, + ...(point.matchCount === undefined ? {} : { matchCount: point.matchCount }), }); } return point; @@ -4254,21 +4275,85 @@ export class PreviewAutomationEvaluationError extends Schema.TaggedErrorClass()( "PreviewAutomationTargetNotFoundError", + PreviewAutomationTargetLookupFields, +) { + override get message(): string { + const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); + return `Preview automation ${this.operation} could not find ${target} in tab ${this.tabId}`; + } +} + +export class PreviewAutomationTargetHiddenError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetHiddenError", + PreviewAutomationTargetLookupFields, +) { + override get message(): string { + const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); + return `Preview automation ${this.operation} found ${target} in tab ${this.tabId}, but it is not visible`; + } +} + +export class PreviewAutomationTargetDisabledError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetDisabledError", + PreviewAutomationTargetLookupFields, +) { + override get message(): string { + const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); + return `Preview automation ${this.operation} found ${target} in tab ${this.tabId}, but it is disabled`; + } +} + +export class PreviewAutomationTargetAmbiguousError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetAmbiguousError", { - operation: Schema.String, - tabId: Schema.String, - selectorKind: PreviewAutomationSelectorKind, - selectorLength: Schema.optionalKey(Schema.Number), + ...PreviewAutomationTargetLookupFields, + matchCount: Schema.Number, }, ) { override get message(): string { const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); - return `Preview automation ${this.operation} could not find ${target} in tab ${this.tabId}`; + return `Preview automation ${this.operation} matched ${this.matchCount} elements for ${target} in tab ${this.tabId}`; } } +const raiseAutomationTargetLookupError = (input: { + readonly operation: string; + readonly tabId: string; + readonly selectorKind: PreviewAutomationSelectorKind; + readonly selectorLength?: number; + readonly failureKind?: "missing" | "hidden" | "disabled" | "ambiguous"; + readonly matchCount?: number; +}) => { + const shared = { + operation: input.operation, + tabId: input.tabId, + selectorKind: input.selectorKind, + ...(input.selectorLength === undefined ? {} : { selectorLength: input.selectorLength }), + }; + if (input.failureKind === "hidden") { + return new PreviewAutomationTargetHiddenError(shared); + } + if (input.failureKind === "disabled") { + return new PreviewAutomationTargetDisabledError(shared); + } + if (input.failureKind === "ambiguous") { + return new PreviewAutomationTargetAmbiguousError({ + ...shared, + matchCount: input.matchCount ?? 0, + }); + } + return new PreviewAutomationTargetNotFoundError(shared); +}; + export class PreviewAutomationTargetNotEditableError extends Schema.TaggedErrorClass()( "PreviewAutomationTargetNotEditableError", { @@ -4387,6 +4472,9 @@ export const PreviewManagerError = Schema.Union([ PreviewAutomationDebuggerAttachedError, PreviewAutomationEvaluationError, PreviewAutomationTargetNotFoundError, + PreviewAutomationTargetHiddenError, + PreviewAutomationTargetDisabledError, + PreviewAutomationTargetAmbiguousError, PreviewAutomationTargetNotEditableError, PreviewAutomationCoordinatesOutsideViewportError, PreviewAutomationInvalidSelectorError, diff --git a/apps/web/src/components/preview/previewAutomationErrors.test.ts b/apps/web/src/components/preview/previewAutomationErrors.test.ts new file mode 100644 index 000000000000..24d74c6ec162 --- /dev/null +++ b/apps/web/src/components/preview/previewAutomationErrors.test.ts @@ -0,0 +1,43 @@ +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { PreviewAutomationOperationError } from "./previewAutomationErrors"; + +describe("PreviewAutomationOperationError", () => { + const context = { + requestId: "request-1", + operation: "click" as const, + environmentId: EnvironmentId.make("environment-1"), + threadId: ThreadId.make("thread-1"), + tabId: "tab-1", + }; + + it("maps typed not-found failures to a visible/disabled/ambiguous reason", () => { + const hidden = PreviewAutomationOperationError.fromCause({ + ...context, + cause: { _tag: "PreviewAutomationTargetHiddenError" }, + }); + const disabled = PreviewAutomationOperationError.fromCause({ + ...context, + cause: { _tag: "PreviewAutomationTargetDisabledError" }, + }); + const ambiguous = PreviewAutomationOperationError.fromCause({ + ...context, + cause: { _tag: "PreviewAutomationTargetAmbiguousError", matchCount: 3 }, + }); + const legacyHidden = PreviewAutomationOperationError.fromCause({ + ...context, + cause: { + _tag: "PreviewAutomationTargetNotFoundError", + failureKind: "hidden", + }, + }); + expect(hidden.message).toContain("not visible"); + expect(disabled.message).toContain("disabled"); + expect(ambiguous.message).toContain("matched 3 elements"); + expect(legacyHidden.message).toContain("not visible"); + expect(hidden.message).not.toContain("secret"); + expect(disabled.message).not.toContain("secret"); + expect(ambiguous.message).not.toContain("secret"); + }); +}); diff --git a/apps/web/src/components/preview/previewAutomationErrors.ts b/apps/web/src/components/preview/previewAutomationErrors.ts index dcf35de53f2d..b327ca7a7005 100644 --- a/apps/web/src/components/preview/previewAutomationErrors.ts +++ b/apps/web/src/components/preview/previewAutomationErrors.ts @@ -134,6 +134,103 @@ export class PreviewAutomationTargetNotEditableHostError extends Schema.TaggedEr } } +const PreviewAutomationTargetHostFields = { + requestId: TrimmedNonEmptyString, + operation: PreviewAutomationOperation, + environmentId: EnvironmentId, + threadId: ThreadId, + tabId: Schema.NullOr(PreviewTabId), + cause: Schema.Defect(), +}; + +const readTargetLookupKind = ( + cause: unknown, +): "missing" | "hidden" | "disabled" | "ambiguous" | null => { + if (typeof cause !== "object" || cause === null || !("_tag" in cause)) return null; + if (cause._tag === "PreviewAutomationTargetHiddenError") return "hidden"; + if (cause._tag === "PreviewAutomationTargetDisabledError") return "disabled"; + if (cause._tag === "PreviewAutomationTargetAmbiguousError") return "ambiguous"; + if (cause._tag !== "PreviewAutomationTargetNotFoundError") return null; + if ( + "failureKind" in cause && + (cause.failureKind === "hidden" || + cause.failureKind === "disabled" || + cause.failureKind === "ambiguous") + ) { + return cause.failureKind; + } + return "missing"; +}; + +const readAmbiguousMatchCount = (cause: unknown): number => { + if ( + typeof cause === "object" && + cause !== null && + "matchCount" in cause && + typeof cause.matchCount === "number" && + Number.isInteger(cause.matchCount) && + cause.matchCount >= 0 + ) { + return cause.matchCount; + } + return 0; +}; + +export class PreviewAutomationTargetNotFoundHostError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetNotFoundHostError", + PreviewAutomationTargetHostFields, +) { + get responseTag() { + return "PreviewAutomationExecutionError" as const; + } + + override get message(): string { + return `Preview automation ${this.operation} request ${this.requestId} could not find a target in tab ${this.tabId ?? "unassigned"}.`; + } +} + +export class PreviewAutomationTargetHiddenHostError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetHiddenHostError", + PreviewAutomationTargetHostFields, +) { + get responseTag() { + return "PreviewAutomationExecutionError" as const; + } + + override get message(): string { + return `Preview automation ${this.operation} request ${this.requestId} found a target in tab ${this.tabId ?? "unassigned"}, but it is not visible.`; + } +} + +export class PreviewAutomationTargetDisabledHostError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetDisabledHostError", + PreviewAutomationTargetHostFields, +) { + get responseTag() { + return "PreviewAutomationExecutionError" as const; + } + + override get message(): string { + return `Preview automation ${this.operation} request ${this.requestId} found a target in tab ${this.tabId ?? "unassigned"}, but it is disabled.`; + } +} + +export class PreviewAutomationTargetAmbiguousHostError extends Schema.TaggedErrorClass()( + "PreviewAutomationTargetAmbiguousHostError", + { + ...PreviewAutomationTargetHostFields, + matchCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + }, +) { + get responseTag() { + return "PreviewAutomationExecutionError" as const; + } + + override get message(): string { + return `Preview automation ${this.operation} request ${this.requestId} matched ${this.matchCount} elements in tab ${this.tabId ?? "unassigned"}.`; + } +} + const targetNotEditableDiagnostics = ( cause: unknown, ): { @@ -183,6 +280,26 @@ export class PreviewAutomationOperationError extends Schema.TaggedErrorClass
 {
     });
   });
 
+  it("maps hidden click targets to a named execution failure without leaking the locator", () => {
+    expect(
+      serializePreviewAutomationError(
+        {
+          _tag: "PreviewAutomationTargetHiddenError",
+          selector: "role=button[name='target-secret']",
+        },
+        {
+          requestId: "request-click",
+          operation: "click",
+          environmentId,
+          threadId,
+          tabId,
+        },
+      ),
+    ).toEqual({
+      _tag: "PreviewAutomationExecutionError",
+      message:
+        "Preview automation click request request-click found a target in tab tab-1, but it is not visible.",
+      detail: {
+        requestId: "request-click",
+        operation: "click",
+        environmentId: "environment-1",
+        threadId: "thread-1",
+        tabId: "tab-1",
+      },
+    });
+    expect(
+      JSON.stringify(
+        serializePreviewAutomationError(
+          {
+            _tag: "PreviewAutomationTargetNotFoundError",
+            failureKind: "hidden",
+            selector: "role=button[name='target-secret']",
+          },
+          {
+            requestId: "request-click",
+            operation: "click",
+            environmentId,
+            threadId,
+            tabId,
+          },
+        ),
+      ),
+    ).not.toContain("secret");
+  });
+
   it("maps desktop non-editable targets to the public typed response", () => {
     expect(
       serializePreviewAutomationError(

From 8ca68dd995e49a53d345e12b3acb54cac1026b91 Mon Sep 17 00:00:00 2001
From: Guilherme Barros 
Date: Mon, 17 Aug 2026 10:34:15 +0200
Subject: [PATCH 2/8] fix(preview): classify click lookup failures on the error
 type

A free function picked hidden/disabled/ambiguous and defaulted
ambiguous matches to 0. The policy now lives on
PreviewAutomationTargetNotFoundError.fromLookupFailure, and
ambiguous requires an explicit match count.
---
 apps/desktop/src/preview/Manager.test.ts | 12 ++++
 apps/desktop/src/preview/Manager.ts      | 76 +++++++++++++-----------
 2 files changed, 54 insertions(+), 34 deletions(-)

diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts
index d79e00adc9d7..d1535fbab86b 100644
--- a/apps/desktop/src/preview/Manager.test.ts
+++ b/apps/desktop/src/preview/Manager.test.ts
@@ -3775,5 +3775,17 @@ describe("Preview automation diagnostics", () => {
     expect(hidden.message).not.toContain("secret");
     expect(disabled.message).not.toContain("secret");
     expect(ambiguous.message).not.toContain("secret");
+
+    const fromAmbiguous = PreviewManager.PreviewAutomationTargetNotFoundError.fromLookupFailure({
+      operation: "click",
+      tabId: "tab_1",
+      selectorKind: "locator",
+      selectorLength: selector.length,
+      failureKind: "ambiguous",
+      matchCount: 3,
+    });
+    expect(fromAmbiguous).toBeInstanceOf(PreviewManager.PreviewAutomationTargetAmbiguousError);
+    expect(fromAmbiguous.message).toContain("matched 3 elements");
+    expect(fromAmbiguous.message).not.toContain("secret");
   });
 });
diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts
index 835f5493aee5..968911cba17e 100644
--- a/apps/desktop/src/preview/Manager.ts
+++ b/apps/desktop/src/preview/Manager.ts
@@ -3545,8 +3545,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
       | { invalidSelector: true; message: string }
       | {
           notFound: true;
-          failureKind: "missing" | "hidden" | "disabled" | "ambiguous";
-          matchCount?: number;
+          failureKind: "missing" | "hidden" | "disabled";
+        }
+      | {
+          notFound: true;
+          failureKind: "ambiguous";
+          matchCount: number;
         }
     >(
       tabId,
@@ -3592,12 +3596,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
       });
     }
     if ("notFound" in point) {
-      return yield* raiseAutomationTargetLookupError({
+      return yield* PreviewAutomationTargetNotFoundError.fromLookupFailure({
         operation: "click",
         tabId,
         ...automationSelectorDiagnostics(input),
-        failureKind: point.failureKind,
-        ...(point.matchCount === undefined ? {} : { matchCount: point.matchCount }),
+        ...(point.failureKind === "ambiguous"
+          ? { failureKind: "ambiguous", matchCount: point.matchCount }
+          : { failureKind: point.failureKind }),
       });
     }
     return point;
@@ -4286,6 +4291,38 @@ export class PreviewAutomationTargetNotFoundError extends Schema.TaggedErrorClas
   "PreviewAutomationTargetNotFoundError",
   PreviewAutomationTargetLookupFields,
 ) {
+  static fromLookupFailure(
+    input: {
+      readonly operation: string;
+      readonly tabId: string;
+      readonly selectorKind: PreviewAutomationSelectorKind;
+      readonly selectorLength?: number;
+    } & (
+      | { readonly failureKind: "ambiguous"; readonly matchCount: number }
+      | { readonly failureKind?: "missing" | "hidden" | "disabled" }
+    ),
+  ) {
+    const shared = {
+      operation: input.operation,
+      tabId: input.tabId,
+      selectorKind: input.selectorKind,
+      ...(input.selectorLength === undefined ? {} : { selectorLength: input.selectorLength }),
+    };
+    if (input.failureKind === "hidden") {
+      return new PreviewAutomationTargetHiddenError(shared);
+    }
+    if (input.failureKind === "disabled") {
+      return new PreviewAutomationTargetDisabledError(shared);
+    }
+    if (input.failureKind === "ambiguous") {
+      return new PreviewAutomationTargetAmbiguousError({
+        ...shared,
+        matchCount: input.matchCount,
+      });
+    }
+    return new PreviewAutomationTargetNotFoundError(shared);
+  }
+
   override get message(): string {
     const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength);
     return `Preview automation ${this.operation} could not find ${target} in tab ${this.tabId}`;
@@ -4325,35 +4362,6 @@ export class PreviewAutomationTargetAmbiguousError extends Schema.TaggedErrorCla
   }
 }
 
-const raiseAutomationTargetLookupError = (input: {
-  readonly operation: string;
-  readonly tabId: string;
-  readonly selectorKind: PreviewAutomationSelectorKind;
-  readonly selectorLength?: number;
-  readonly failureKind?: "missing" | "hidden" | "disabled" | "ambiguous";
-  readonly matchCount?: number;
-}) => {
-  const shared = {
-    operation: input.operation,
-    tabId: input.tabId,
-    selectorKind: input.selectorKind,
-    ...(input.selectorLength === undefined ? {} : { selectorLength: input.selectorLength }),
-  };
-  if (input.failureKind === "hidden") {
-    return new PreviewAutomationTargetHiddenError(shared);
-  }
-  if (input.failureKind === "disabled") {
-    return new PreviewAutomationTargetDisabledError(shared);
-  }
-  if (input.failureKind === "ambiguous") {
-    return new PreviewAutomationTargetAmbiguousError({
-      ...shared,
-      matchCount: input.matchCount ?? 0,
-    });
-  }
-  return new PreviewAutomationTargetNotFoundError(shared);
-};
-
 export class PreviewAutomationTargetNotEditableError extends Schema.TaggedErrorClass()(
   "PreviewAutomationTargetNotEditableError",
   {

From 1c01be5dd0ed478f0f123620222b15968c15ce2d Mon Sep 17 00:00:00 2001
From: Guilherme Barros 
Date: Mon, 17 Aug 2026 10:39:43 +0200
Subject: [PATCH 3/8] fix(preview): map click failures from tagged errors only

Host mapping still accepted a failureKind field on
PreviewAutomationTargetNotFoundError. Nothing emits that shape.
Classification now uses the hidden, disabled, and ambiguous tags only.
---
 .../preview/previewAutomationErrors.test.ts          |  9 +++------
 .../components/preview/previewAutomationErrors.ts    | 12 ++----------
 .../preview/previewAutomationRequestConsumer.test.ts |  3 +--
 3 files changed, 6 insertions(+), 18 deletions(-)

diff --git a/apps/web/src/components/preview/previewAutomationErrors.test.ts b/apps/web/src/components/preview/previewAutomationErrors.test.ts
index 24d74c6ec162..70ba78977337 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.test.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.test.ts
@@ -25,17 +25,14 @@ describe("PreviewAutomationOperationError", () => {
       ...context,
       cause: { _tag: "PreviewAutomationTargetAmbiguousError", matchCount: 3 },
     });
-    const legacyHidden = PreviewAutomationOperationError.fromCause({
+    const missing = PreviewAutomationOperationError.fromCause({
       ...context,
-      cause: {
-        _tag: "PreviewAutomationTargetNotFoundError",
-        failureKind: "hidden",
-      },
+      cause: { _tag: "PreviewAutomationTargetNotFoundError" },
     });
     expect(hidden.message).toContain("not visible");
     expect(disabled.message).toContain("disabled");
     expect(ambiguous.message).toContain("matched 3 elements");
-    expect(legacyHidden.message).toContain("not visible");
+    expect(missing.message).toContain("could not find a target");
     expect(hidden.message).not.toContain("secret");
     expect(disabled.message).not.toContain("secret");
     expect(ambiguous.message).not.toContain("secret");
diff --git a/apps/web/src/components/preview/previewAutomationErrors.ts b/apps/web/src/components/preview/previewAutomationErrors.ts
index b327ca7a7005..85a9eddfb3fb 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.ts
@@ -150,16 +150,8 @@ const readTargetLookupKind = (
   if (cause._tag === "PreviewAutomationTargetHiddenError") return "hidden";
   if (cause._tag === "PreviewAutomationTargetDisabledError") return "disabled";
   if (cause._tag === "PreviewAutomationTargetAmbiguousError") return "ambiguous";
-  if (cause._tag !== "PreviewAutomationTargetNotFoundError") return null;
-  if (
-    "failureKind" in cause &&
-    (cause.failureKind === "hidden" ||
-      cause.failureKind === "disabled" ||
-      cause.failureKind === "ambiguous")
-  ) {
-    return cause.failureKind;
-  }
-  return "missing";
+  if (cause._tag === "PreviewAutomationTargetNotFoundError") return "missing";
+  return null;
 };
 
 const readAmbiguousMatchCount = (cause: unknown): number => {
diff --git a/apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts b/apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts
index 247f09746f69..2ee0d6aacfdd 100644
--- a/apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts
+++ b/apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts
@@ -322,8 +322,7 @@ describe("previewAutomationRequestConsumer", () => {
       JSON.stringify(
         serializePreviewAutomationError(
           {
-            _tag: "PreviewAutomationTargetNotFoundError",
-            failureKind: "hidden",
+            _tag: "PreviewAutomationTargetHiddenError",
             selector: "role=button[name='target-secret']",
           },
           {

From 8dddd9e8504679111be195d9a733d7b7f83cb93e Mon Sep 17 00:00:00 2001
From: Theo Browne 
Date: Tue, 1 Sep 2026 16:25:53 -0700
Subject: [PATCH 4/8] feat(contracts): type preview click outcomes

---
 packages/contracts/src/ipc.test.ts | 25 +++++++++++++++++++++++-
 packages/contracts/src/ipc.ts      | 31 +++++++++++++++++++++++++++++-
 2 files changed, 54 insertions(+), 2 deletions(-)

diff --git a/packages/contracts/src/ipc.test.ts b/packages/contracts/src/ipc.test.ts
index 20db75368a9a..87f22907ad07 100644
--- a/packages/contracts/src/ipc.test.ts
+++ b/packages/contracts/src/ipc.test.ts
@@ -1,7 +1,10 @@
 import * as Schema from "effect/Schema";
 import { describe, expect, it } from "vite-plus/test";
 
-import { DesktopEnvironmentBootstrapSchema } from "./ipc.ts";
+import {
+  DesktopEnvironmentBootstrapSchema,
+  DesktopPreviewAutomationClickResultSchema,
+} from "./ipc.ts";
 
 describe("DesktopEnvironmentBootstrapSchema", () => {
   const decode = Schema.decodeUnknownSync(DesktopEnvironmentBootstrapSchema);
@@ -36,3 +39,23 @@ describe("DesktopEnvironmentBootstrapSchema", () => {
     ).toBeNull();
   });
 });
+
+describe("DesktopPreviewAutomationClickResultSchema", () => {
+  const decode = Schema.decodeUnknownSync(DesktopPreviewAutomationClickResultSchema);
+
+  it.each([
+    { _tag: "Dispatched" },
+    { _tag: "NotSent", reason: "tab-not-visible" },
+    { _tag: "NotSent", reason: "timeout", timeoutMs: 50 },
+    { _tag: "NotSent", reason: "target-missing" },
+    { _tag: "NotSent", reason: "target-hidden" },
+    { _tag: "NotSent", reason: "target-disabled" },
+    { _tag: "NotSent", reason: "target-ambiguous", matchCount: 2 },
+  ] as const)("decodes $reason", (result) => {
+    expect(decode(result)).toEqual(result);
+  });
+
+  it("rejects an ambiguous target without a positive match count", () => {
+    expect(() => decode({ _tag: "NotSent", reason: "target-ambiguous", matchCount: 0 })).toThrow();
+  });
+});
diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts
index 88dc2b26b280..f447d346f0c1 100644
--- a/packages/contracts/src/ipc.ts
+++ b/packages/contracts/src/ipc.ts
@@ -1001,6 +1001,32 @@ export const DesktopPreviewAutomationClickInputSchema = Schema.Struct({
   input: PreviewAutomationClickInput,
 });
 
+export const DesktopPreviewAutomationClickResultSchema = Schema.Union([
+  Schema.TaggedStruct("Dispatched", {}),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("tab-not-visible"),
+  }),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("timeout"),
+    timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
+  }),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("target-missing"),
+  }),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("target-hidden"),
+  }),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("target-disabled"),
+  }),
+  Schema.TaggedStruct("NotSent", {
+    reason: Schema.Literal("target-ambiguous"),
+    matchCount: Schema.Int.check(Schema.isGreaterThan(0)),
+  }),
+]);
+export type DesktopPreviewAutomationClickResult =
+  typeof DesktopPreviewAutomationClickResultSchema.Type;
+
 export const DesktopPreviewAutomationTypeInputSchema = Schema.Struct({
   tabId: DesktopPreviewTabIdSchema,
   input: PreviewAutomationTypeInput,
@@ -1194,7 +1220,10 @@ export interface DesktopPreviewBridge {
   automation: {
     status: (tabId: string) => Promise;
     snapshot: (tabId: string) => Promise;
-    click: (tabId: string, input: PreviewAutomationClickInput) => Promise;
+    click: (
+      tabId: string,
+      input: PreviewAutomationClickInput,
+    ) => Promise;
     type: (tabId: string, input: PreviewAutomationTypeInput) => Promise;
     press: (tabId: string, input: PreviewAutomationPressInput) => Promise;
     scroll: (tabId: string, input: PreviewAutomationScrollInput) => Promise;

From b6a6a703c803eaf1f41f5f2fcb629c51e3a83eba Mon Sep 17 00:00:00 2001
From: Theo Browne 
Date: Tue, 1 Sep 2026 16:38:59 -0700
Subject: [PATCH 5/8] fix(preview): preserve click target failure reasons

---
 apps/desktop/src/ipc/methods/preview.test.ts  |  18 +++
 apps/desktop/src/ipc/methods/preview.ts       |   5 +-
 apps/desktop/src/preview/Manager.test.ts      |  98 +++++++-----
 apps/desktop/src/preview/Manager.ts           | 133 +++++++----------
 apps/server/src/mcp/McpHttpServer.test.ts     |  45 ++++++
 .../src/mcp/PreviewAutomationBroker.test.ts   |  50 +++++++
 .../server/src/mcp/PreviewAutomationBroker.ts |  29 +++-
 .../preview/PreviewAutomationHosts.tsx        |  16 +-
 .../preview/previewAutomationErrors.test.ts   |  54 ++++---
 .../preview/previewAutomationErrors.ts        | 141 ++++++------------
 .../previewAutomationRequestConsumer.test.ts  |  56 +++----
 packages/contracts/src/previewAutomation.ts   |  41 +++++
 12 files changed, 414 insertions(+), 272 deletions(-)

diff --git a/apps/desktop/src/ipc/methods/preview.test.ts b/apps/desktop/src/ipc/methods/preview.test.ts
index e7770dc629dd..8b5722d6c3ca 100644
--- a/apps/desktop/src/ipc/methods/preview.test.ts
+++ b/apps/desktop/src/ipc/methods/preview.test.ts
@@ -79,6 +79,24 @@ describe("preview IPC methods", () => {
     }),
   );
 
+  effectIt.effect("returns typed click outcomes across the preview IPC handler", () =>
+    Effect.gen(function* () {
+      const result = {
+        _tag: "NotSent",
+        reason: "target-disabled",
+      } as const;
+      const manager = PreviewManager.PreviewManager.of({
+        automationClick: () => Effect.succeed(result),
+      } as unknown as PreviewManager.PreviewManager["Service"]);
+
+      expect(
+        yield* PreviewIpc.automationClick
+          .handler({ tabId: "tab-1", input: { locator: "role=button[name='Send']" } })
+          .pipe(Effect.provideService(PreviewManager.PreviewManager, manager)),
+      ).toEqual(result);
+    }),
+  );
+
   it("keeps the public automation status tab id limit", () => {
     const encode = Schema.encodeUnknownSync(PreviewAutomationStatus);
     const tabId = "t".repeat(129);
diff --git a/apps/desktop/src/ipc/methods/preview.ts b/apps/desktop/src/ipc/methods/preview.ts
index 5229d36c31f1..9122d8b5947f 100644
--- a/apps/desktop/src/ipc/methods/preview.ts
+++ b/apps/desktop/src/ipc/methods/preview.ts
@@ -2,6 +2,7 @@ import {
   DesktopPreviewAnnotationThemeInputSchema,
   DesktopPreviewArtifactInputSchema,
   DesktopPreviewAutomationClickInputSchema,
+  DesktopPreviewAutomationClickResultSchema,
   DesktopPreviewAutomationEvaluateInputSchema,
   DesktopPreviewAutomationPressInputSchema,
   DesktopPreviewAutomationScrollInputSchema,
@@ -302,10 +303,10 @@ export const automationSnapshot = DesktopIpc.makeIpcMethod({
 export const automationClick = DesktopIpc.makeIpcMethod({
   channel: IpcChannels.PREVIEW_AUTOMATION_CLICK_CHANNEL,
   payload: DesktopPreviewAutomationClickInputSchema,
-  result: Schema.Void,
+  result: DesktopPreviewAutomationClickResultSchema,
   handler: Effect.fn("desktop.ipc.preview.automationClick")(function* ({ tabId, input }) {
     const manager = yield* PreviewManager.PreviewManager;
-    yield* manager.automationClick(tabId, input);
+    return yield* manager.automationClick(tabId, input);
   }),
 });
 
diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts
index d1535fbab86b..96575b3e2e41 100644
--- a/apps/desktop/src/preview/Manager.test.ts
+++ b/apps/desktop/src/preview/Manager.test.ts
@@ -3748,44 +3748,62 @@ describe("Preview automation diagnostics", () => {
     expect("locator" in error).toBe(false);
   });
 
-  it("names hidden, disabled, and ambiguous click failures without leaking the locator", () => {
-    const selector = "role=button[name='target-secret']";
-    const hidden = new PreviewManager.PreviewAutomationTargetHiddenError({
-      operation: "click",
-      tabId: "tab_1",
-      selectorKind: "locator",
-      selectorLength: selector.length,
-    });
-    const disabled = new PreviewManager.PreviewAutomationTargetDisabledError({
-      operation: "click",
-      tabId: "tab_1",
-      selectorKind: "locator",
-      selectorLength: selector.length,
-    });
-    const ambiguous = new PreviewManager.PreviewAutomationTargetAmbiguousError({
-      operation: "click",
-      tabId: "tab_1",
-      selectorKind: "locator",
-      selectorLength: selector.length,
-      matchCount: 3,
-    });
-    expect(hidden.message).toContain("not visible");
-    expect(disabled.message).toContain("disabled");
-    expect(ambiguous.message).toContain("matched 3 elements");
-    expect(hidden.message).not.toContain("secret");
-    expect(disabled.message).not.toContain("secret");
-    expect(ambiguous.message).not.toContain("secret");
-
-    const fromAmbiguous = PreviewManager.PreviewAutomationTargetNotFoundError.fromLookupFailure({
-      operation: "click",
-      tabId: "tab_1",
-      selectorKind: "locator",
-      selectorLength: selector.length,
-      failureKind: "ambiguous",
-      matchCount: 3,
-    });
-    expect(fromAmbiguous).toBeInstanceOf(PreviewManager.PreviewAutomationTargetAmbiguousError);
-    expect(fromAmbiguous.message).toContain("matched 3 elements");
-    expect(fromAmbiguous.message).not.toContain("secret");
-  });
+  effectIt.effect("returns typed click lookup failures without dispatching input", () =>
+    withManager((manager) =>
+      Effect.gen(function* () {
+        const selector = "role=button[name='target-secret']";
+        let lookupResult: unknown = { notFound: true, failureKind: "missing" };
+        const sendCommand = vi.fn(async (method: string, params?: Record) => {
+          if (method !== "Runtime.evaluate") return undefined;
+          const expression = String(params?.["expression"] ?? "");
+          return expression.includes("const parsed = injected.parseSelector")
+            ? { result: { value: lookupResult } }
+            : { result: { value: true } };
+        });
+        fromId.mockReturnValue({
+          ...makeTestPreviewWebContents(
+            vi.fn(async () => ({
+              toJPEG: () => Buffer.from("unused-click-frame"),
+              toPNG: () => Buffer.from("unused-click-frame"),
+              getSize: () => ({ width: 1280, height: 720 }),
+            })),
+          ),
+          isDevToolsOpened: () => false,
+          debugger: {
+            isAttached: () => false,
+            attach: vi.fn(),
+            sendCommand,
+            on: vi.fn(),
+            off: vi.fn(),
+          },
+        } as never);
+
+        yield* manager.createTab("tab_lookup");
+        yield* manager.registerWebview("tab_lookup", 42);
+
+        const missing = yield* manager.automationClick("tab_lookup", { locator: selector });
+        lookupResult = { notFound: true, failureKind: "hidden" };
+        const hidden = yield* manager.automationClick("tab_lookup", { locator: selector });
+        lookupResult = { notFound: true, failureKind: "disabled" };
+        const disabled = yield* manager.automationClick("tab_lookup", { locator: selector });
+        lookupResult = { notFound: true, failureKind: "ambiguous", matchCount: 3 };
+        const ambiguous = yield* manager.automationClick("tab_lookup", { locator: selector });
+        const snapshot = yield* manager.automationSnapshot("tab_lookup");
+
+        expect([missing, hidden, disabled, ambiguous]).toEqual([
+          { _tag: "NotSent", reason: "target-missing" },
+          { _tag: "NotSent", reason: "target-hidden" },
+          { _tag: "NotSent", reason: "target-disabled" },
+          { _tag: "NotSent", reason: "target-ambiguous", matchCount: 3 },
+        ]);
+        expect(snapshot.actionTimeline.filter((action) => action.action === "click")).toEqual([
+          expect.objectContaining({ status: "failed" }),
+          expect.objectContaining({ status: "failed" }),
+          expect.objectContaining({ status: "failed" }),
+          expect.objectContaining({ status: "failed" }),
+        ]);
+        expect(sendCommand).not.toHaveBeenCalledWith("Input.dispatchMouseEvent", expect.anything());
+      }),
+    ),
+  );
 });
diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts
index 968911cba17e..6e3a9708f8af 100644
--- a/apps/desktop/src/preview/Manager.ts
+++ b/apps/desktop/src/preview/Manager.ts
@@ -8,6 +8,7 @@
 import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts";
 import type {
   DesktopPreviewAnnotationTheme,
+  DesktopPreviewAutomationClickResult,
   DesktopPreviewAutomationStatus,
   DesktopPreviewColorScheme,
   DesktopPreviewFavicon,
@@ -3596,13 +3597,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
       });
     }
     if ("notFound" in point) {
-      return yield* PreviewAutomationTargetNotFoundError.fromLookupFailure({
+      return yield* new PreviewAutomationTargetLookupError({
         operation: "click",
         tabId,
         ...automationSelectorDiagnostics(input),
-        ...(point.failureKind === "ambiguous"
-          ? { failureKind: "ambiguous", matchCount: point.matchCount }
-          : { failureKind: point.failureKind }),
+        failureKind: point.failureKind,
+        ...(point.failureKind === "ambiguous" ? { matchCount: point.matchCount } : {}),
       });
     }
     return point;
@@ -3681,8 +3681,34 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
     input: PreviewAutomationClickInput,
   ) {
     const wc = yield* requireWebContents(tabId);
-    yield* withControlSession(tabId, wc, "click", (send) =>
-      performAutomationClick(tabId, input, send),
+    return yield* Effect.gen(function* () {
+      yield* withControlSession(tabId, wc, "click", (send) =>
+        performAutomationClick(tabId, input, send),
+      );
+      return { _tag: "Dispatched" } satisfies DesktopPreviewAutomationClickResult;
+    }).pipe(
+      Effect.catchTag(
+        "PreviewAutomationTargetLookupError",
+        (
+          error,
+        ): Effect.Effect<
+          DesktopPreviewAutomationClickResult,
+          PreviewAutomationTargetLookupError
+        > => {
+          if (error.failureKind === "ambiguous") {
+            if (error.matchCount === undefined) return Effect.fail(error);
+            return Effect.succeed({
+              _tag: "NotSent",
+              reason: "target-ambiguous",
+              matchCount: error.matchCount,
+            });
+          }
+          return Effect.succeed({
+            _tag: "NotSent",
+            reason: `target-${error.failureKind}`,
+          });
+        },
+      ),
     );
   });
 
@@ -4280,85 +4306,44 @@ export class PreviewAutomationEvaluationError extends Schema.TaggedErrorClass()(
   "PreviewAutomationTargetNotFoundError",
-  PreviewAutomationTargetLookupFields,
+  {
+    operation: Schema.String,
+    tabId: Schema.String,
+    selectorKind: PreviewAutomationSelectorKind,
+    selectorLength: Schema.optionalKey(Schema.Number),
+  },
 ) {
-  static fromLookupFailure(
-    input: {
-      readonly operation: string;
-      readonly tabId: string;
-      readonly selectorKind: PreviewAutomationSelectorKind;
-      readonly selectorLength?: number;
-    } & (
-      | { readonly failureKind: "ambiguous"; readonly matchCount: number }
-      | { readonly failureKind?: "missing" | "hidden" | "disabled" }
-    ),
-  ) {
-    const shared = {
-      operation: input.operation,
-      tabId: input.tabId,
-      selectorKind: input.selectorKind,
-      ...(input.selectorLength === undefined ? {} : { selectorLength: input.selectorLength }),
-    };
-    if (input.failureKind === "hidden") {
-      return new PreviewAutomationTargetHiddenError(shared);
-    }
-    if (input.failureKind === "disabled") {
-      return new PreviewAutomationTargetDisabledError(shared);
-    }
-    if (input.failureKind === "ambiguous") {
-      return new PreviewAutomationTargetAmbiguousError({
-        ...shared,
-        matchCount: input.matchCount,
-      });
-    }
-    return new PreviewAutomationTargetNotFoundError(shared);
-  }
-
   override get message(): string {
     const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength);
     return `Preview automation ${this.operation} could not find ${target} in tab ${this.tabId}`;
   }
 }
 
-export class PreviewAutomationTargetHiddenError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetHiddenError",
-  PreviewAutomationTargetLookupFields,
-) {
-  override get message(): string {
-    const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength);
-    return `Preview automation ${this.operation} found ${target} in tab ${this.tabId}, but it is not visible`;
-  }
-}
-
-export class PreviewAutomationTargetDisabledError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetDisabledError",
-  PreviewAutomationTargetLookupFields,
-) {
-  override get message(): string {
-    const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength);
-    return `Preview automation ${this.operation} found ${target} in tab ${this.tabId}, but it is disabled`;
-  }
-}
-
-export class PreviewAutomationTargetAmbiguousError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetAmbiguousError",
+export class PreviewAutomationTargetLookupError extends Schema.TaggedErrorClass()(
+  "PreviewAutomationTargetLookupError",
   {
-    ...PreviewAutomationTargetLookupFields,
-    matchCount: Schema.Number,
+    operation: Schema.String,
+    tabId: Schema.String,
+    selectorKind: PreviewAutomationSelectorKind,
+    selectorLength: Schema.optionalKey(Schema.Number),
+    failureKind: Schema.Literals(["missing", "hidden", "disabled", "ambiguous"]),
+    matchCount: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
   },
 ) {
   override get message(): string {
     const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength);
-    return `Preview automation ${this.operation} matched ${this.matchCount} elements for ${target} in tab ${this.tabId}`;
+    if (this.failureKind === "hidden") {
+      return `Preview automation ${this.operation} found ${target}, but it is not visible`;
+    }
+    if (this.failureKind === "disabled") {
+      return `Preview automation ${this.operation} found ${target}, but it is disabled`;
+    }
+    if (this.failureKind === "ambiguous") {
+      return `Preview automation ${this.operation} matched ${this.matchCount ?? 0} elements for ${target}`;
+    }
+    return `Preview automation ${this.operation} could not find ${target}`;
   }
 }
 
@@ -4480,9 +4465,7 @@ export const PreviewManagerError = Schema.Union([
   PreviewAutomationDebuggerAttachedError,
   PreviewAutomationEvaluationError,
   PreviewAutomationTargetNotFoundError,
-  PreviewAutomationTargetHiddenError,
-  PreviewAutomationTargetDisabledError,
-  PreviewAutomationTargetAmbiguousError,
+  PreviewAutomationTargetLookupError,
   PreviewAutomationTargetNotEditableError,
   PreviewAutomationCoordinatesOutsideViewportError,
   PreviewAutomationInvalidSelectorError,
@@ -4569,7 +4552,7 @@ export class PreviewManager extends Context.Service<
     readonly automationClick: (
       tabId: string,
       input: PreviewAutomationClickInput,
-    ) => Effect.Effect;
+    ) => Effect.Effect;
     readonly automationType: (
       tabId: string,
       input: PreviewAutomationTypeInput,
diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts
index fa2880f9c364..9468da95eb04 100644
--- a/apps/server/src/mcp/McpHttpServer.test.ts
+++ b/apps/server/src/mcp/McpHttpServer.test.ts
@@ -97,6 +97,51 @@ it.effect("returns bounded structural preview snapshot failures", () =>
   ).pipe(Effect.provide(TestLayer)),
 );
 
+it.effect("returns a typed click lookup reason through the MCP tool", () =>
+  Effect.scoped(
+    Effect.gen(function* () {
+      const locator = "role=button[name='request-secret']";
+      const server = yield* McpServer.McpServer;
+      const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
+      const events = yield* broker.connect({
+        clientId: "mcp-click-failure-client",
+        environmentId,
+      });
+      yield* Stream.runForEach(events, (event) =>
+        event.type === "connected"
+          ? Effect.void
+          : broker.respond({
+              clientId: "mcp-click-failure-client",
+              connectionId: event.connectionId,
+              requestId: event.request.requestId,
+              ok: false,
+              error: {
+                _tag: "PreviewAutomationTargetLookupError",
+                message: "The preview click target is disabled.",
+                detail: { failureKind: "disabled" },
+              },
+            }),
+      ).pipe(Effect.forkScoped);
+      yield* Effect.yieldNow;
+
+      const result = yield* server
+        .callTool({ name: "preview_click", arguments: { locator } })
+        .pipe(
+          Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
+          Effect.provideService(McpSchema.McpServerClient, client),
+        );
+
+      expect(result.isError).toBe(true);
+      expect(result.content).toEqual([
+        {
+          type: "text",
+          text: `Preview automation click found locator (${locator.length} characters), but it is disabled.`,
+        },
+      ]);
+    }),
+  ).pipe(Effect.provide(TestLayer)),
+);
+
 it.effect("terminates HTTP MCP sessions with DELETE", () =>
   Effect.scoped(
     Effect.gen(function* () {
diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts
index 3bc0fd71308e..24c2688f2fff 100644
--- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts
+++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts
@@ -6,6 +6,7 @@ import {
   PreviewAutomationInvalidSelectorError,
   PreviewAutomationMalformedResponseError,
   PreviewAutomationNoAvailableHostError,
+  PreviewAutomationTargetLookupError,
   PreviewAutomationTargetNotEditableError,
   PreviewTabId,
   ProviderInstanceId,
@@ -404,6 +405,55 @@ it.effect("classifies a remote non-editable target without collapsing it to exec
   );
 });
 
+it.effect("preserves a remote click lookup reason without exposing the locator", () => {
+  const locator = "role=button[name='request-secret']";
+  const remoteError = {
+    _tag: "PreviewAutomationTargetLookupError",
+    message: "The preview click target is not visible.",
+    detail: { failureKind: "hidden" },
+  } as const;
+
+  return Effect.scoped(
+    Effect.gen(function* () {
+      const broker = yield* makeBroker;
+      const requests = requestsFrom(yield* broker.connect(makeHost()));
+      yield* Stream.runForEach(requests, (request) =>
+        broker.respond({
+          clientId: "client-1",
+          connectionId: request.connectionId,
+          requestId: request.requestId,
+          ok: false,
+          error: remoteError,
+        }),
+      ).pipe(Effect.forkScoped);
+      yield* Effect.yieldNow;
+
+      const error = yield* broker
+        .invoke({
+          scope,
+          operation: "click",
+          input: { locator },
+          tabId: PreviewTabId.make("tab-1"),
+        })
+        .pipe(Effect.flip);
+
+      expect(error).toBeInstanceOf(PreviewAutomationTargetLookupError);
+      expect(error).toMatchObject({
+        operation: "click",
+        failureKind: "hidden",
+        selectorKind: "locator",
+        selectorLength: locator.length,
+        remoteTag: "PreviewAutomationTargetLookupError",
+      });
+      expect(error.message).toBe(
+        `Preview automation click found locator (${locator.length} characters), but it is not visible.`,
+      );
+      expect(error.message).not.toContain("request-secret");
+      expect(error.cause).toBe(remoteError);
+    }),
+  );
+});
+
 it.effect("distinguishes malformed remote failures", () =>
   Effect.scoped(
     Effect.gen(function* () {
diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts
index 3e9bfaac26ff..23928e18dc89 100644
--- a/apps/server/src/mcp/PreviewAutomationBroker.ts
+++ b/apps/server/src/mcp/PreviewAutomationBroker.ts
@@ -10,6 +10,8 @@ import {
   PreviewAutomationRequestQueueClosedError,
   PreviewAutomationResultTooLargeError,
   PreviewAutomationTabNotFoundError,
+  PreviewAutomationTargetLookupError,
+  PreviewAutomationTargetLookupFailureKind,
   PreviewAutomationTargetNotEditableError,
   PreviewAutomationTimeoutError,
   PreviewAutomationUnsupportedClientError,
@@ -183,6 +185,14 @@ function remoteDetailKind(detail: unknown): RemoteDetailKind {
   }
 }
 
+const PreviewAutomationTargetLookupRemoteDetail = Schema.Struct({
+  failureKind: PreviewAutomationTargetLookupFailureKind,
+  matchCount: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
+});
+const isPreviewAutomationTargetLookupRemoteDetail = Schema.is(
+  PreviewAutomationTargetLookupRemoteDetail,
+);
+
 const classifyResponseError = (
   context: PreviewAutomationRequestErrorContext,
   error: NonNullable,
@@ -255,6 +265,16 @@ const classifyResponseError = (
           : { selectorLength: remoteSelectorLength ?? context.selectorLength }),
       });
     }
+    case "PreviewAutomationTargetLookupError": {
+      if (!isPreviewAutomationTargetLookupRemoteDetail(error.detail)) break;
+      if (error.detail.failureKind === "ambiguous" && error.detail.matchCount === undefined) break;
+      return new PreviewAutomationTargetLookupError({
+        ...context,
+        ...remoteDiagnostics,
+        failureKind: error.detail.failureKind,
+        ...(error.detail.matchCount === undefined ? {} : { matchCount: error.detail.matchCount }),
+      });
+    }
     case "PreviewAutomationResultTooLargeError": {
       const detail =
         typeof error.detail === "object" && error.detail !== null ? error.detail : undefined;
@@ -278,11 +298,12 @@ const classifyResponseError = (
         ...remoteDiagnostics,
       });
     default:
-      return new PreviewAutomationExecutionError({
-        ...context,
-        ...remoteDiagnostics,
-      });
+      break;
   }
+  return new PreviewAutomationExecutionError({
+    ...context,
+    ...remoteDiagnostics,
+  });
 };
 
 export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
diff --git a/apps/web/src/components/preview/PreviewAutomationHosts.tsx b/apps/web/src/components/preview/PreviewAutomationHosts.tsx
index 1faf928b1cf5..b46447ba7278 100644
--- a/apps/web/src/components/preview/PreviewAutomationHosts.tsx
+++ b/apps/web/src/components/preview/PreviewAutomationHosts.tsx
@@ -52,6 +52,7 @@ import { useAtomCommand } from "~/state/use-atom-command";
 
 import { previewBridge } from "./previewBridge";
 import {
+  confirmPreviewAutomationClickTarget,
   PreviewAutomationOperationError,
   PreviewAutomationOverlayTimeoutError,
   PreviewAutomationRecordingNotActiveError,
@@ -595,9 +596,18 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
           }
           case "click": {
             const ready = await requireReadyTab();
-            return await ready.bridge.automation.click(
-              ready.runtimeTabId,
-              request.input as Parameters[1],
+            return confirmPreviewAutomationClickTarget(
+              await ready.bridge.automation.click(
+                ready.runtimeTabId,
+                request.input as Parameters[1],
+              ),
+              {
+                requestId: request.requestId,
+                operation: "click",
+                environmentId,
+                threadId: request.threadId,
+                tabId: ready.tabId,
+              },
             );
           }
           case "type": {
diff --git a/apps/web/src/components/preview/previewAutomationErrors.test.ts b/apps/web/src/components/preview/previewAutomationErrors.test.ts
index 70ba78977337..f165cd75809e 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.test.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.test.ts
@@ -1,9 +1,12 @@
 import { EnvironmentId, ThreadId } from "@t3tools/contracts";
 import { describe, expect, it } from "vite-plus/test";
 
-import { PreviewAutomationOperationError } from "./previewAutomationErrors";
+import {
+  confirmPreviewAutomationClickTarget,
+  PreviewAutomationTargetLookupHostError,
+} from "./previewAutomationErrors";
 
-describe("PreviewAutomationOperationError", () => {
+describe("confirmPreviewAutomationClickTarget", () => {
   const context = {
     requestId: "request-1",
     operation: "click" as const,
@@ -12,27 +15,40 @@ describe("PreviewAutomationOperationError", () => {
     tabId: "tab-1",
   };
 
-  it("maps typed not-found failures to a visible/disabled/ambiguous reason", () => {
-    const hidden = PreviewAutomationOperationError.fromCause({
-      ...context,
-      cause: { _tag: "PreviewAutomationTargetHiddenError" },
-    });
-    const disabled = PreviewAutomationOperationError.fromCause({
-      ...context,
-      cause: { _tag: "PreviewAutomationTargetDisabledError" },
-    });
-    const ambiguous = PreviewAutomationOperationError.fromCause({
-      ...context,
-      cause: { _tag: "PreviewAutomationTargetAmbiguousError", matchCount: 3 },
-    });
-    const missing = PreviewAutomationOperationError.fromCause({
-      ...context,
-      cause: { _tag: "PreviewAutomationTargetNotFoundError" },
+  const lookupError = (
+    result:
+      | { readonly _tag: "NotSent"; readonly reason: "target-missing" }
+      | { readonly _tag: "NotSent"; readonly reason: "target-hidden" }
+      | { readonly _tag: "NotSent"; readonly reason: "target-disabled" }
+      | {
+          readonly _tag: "NotSent";
+          readonly reason: "target-ambiguous";
+          readonly matchCount: number;
+        },
+  ) => {
+    try {
+      confirmPreviewAutomationClickTarget(result, context);
+      throw new Error("Expected click target confirmation to fail");
+    } catch (error) {
+      expect(error).toBeInstanceOf(PreviewAutomationTargetLookupHostError);
+      return error as PreviewAutomationTargetLookupHostError;
+    }
+  };
+
+  it("maps typed IPC outcomes to visible, disabled, ambiguous, and missing reasons", () => {
+    const hidden = lookupError({ _tag: "NotSent", reason: "target-hidden" });
+    const disabled = lookupError({ _tag: "NotSent", reason: "target-disabled" });
+    const ambiguous = lookupError({
+      _tag: "NotSent",
+      reason: "target-ambiguous",
+      matchCount: 3,
     });
+    const missing = lookupError({ _tag: "NotSent", reason: "target-missing" });
+
     expect(hidden.message).toContain("not visible");
     expect(disabled.message).toContain("disabled");
     expect(ambiguous.message).toContain("matched 3 elements");
-    expect(missing.message).toContain("could not find a target");
+    expect(missing.message).toContain("not found");
     expect(hidden.message).not.toContain("secret");
     expect(disabled.message).not.toContain("secret");
     expect(ambiguous.message).not.toContain("secret");
diff --git a/apps/web/src/components/preview/previewAutomationErrors.ts b/apps/web/src/components/preview/previewAutomationErrors.ts
index 85a9eddfb3fb..b9b5e932af98 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.ts
@@ -1,4 +1,5 @@
 import {
+  type DesktopPreviewAutomationClickResult,
   EnvironmentId,
   type PreviewAutomationHost,
   PreviewAutomationOperation,
@@ -140,86 +141,61 @@ const PreviewAutomationTargetHostFields = {
   environmentId: EnvironmentId,
   threadId: ThreadId,
   tabId: Schema.NullOr(PreviewTabId),
-  cause: Schema.Defect(),
 };
 
-const readTargetLookupKind = (
-  cause: unknown,
-): "missing" | "hidden" | "disabled" | "ambiguous" | null => {
-  if (typeof cause !== "object" || cause === null || !("_tag" in cause)) return null;
-  if (cause._tag === "PreviewAutomationTargetHiddenError") return "hidden";
-  if (cause._tag === "PreviewAutomationTargetDisabledError") return "disabled";
-  if (cause._tag === "PreviewAutomationTargetAmbiguousError") return "ambiguous";
-  if (cause._tag === "PreviewAutomationTargetNotFoundError") return "missing";
-  return null;
-};
-
-const readAmbiguousMatchCount = (cause: unknown): number => {
-  if (
-    typeof cause === "object" &&
-    cause !== null &&
-    "matchCount" in cause &&
-    typeof cause.matchCount === "number" &&
-    Number.isInteger(cause.matchCount) &&
-    cause.matchCount >= 0
-  ) {
-    return cause.matchCount;
-  }
-  return 0;
-};
-
-export class PreviewAutomationTargetNotFoundHostError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetNotFoundHostError",
-  PreviewAutomationTargetHostFields,
-) {
-  get responseTag() {
-    return "PreviewAutomationExecutionError" as const;
-  }
-
-  override get message(): string {
-    return `Preview automation ${this.operation} request ${this.requestId} could not find a target in tab ${this.tabId ?? "unassigned"}.`;
-  }
-}
-
-export class PreviewAutomationTargetHiddenHostError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetHiddenHostError",
-  PreviewAutomationTargetHostFields,
-) {
-  get responseTag() {
-    return "PreviewAutomationExecutionError" as const;
-  }
-
-  override get message(): string {
-    return `Preview automation ${this.operation} request ${this.requestId} found a target in tab ${this.tabId ?? "unassigned"}, but it is not visible.`;
-  }
-}
-
-export class PreviewAutomationTargetDisabledHostError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetDisabledHostError",
-  PreviewAutomationTargetHostFields,
-) {
-  get responseTag() {
-    return "PreviewAutomationExecutionError" as const;
-  }
-
-  override get message(): string {
-    return `Preview automation ${this.operation} request ${this.requestId} found a target in tab ${this.tabId ?? "unassigned"}, but it is disabled.`;
-  }
-}
-
-export class PreviewAutomationTargetAmbiguousHostError extends Schema.TaggedErrorClass()(
-  "PreviewAutomationTargetAmbiguousHostError",
+export class PreviewAutomationTargetLookupHostError extends Schema.TaggedErrorClass()(
+  "PreviewAutomationTargetLookupHostError",
   {
     ...PreviewAutomationTargetHostFields,
-    matchCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+    failureKind: Schema.Literals(["missing", "hidden", "disabled", "ambiguous"]),
+    matchCount: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
   },
 ) {
   get responseTag() {
-    return "PreviewAutomationExecutionError" as const;
+    return "PreviewAutomationTargetLookupError" as const;
   }
 
   override get message(): string {
-    return `Preview automation ${this.operation} request ${this.requestId} matched ${this.matchCount} elements in tab ${this.tabId ?? "unassigned"}.`;
+    if (this.failureKind === "hidden") return "The preview click target is not visible.";
+    if (this.failureKind === "disabled") return "The preview click target is disabled.";
+    if (this.failureKind === "ambiguous") {
+      return this.matchCount === undefined
+        ? "The preview click target matched multiple elements."
+        : `The preview click target matched ${this.matchCount} elements.`;
+    }
+    return "The preview click target was not found.";
+  }
+}
+
+export function confirmPreviewAutomationClickTarget(
+  result: DesktopPreviewAutomationClickResult | void,
+  context: PreviewAutomationOperationContext & { readonly operation: "click" },
+): DesktopPreviewAutomationClickResult | void {
+  if (result?._tag !== "NotSent") return result;
+  switch (result.reason) {
+    case "target-missing":
+      throw new PreviewAutomationTargetLookupHostError({
+        ...context,
+        failureKind: "missing",
+      });
+    case "target-hidden":
+      throw new PreviewAutomationTargetLookupHostError({
+        ...context,
+        failureKind: "hidden",
+      });
+    case "target-disabled":
+      throw new PreviewAutomationTargetLookupHostError({
+        ...context,
+        failureKind: "disabled",
+      });
+    case "target-ambiguous":
+      throw new PreviewAutomationTargetLookupHostError({
+        ...context,
+        failureKind: "ambiguous",
+        matchCount: result.matchCount,
+      });
+    default:
+      return result;
   }
 }
 
@@ -272,26 +248,6 @@ export class PreviewAutomationOperationError extends Schema.TaggedErrorClass
 {
   });
 
   it("maps hidden click targets to a named execution failure without leaking the locator", () => {
-    expect(
-      serializePreviewAutomationError(
-        {
-          _tag: "PreviewAutomationTargetHiddenError",
-          selector: "role=button[name='target-secret']",
-        },
-        {
-          requestId: "request-click",
-          operation: "click",
-          environmentId,
-          threadId,
-          tabId,
-        },
-      ),
-    ).toEqual({
-      _tag: "PreviewAutomationExecutionError",
-      message:
-        "Preview automation click request request-click found a target in tab tab-1, but it is not visible.",
+    const context = {
+      requestId: "request-click",
+      operation: "click" as const,
+      environmentId,
+      threadId,
+      tabId,
+    };
+    let error: unknown;
+    try {
+      confirmPreviewAutomationClickTarget({ _tag: "NotSent", reason: "target-hidden" }, context);
+    } catch (cause) {
+      error = cause;
+    }
+
+    const response = serializePreviewAutomationError(error, context);
+    expect(response).toEqual({
+      _tag: "PreviewAutomationTargetLookupError",
+      message: "The preview click target is not visible.",
       detail: {
         requestId: "request-click",
         operation: "click",
         environmentId: "environment-1",
         threadId: "thread-1",
         tabId: "tab-1",
+        failureKind: "hidden",
       },
     });
-    expect(
-      JSON.stringify(
-        serializePreviewAutomationError(
-          {
-            _tag: "PreviewAutomationTargetHiddenError",
-            selector: "role=button[name='target-secret']",
-          },
-          {
-            requestId: "request-click",
-            operation: "click",
-            environmentId,
-            threadId,
-            tabId,
-          },
-        ),
-      ),
-    ).not.toContain("secret");
+    expect(JSON.stringify(response)).not.toContain("target-secret");
   });
 
   it("maps desktop non-editable targets to the public typed response", () => {
diff --git a/packages/contracts/src/previewAutomation.ts b/packages/contracts/src/previewAutomation.ts
index e33615fa4c05..78048300538a 100644
--- a/packages/contracts/src/previewAutomation.ts
+++ b/packages/contracts/src/previewAutomation.ts
@@ -800,6 +800,46 @@ export class PreviewAutomationTargetNotEditableError extends Schema.TaggedErrorC
   }
 }
 
+export const PreviewAutomationTargetLookupFailureKind = Schema.Literals([
+  "missing",
+  "hidden",
+  "disabled",
+  "ambiguous",
+]);
+export type PreviewAutomationTargetLookupFailureKind =
+  typeof PreviewAutomationTargetLookupFailureKind.Type;
+
+export class PreviewAutomationTargetLookupError extends Schema.TaggedErrorClass()(
+  "PreviewAutomationTargetLookupError",
+  {
+    ...PreviewAutomationRequestErrorFields,
+    ...PreviewAutomationRemoteDiagnosticFields,
+    failureKind: PreviewAutomationTargetLookupFailureKind,
+    matchCount: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
+    selectorKind: Schema.optional(Schema.Literals(["locator", "selector"])),
+    selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
+  },
+) {
+  override get message(): string {
+    const target =
+      this.selectorKind === undefined || this.selectorLength === undefined
+        ? "target"
+        : `${this.selectorKind} (${this.selectorLength} characters)`;
+    if (this.failureKind === "hidden") {
+      return `Preview automation ${this.operation} found ${target}, but it is not visible.`;
+    }
+    if (this.failureKind === "disabled") {
+      return `Preview automation ${this.operation} found ${target}, but it is disabled.`;
+    }
+    if (this.failureKind === "ambiguous") {
+      return this.matchCount === undefined
+        ? `Preview automation ${this.operation} matched multiple elements for ${target}.`
+        : `Preview automation ${this.operation} matched ${this.matchCount} elements for ${target}.`;
+    }
+    return `Preview automation ${this.operation} could not find ${target}.`;
+  }
+}
+
 export class PreviewAutomationResultTooLargeError extends Schema.TaggedErrorClass()(
   "PreviewAutomationResultTooLargeError",
   {
@@ -866,6 +906,7 @@ export const PreviewAutomationError = Schema.Union([
   PreviewAutomationExecutionError,
   PreviewAutomationInvalidSelectorError,
   PreviewAutomationTargetNotEditableError,
+  PreviewAutomationTargetLookupError,
   PreviewAutomationResultTooLargeError,
   PreviewAutomationClientDisconnectedError,
   PreviewAutomationRequestQueueClosedError,

From 58f021f0368b8e3d2a7aad6f96bcb65c566aee78 Mon Sep 17 00:00:00 2001
From: Theo Browne 
Date: Tue, 1 Sep 2026 16:47:05 -0700
Subject: [PATCH 6/8] fix(preview): address click lookup review

---
 apps/desktop/src/preview/Manager.test.ts | 14 ++++++++++++++
 apps/desktop/src/preview/Manager.ts      | 11 ++++++-----
 2 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts
index 96575b3e2e41..1f5adb1a50e3 100644
--- a/apps/desktop/src/preview/Manager.test.ts
+++ b/apps/desktop/src/preview/Manager.test.ts
@@ -3748,6 +3748,20 @@ describe("Preview automation diagnostics", () => {
     expect("locator" in error).toBe(false);
   });
 
+  it("does not invent an ambiguous target match count", () => {
+    const error = new PreviewManager.PreviewAutomationTargetLookupError({
+      operation: "click",
+      tabId: "tab_1",
+      selectorKind: "locator",
+      selectorLength: 12,
+      failureKind: "ambiguous",
+    });
+
+    expect(error.message).toBe(
+      "Preview automation click matched multiple elements for locator (12 characters)",
+    );
+  });
+
   effectIt.effect("returns typed click lookup failures without dispatching input", () =>
     withManager((manager) =>
       Effect.gen(function* () {
diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts
index 6e3a9708f8af..7f9c25518f3c 100644
--- a/apps/desktop/src/preview/Manager.ts
+++ b/apps/desktop/src/preview/Manager.ts
@@ -3687,9 +3687,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
       );
       return { _tag: "Dispatched" } satisfies DesktopPreviewAutomationClickResult;
     }).pipe(
-      Effect.catchTag(
-        "PreviewAutomationTargetLookupError",
-        (
+      Effect.catchTags({
+        PreviewAutomationTargetLookupError: (
           error,
         ): Effect.Effect<
           DesktopPreviewAutomationClickResult,
@@ -3708,7 +3707,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
             reason: `target-${error.failureKind}`,
           });
         },
-      ),
+      }),
     );
   });
 
@@ -4341,7 +4340,9 @@ export class PreviewAutomationTargetLookupError extends Schema.TaggedErrorClass<
       return `Preview automation ${this.operation} found ${target}, but it is disabled`;
     }
     if (this.failureKind === "ambiguous") {
-      return `Preview automation ${this.operation} matched ${this.matchCount ?? 0} elements for ${target}`;
+      return this.matchCount === undefined
+        ? `Preview automation ${this.operation} matched multiple elements for ${target}`
+        : `Preview automation ${this.operation} matched ${this.matchCount} elements for ${target}`;
     }
     return `Preview automation ${this.operation} could not find ${target}`;
   }

From e06a2cd86146f2b323d64e002ccd64dbe4d358cf Mon Sep 17 00:00:00 2001
From: Theo Browne 
Date: Tue, 1 Sep 2026 16:58:01 -0700
Subject: [PATCH 7/8] fix(preview): use actual click selector matches

---
 apps/desktop/src/preview/Manager.test.ts | 62 ++++++++++++++++++++++--
 apps/desktop/src/preview/Manager.ts      | 18 ++-----
 2 files changed, 64 insertions(+), 16 deletions(-)

diff --git a/apps/desktop/src/preview/Manager.test.ts b/apps/desktop/src/preview/Manager.test.ts
index 1f5adb1a50e3..29e7228ba03f 100644
--- a/apps/desktop/src/preview/Manager.test.ts
+++ b/apps/desktop/src/preview/Manager.test.ts
@@ -2,6 +2,7 @@ import { it as effectIt } from "@effect/vitest";
 import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts";
 import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts";
 import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
+import * as NodeVM from "node:vm";
 import * as Cause from "effect/Cause";
 import * as Deferred from "effect/Deferred";
 import * as Effect from "effect/Effect";
@@ -3767,12 +3768,15 @@ describe("Preview automation diagnostics", () => {
       Effect.gen(function* () {
         const selector = "role=button[name='target-secret']";
         let lookupResult: unknown = { notFound: true, failureKind: "missing" };
+        let lookupExpression = "";
         const sendCommand = vi.fn(async (method: string, params?: Record) => {
           if (method !== "Runtime.evaluate") return undefined;
           const expression = String(params?.["expression"] ?? "");
-          return expression.includes("const parsed = injected.parseSelector")
-            ? { result: { value: lookupResult } }
-            : { result: { value: true } };
+          if (expression.includes("const parsed = injected.parseSelector")) {
+            lookupExpression = expression;
+            return { result: { value: lookupResult } };
+          }
+          return { result: { value: true } };
         });
         fromId.mockReturnValue({
           ...makeTestPreviewWebContents(
@@ -3817,6 +3821,58 @@ describe("Preview automation diagnostics", () => {
           expect.objectContaining({ status: "failed" }),
         ]);
         expect(sendCommand).not.toHaveBeenCalledWith("Input.dispatchMouseEvent", expect.anything());
+
+        const element = {
+          scrollIntoView: vi.fn(),
+          getBoundingClientRect: () => ({ left: 20, top: 10, width: 40, height: 20 }),
+        };
+        const runLookup = (
+          matches: ReadonlyArray,
+          state: { readonly visible: boolean; readonly enabled: boolean },
+        ) => {
+          const querySelectorAll = vi.fn(() => matches);
+          const elementState = vi.fn((_element: typeof element, name: keyof typeof state) => ({
+            matches: state[name],
+          }));
+          const result = NodeVM.runInNewContext(lookupExpression, {
+            document: {},
+            globalThis: {
+              __t3PlaywrightInjected: {
+                parseSelector: vi.fn(() => ({ parts: [] })),
+                querySelectorAll,
+                elementState,
+              },
+            },
+          });
+          return { result, querySelectorAll, elementState };
+        };
+
+        const missingLookup = runLookup([], { visible: true, enabled: true });
+        expect(missingLookup.result).toEqual({ notFound: true, failureKind: "missing" });
+        expect(missingLookup.querySelectorAll).toHaveBeenCalledOnce();
+
+        const ambiguousLookup = runLookup([element, element, element], {
+          visible: true,
+          enabled: true,
+        });
+        expect(ambiguousLookup.result).toEqual({
+          notFound: true,
+          failureKind: "ambiguous",
+          matchCount: 3,
+        });
+        expect(ambiguousLookup.querySelectorAll).toHaveBeenCalledOnce();
+
+        const hiddenLookup = runLookup([element], { visible: false, enabled: true });
+        expect(hiddenLookup.result).toEqual({ notFound: true, failureKind: "hidden" });
+        expect(hiddenLookup.elementState).toHaveBeenCalledWith(element, "visible");
+
+        const disabledLookup = runLookup([element], { visible: true, enabled: false });
+        expect(disabledLookup.result).toEqual({ notFound: true, failureKind: "disabled" });
+        expect(disabledLookup.elementState).toHaveBeenCalledWith(element, "enabled");
+
+        const visibleLookup = runLookup([element], { visible: true, enabled: true });
+        expect(visibleLookup.result).toEqual({ x: 40, y: 20 });
+        expect(element.scrollIntoView).toHaveBeenCalledWith({ block: "center", inline: "center" });
       }),
     ),
   );
diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts
index 7f9c25518f3c..a2c0f7730e94 100644
--- a/apps/desktop/src/preview/Manager.ts
+++ b/apps/desktop/src/preview/Manager.ts
@@ -3560,20 +3560,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
           try {
             const injected = globalThis.__t3PlaywrightInjected;
             const parsed = injected.parseSelector(${locatorJson});
-            let element;
-            try {
-              element = injected.querySelector(parsed, document, true);
-            } catch (error) {
-              const message = String(error);
-              if (message.toLowerCase().includes("strict mode")) {
-                const matches = injected.querySelectorAll
-                  ? injected.querySelectorAll(parsed, document)
-                  : [];
-                return { notFound: true, failureKind: "ambiguous", matchCount: matches.length || 2 };
-              }
-              throw error;
+            const matches = injected.querySelectorAll(parsed, document);
+            if (matches.length === 0) return { notFound: true, failureKind: "missing" };
+            if (matches.length > 1) {
+              return { notFound: true, failureKind: "ambiguous", matchCount: matches.length };
             }
-            if (!element) return { notFound: true, failureKind: "missing" };
+            const element = matches[0];
             const visible = injected.elementState(element, "visible");
             const enabled = injected.elementState(element, "enabled");
             if (!visible.matches) return { notFound: true, failureKind: "hidden" };

From def8129565b327f59842541578580bcbf5b0b9bd Mon Sep 17 00:00:00 2001
From: Theo Browne 
Date: Tue, 1 Sep 2026 17:16:18 -0700
Subject: [PATCH 8/8] fix(web): reject undispatched preview clicks

---
 .../preview/previewAutomationErrors.test.ts   | 38 ++++++++++++++++++-
 .../preview/previewAutomationErrors.ts        |  2 +-
 2 files changed, 38 insertions(+), 2 deletions(-)

diff --git a/apps/web/src/components/preview/previewAutomationErrors.test.ts b/apps/web/src/components/preview/previewAutomationErrors.test.ts
index f165cd75809e..df831819c933 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.test.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.test.ts
@@ -1,11 +1,18 @@
-import { EnvironmentId, ThreadId } from "@t3tools/contracts";
+import {
+  type DesktopPreviewAutomationClickResult,
+  EnvironmentId,
+  ThreadId,
+} from "@t3tools/contracts";
 import { describe, expect, it } from "vite-plus/test";
 
 import {
   confirmPreviewAutomationClickTarget,
+  PreviewAutomationOperationError,
   PreviewAutomationTargetLookupHostError,
 } from "./previewAutomationErrors";
 
+type NotSentClickResult = Extract;
+
 describe("confirmPreviewAutomationClickTarget", () => {
   const context = {
     requestId: "request-1",
@@ -53,4 +60,33 @@ describe("confirmPreviewAutomationClickTarget", () => {
     expect(disabled.message).not.toContain("secret");
     expect(ambiguous.message).not.toContain("secret");
   });
+
+  it("fails every NotSent outcome and preserves successful results", () => {
+    const results = [
+      { _tag: "NotSent", reason: "tab-not-visible" },
+      { _tag: "NotSent", reason: "timeout", timeoutMs: 5_000 },
+      { _tag: "NotSent", reason: "target-missing" },
+      { _tag: "NotSent", reason: "target-hidden" },
+      { _tag: "NotSent", reason: "target-disabled" },
+      { _tag: "NotSent", reason: "target-ambiguous", matchCount: 3 },
+    ] satisfies ReadonlyArray;
+
+    for (const result of results) {
+      expect(() => confirmPreviewAutomationClickTarget(result, context)).toThrow();
+    }
+
+    for (const result of results.slice(0, 2)) {
+      try {
+        confirmPreviewAutomationClickTarget(result, context);
+        throw new Error("Expected click target confirmation to fail");
+      } catch (error) {
+        expect(error).toBeInstanceOf(PreviewAutomationOperationError);
+        expect((error as PreviewAutomationOperationError).cause).toEqual(result);
+      }
+    }
+
+    const dispatched = { _tag: "Dispatched" } as const;
+    expect(confirmPreviewAutomationClickTarget(dispatched, context)).toBe(dispatched);
+    expect(confirmPreviewAutomationClickTarget(undefined, context)).toBeUndefined();
+  });
 });
diff --git a/apps/web/src/components/preview/previewAutomationErrors.ts b/apps/web/src/components/preview/previewAutomationErrors.ts
index b9b5e932af98..376f833ce534 100644
--- a/apps/web/src/components/preview/previewAutomationErrors.ts
+++ b/apps/web/src/components/preview/previewAutomationErrors.ts
@@ -195,7 +195,7 @@ export function confirmPreviewAutomationClickTarget(
         matchCount: result.matchCount,
       });
     default:
-      return result;
+      throw new PreviewAutomationOperationError({ ...context, cause: result });
   }
 }