From f6633fe1ba5d2ff7277be736204ee1358e22c0fa Mon Sep 17 00:00:00 2001
From: Nikhil Sonti <nikhilsv92@gmail.com>
Date: Wed, 23 Jul 2025 14:04:16 -0700
Subject: [PATCH] browseros api updates - 1

---
 chrome/browser/extensions/BUILD.gn            |   2 +
 .../api/browser_os/browser_os_api.cc          |  82 +---
 .../api/browser_os/browser_os_api_helpers.cc  | 432 ++++++++++++++++--
 .../api/browser_os/browser_os_api_helpers.h   |  53 ++-
 .../api/browser_os/browser_os_api_utils.cc    | 359 ++++-----------
 .../api/browser_os/browser_os_api_utils.h     |  54 ++-
 .../browser_os/browser_os_change_detector.cc  | 271 +++++++++++
 .../browser_os/browser_os_change_detector.h   | 120 +++++
 .../browser_os_snapshot_processor.cc          |  16 +-
 chrome/common/extensions/api/browser_os.idl   |  11 +-
 10 files changed, 990 insertions(+), 410 deletions(-)
 create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc
 create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_change_detector.h

diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
index 7b601eb14acab..37fc8b1650ca4 100644
--- a/chrome/browser/extensions/BUILD.gn
+++ b/chrome/browser/extensions/BUILD.gn
@@ -522,6 +522,8 @@ source_set("extensions") {
       "api/browser_os/browser_os_api_helpers.h",
       "api/browser_os/browser_os_api_utils.cc",
       "api/browser_os/browser_os_api_utils.h",
+      "api/browser_os/browser_os_change_detector.cc",
+      "api/browser_os/browser_os_change_detector.h",
       "api/browser_os/browser_os_content_processor.cc",
       "api/browser_os/browser_os_content_processor.h",
       "api/browser_os/browser_os_snapshot_processor.cc",
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.cc b/chrome/browser/extensions/api/browser_os/browser_os_api.cc
index 6bee5ce12bb9e..31d54b9d0fb58 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_api.cc
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.cc
@@ -18,6 +18,7 @@
 #include "base/values.h"
 #include "chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h"
 #include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_change_detector.h"
 #include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h"
 #include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
 #include "chrome/browser/extensions/extension_tab_util.h"
@@ -254,15 +255,13 @@ ExtensionFunction::ResponseAction BrowserOSClickFunction::Run() {
   
   const NodeInfo& node_info = node_it->second;
   
-  // Calculate click point (center of the element)
-  gfx::PointF click_point(
-      node_info.bounds.x() + node_info.bounds.width() / 2.0f,
-      node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+  // Perform click with change detection and retrying
+  ChangeDetectionResult change_result = Click(web_contents, node_info);
   
-  // Perform the click
-  PerformClick(web_contents, click_point);
+  // Convert result to API response
+  base::Value::Dict response = ChangeDetectionResultToDict(change_result);
   
-  return RespondNow(NoArguments());
+  return RespondNow(WithArguments(std::move(response)));
 }
 
 // Implementation of BrowserOSInputTextFunction
@@ -299,62 +298,11 @@ ExtensionFunction::ResponseAction BrowserOSInputTextFunction::Run() {
   
   
   // First, click on the element to focus it
-  gfx::PointF click_point(
-      node_info.bounds.x() + node_info.bounds.width() / 2.0f,
-      node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+  Click(web_contents, node_info);
   
-  PerformClick(web_contents, click_point);
-  
-  // Get render widget host for text input
-  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
-  if (!rfh) {
-    return RespondNow(Error("No render frame"));
-  }
-  
-  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
-  if (!rwh) {
-    return RespondNow(Error("No render widget host"));
-  }
-  
-  // Convert text to UTF16
-  std::u16string text16 = base::UTF8ToUTF16(params->text);
-  
-  // Add a small delay to ensure the element is focused after click
-  // Then send the text using ImeCommitText
-  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
-      FROM_HERE,
-      base::BindOnce(
-          [](content::RenderWidgetHost* rwh, const std::u16string& text) {
-            if (!rwh)
-              return;
-            
-            content::RenderWidgetHostImpl* rwhi = 
-                static_cast<content::RenderWidgetHostImpl*>(rwh);
-            
-            // Ensure the widget has focus
-            rwhi->Focus();
-            
-            // Try multiple approaches to input text
-            // 1. First try ImeSetComposition to simulate typing
-            rwhi->ImeSetComposition(text,
-                                   std::vector<ui::ImeTextSpan>(),
-                                   gfx::Range::InvalidRange(),
-                                   text.length(),  // selection_start at end
-                                   text.length()); // selection_end at end
-            
-            // 2. Then commit the text
-            rwhi->ImeCommitText(text,
-                                std::vector<ui::ImeTextSpan>(),
-                                gfx::Range::InvalidRange(),
-                                0);  // relative_cursor_pos = 0 means after the text
-            
-            // 3. Finish composing to ensure text is committed
-            rwhi->ImeFinishComposingText(false);
-            
-          },
-          rwh, text16),
-      base::Milliseconds(100));  // Increase delay to 100ms for better focus handling
   
+  // Type the text into the focused element
+  Type(web_contents, params->text);
   
   return RespondNow(NoArguments());
 }
@@ -392,11 +340,7 @@ ExtensionFunction::ResponseAction BrowserOSClearFunction::Run() {
   const NodeInfo& node_info = node_it->second;
   
   // First, click on the element to focus it
-  gfx::PointF click_point(
-      node_info.bounds.x() + node_info.bounds.width() / 2.0f,
-      node_info.bounds.y() + node_info.bounds.height() / 2.0f);
-  
-  PerformClick(web_contents, click_point);
+  Click(web_contents, node_info);
   
   // Get render widget host for keyboard events
   content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
@@ -508,7 +452,7 @@ ExtensionFunction::ResponseAction BrowserOSScrollUpFunction::Run() {
   int scroll_amount = viewport_bounds.height() * 0.9;  // 90% of viewport height
   
   // Perform scroll up (negative delta_y)
-  PerformScroll(web_contents, 0, -scroll_amount, true);
+  Scroll(web_contents, 0, -scroll_amount, true);
   
   return RespondNow(NoArguments());
 }
@@ -551,7 +495,7 @@ ExtensionFunction::ResponseAction BrowserOSScrollDownFunction::Run() {
   int scroll_amount = viewport_bounds.height() * 0.9;  // 90% of viewport height
   
   // Perform scroll down (positive delta_y)
-  PerformScroll(web_contents, 0, scroll_amount, true);
+  Scroll(web_contents, 0, scroll_amount, true);
   
   return RespondNow(NoArguments());
 }
@@ -664,7 +608,7 @@ ExtensionFunction::ResponseAction BrowserOSSendKeysFunction::Run() {
   }
   
   // Send the key
-  SendSpecialKey(web_contents, params->key);
+  KeyPress(web_contents, params->key);
   
   return RespondNow(NoArguments());
 }
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
index e66c5004440ae..2e2c9a875dd09 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc
@@ -4,60 +4,406 @@
 
 #include "chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h"
 
-#include "chrome/browser/extensions/extension_tab_util.h"
-#include "chrome/browser/extensions/window_controller.h"
-#include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/browser_finder.h"
-#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task/sequenced_task_runner.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_change_detector.h"
+#include "components/input/native_web_keyboard_event.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/browser/renderer_host/render_widget_host_impl.h"
+#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/web_contents.h"
+#include "third_party/blink/public/common/input/web_input_event.h"
+#include "third_party/blink/public/common/input/web_keyboard_event.h"
+#include "third_party/blink/public/common/input/web_mouse_event.h"
+#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
+#include "ui/base/ime/ime_text_span.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/events/keycodes/dom/dom_code.h"
+#include "ui/events/keycodes/dom/dom_key.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+#include "ui/gfx/geometry/point_f.h"
+#include "ui/gfx/range/range.h"
 
 namespace extensions {
 namespace api {
 
-std::optional<TabInfo> GetTabFromOptionalId(
-    std::optional<int> tab_id_param,
-    content::BrowserContext* browser_context,
-    bool include_incognito_information,
-    std::string* error_message) {
-  content::WebContents* web_contents = nullptr;
-  int tab_id = -1;
-  
-  if (tab_id_param) {
-    // Get specific tab by ID
-    WindowController* controller = nullptr;
-    int tab_index = -1;
-    if (!ExtensionTabUtil::GetTabById(*tab_id_param, browser_context,
-                                      include_incognito_information,
-                                      &controller, &web_contents,
-                                      &tab_index)) {
-      if (error_message) {
-        *error_message = "Tab not found";
+// Helper to create and dispatch mouse events for clicking
+void PointClick(content::WebContents* web_contents, 
+                  const gfx::PointF& point) {
+  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+  if (!rfh)
+    return;
+    
+  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+  if (!rwh)
+    return;
+    
+  content::RenderWidgetHostView* rwhv = rwh->GetView();
+  if (!rwhv)
+    return;
+
+  // Get viewport bounds for screen position calculation
+  gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+  gfx::PointF viewport_origin(viewport_bounds.x(), viewport_bounds.y());
+  
+  // The coordinates are already in widget space (CSS pixels)
+  gfx::PointF widget_point = point;
+
+  // Create mouse down event
+  blink::WebMouseEvent mouse_down;
+  mouse_down.SetType(blink::WebInputEvent::Type::kMouseDown);
+  mouse_down.button = blink::WebPointerProperties::Button::kLeft;
+  mouse_down.click_count = 1;
+  mouse_down.SetPositionInWidget(widget_point.x(), widget_point.y());
+  mouse_down.SetPositionInScreen(widget_point.x() + viewport_origin.x(), 
+                                widget_point.y() + viewport_origin.y());
+  mouse_down.SetTimeStamp(ui::EventTimeForNow());
+  mouse_down.SetModifiers(blink::WebInputEvent::kLeftButtonDown);
+  
+  // Create mouse up event
+  blink::WebMouseEvent mouse_up;
+  mouse_up.SetType(blink::WebInputEvent::Type::kMouseUp);
+  mouse_up.button = blink::WebPointerProperties::Button::kLeft;
+  mouse_up.click_count = 1;
+  mouse_up.SetPositionInWidget(widget_point.x(), widget_point.y());
+  mouse_up.SetPositionInScreen(widget_point.x() + viewport_origin.x(),
+                              widget_point.y() + viewport_origin.y());
+  mouse_up.SetTimeStamp(ui::EventTimeForNow());
+  
+  // Send the events
+  rwh->ForwardMouseEvent(mouse_down);
+  rwh->ForwardMouseEvent(mouse_up);
+}
+
+// Helper to perform HTML-based click using JS (uses ID, class, or tag)
+void HtmlClick(content::WebContents* web_contents,
+                      const NodeInfo& node_info) {
+  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+  if (!rfh)
+    return;
+    
+  // Build the JavaScript to find and click the element
+  std::u16string js_code = u"(function() {";
+  
+  // Try to find element by ID first
+  auto id_it = node_info.attributes.find("id");
+  if (id_it != node_info.attributes.end() && !id_it->second.empty()) {
+    js_code += u"  var element = document.getElementById('" + 
+               base::UTF8ToUTF16(id_it->second) + u"');";
+    js_code += u"  if (element) {";
+    js_code += u"    element.click();";
+    js_code += u"    return 'clicked by id';";
+    js_code += u"  }";
+  }
+  
+  // Try to find by class and tag combination
+  auto class_it = node_info.attributes.find("class");
+  auto tag_it = node_info.attributes.find("html-tag");
+  
+  if (class_it != node_info.attributes.end() && !class_it->second.empty() &&
+      tag_it != node_info.attributes.end() && !tag_it->second.empty()) {
+    // Split class names and create selector
+    std::string class_selector = "." + class_it->second;
+    // Replace spaces with dots for multiple classes
+    for (size_t i = 0; i < class_selector.length(); ++i) {
+      if (class_selector[i] == ' ') {
+        class_selector[i] = '.';
       }
-      return std::nullopt;
     }
-    tab_id = *tab_id_param;
+    
+    js_code += u"  var elements = document.querySelectorAll('" + 
+               base::UTF8ToUTF16(tag_it->second + class_selector) + u"');";
+    js_code += u"  if (elements.length > 0) {";
+    js_code += u"    elements[0].click();";
+    js_code += u"    return 'clicked by class and tag';";
+    js_code += u"  }";
+  }
+  
+  // Fallback: try just by tag name if available
+  if (tag_it != node_info.attributes.end() && !tag_it->second.empty()) {
+    js_code += u"  var elements = document.getElementsByTagName('" + 
+               base::UTF8ToUTF16(tag_it->second) + u"');";
+    js_code += u"  if (elements.length > 0) {";
+    js_code += u"    elements[0].click();";
+    js_code += u"    return 'clicked by tag';";
+    js_code += u"  }";
+  }
+  
+  js_code += u"  return 'no element found';";
+  js_code += u"})();";
+  
+  // Execute the JavaScript
+  rfh->ExecuteJavaScriptForTests(
+      js_code,
+      base::NullCallback(),
+      /*honor_js_content_settings=*/false);
+}
+
+// Helper to perform scroll actions using mouse wheel events
+void Scroll(content::WebContents* web_contents,
+                   int delta_x,
+                   int delta_y,
+                   bool precise) {
+  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+  if (!rfh)
+    return;
+    
+  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+  if (!rwh)
+    return;
+    
+  content::RenderWidgetHostView* rwhv = rwh->GetView();
+  if (!rwhv)
+    return;
+
+  // Get viewport bounds and center point
+  gfx::Rect viewport_bounds = rwhv->GetViewBounds();
+  gfx::PointF center_point(viewport_bounds.width() / 2.0f,
+                          viewport_bounds.height() / 2.0f);
+  
+  // Create mouse wheel event
+  blink::WebMouseWheelEvent wheel_event;
+  wheel_event.SetType(blink::WebInputEvent::Type::kMouseWheel);
+  wheel_event.SetPositionInWidget(center_point.x(), center_point.y());
+  wheel_event.SetPositionInScreen(center_point.x() + viewport_bounds.x(),
+                                 center_point.y() + viewport_bounds.y());
+  wheel_event.SetTimeStamp(ui::EventTimeForNow());
+  
+  // Set the scroll deltas
+  wheel_event.delta_x = delta_x;
+  wheel_event.delta_y = delta_y;
+  
+  // Set wheel tick values (120 = one notch)
+  wheel_event.wheel_ticks_x = delta_x / 120.0f;
+  wheel_event.wheel_ticks_y = delta_y / 120.0f;
+  
+  // Phase information for smooth scrolling
+  wheel_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
+  
+  // Precise scrolling for touchpad, non-precise for mouse wheel
+  if (precise) {
+    // For precise scrolling, deltas are in pixels
+    wheel_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel;
   } else {
-    // Get active tab
-    Browser* browser = chrome::FindLastActive();
-    if (!browser) {
-      if (error_message) {
-        *error_message = "No active browser";
-      }
-      return std::nullopt;
-    }
+    // For non-precise scrolling, deltas are in lines
+    wheel_event.delta_units = ui::ScrollGranularity::kScrollByLine;
+  }
+  
+  // Send the wheel event
+  rwh->ForwardWheelEvent(wheel_event);
+  
+  // Send phase ended event for smooth scrolling
+  wheel_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
+  wheel_event.delta_x = 0;
+  wheel_event.delta_y = 0;
+  wheel_event.wheel_ticks_x = 0;
+  wheel_event.wheel_ticks_y = 0;
+  rwh->ForwardWheelEvent(wheel_event);
+}
 
-    web_contents = browser->tab_strip_model()->GetActiveWebContents();
-    if (!web_contents) {
-      if (error_message) {
-        *error_message = "No active tab";
-      }
-      return std::nullopt;
-    }
-    tab_id = ExtensionTabUtil::GetTabId(web_contents);
+// Helper to send special key events
+void KeyPress(content::WebContents* web_contents,
+                    const std::string& key) {
+  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+  if (!rfh)
+    return;
+    
+  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+  if (!rwh)
+    return;
+
+  // Map key names to Windows key codes and DOM codes/keys
+  ui::KeyboardCode windows_key_code;
+  ui::DomCode dom_code;
+  ui::DomKey dom_key;
+  
+  // Use if-else chain to avoid static initialization
+  if (key == "Enter") {
+    windows_key_code = ui::VKEY_RETURN;
+    dom_code = ui::DomCode::ENTER;
+    dom_key = ui::DomKey::ENTER;
+  } else if (key == "Delete") {
+    windows_key_code = ui::VKEY_DELETE;
+    dom_code = ui::DomCode::DEL;
+    dom_key = ui::DomKey::DEL;
+  } else if (key == "Backspace") {
+    windows_key_code = ui::VKEY_BACK;
+    dom_code = ui::DomCode::BACKSPACE;
+    dom_key = ui::DomKey::BACKSPACE;
+  } else if (key == "Tab") {
+    windows_key_code = ui::VKEY_TAB;
+    dom_code = ui::DomCode::TAB;
+    dom_key = ui::DomKey::TAB;
+  } else if (key == "Escape") {
+    windows_key_code = ui::VKEY_ESCAPE;
+    dom_code = ui::DomCode::ESCAPE;
+    dom_key = ui::DomKey::ESCAPE;
+  } else if (key == "ArrowUp") {
+    windows_key_code = ui::VKEY_UP;
+    dom_code = ui::DomCode::ARROW_UP;
+    dom_key = ui::DomKey::ARROW_UP;
+  } else if (key == "ArrowDown") {
+    windows_key_code = ui::VKEY_DOWN;
+    dom_code = ui::DomCode::ARROW_DOWN;
+    dom_key = ui::DomKey::ARROW_DOWN;
+  } else if (key == "ArrowLeft") {
+    windows_key_code = ui::VKEY_LEFT;
+    dom_code = ui::DomCode::ARROW_LEFT;
+    dom_key = ui::DomKey::ARROW_LEFT;
+  } else if (key == "ArrowRight") {
+    windows_key_code = ui::VKEY_RIGHT;
+    dom_code = ui::DomCode::ARROW_RIGHT;
+    dom_key = ui::DomKey::ARROW_RIGHT;
+  } else if (key == "Home") {
+    windows_key_code = ui::VKEY_HOME;
+    dom_code = ui::DomCode::HOME;
+    dom_key = ui::DomKey::HOME;
+  } else if (key == "End") {
+    windows_key_code = ui::VKEY_END;
+    dom_code = ui::DomCode::END;
+    dom_key = ui::DomKey::END;
+  } else if (key == "PageUp") {
+    windows_key_code = ui::VKEY_PRIOR;
+    dom_code = ui::DomCode::PAGE_UP;
+    dom_key = ui::DomKey::PAGE_UP;
+  } else if (key == "PageDown") {
+    windows_key_code = ui::VKEY_NEXT;
+    dom_code = ui::DomCode::PAGE_DOWN;
+    dom_key = ui::DomKey::PAGE_DOWN;
+  } else {
+    return;  // Unsupported key
+  }
+  
+  // Create keyboard event
+  input::NativeWebKeyboardEvent key_down(
+      blink::WebInputEvent::Type::kKeyDown,
+      blink::WebInputEvent::kNoModifiers,
+      ui::EventTimeForNow());
+  
+  key_down.windows_key_code = windows_key_code;
+  key_down.native_key_code = windows_key_code;
+  key_down.dom_code = static_cast<int>(dom_code);
+  key_down.dom_key = static_cast<int>(dom_key);
+  
+  // Send key down
+  rwh->ForwardKeyboardEvent(key_down);
+  
+  // For Enter key, also send char event
+  // This is for `input` elements on web pages expect this to trigger submit
+  if (key == "Enter") {
+    input::NativeWebKeyboardEvent char_event(
+        blink::WebInputEvent::Type::kChar,
+        blink::WebInputEvent::kNoModifiers,
+        ui::EventTimeForNow());
+    
+    char_event.windows_key_code = windows_key_code;
+    char_event.native_key_code = windows_key_code;
+    char_event.dom_code = static_cast<int>(dom_code);
+    char_event.dom_key = static_cast<int>(dom_key);
+    char_event.text[0] = '\r';  // Carriage return character
+    char_event.unmodified_text[0] = '\r';
+    
+    rwh->ForwardKeyboardEvent(char_event);
+  }
+  
+  // For most keys, also send key up
+  if (key != "Tab") {  // Tab usually doesn't need key up for focus change
+    input::NativeWebKeyboardEvent key_up(
+        blink::WebInputEvent::Type::kKeyUp,
+        blink::WebInputEvent::kNoModifiers,
+        ui::EventTimeForNow());
+    
+    key_up.windows_key_code = windows_key_code;
+    key_up.native_key_code = windows_key_code;
+    key_up.dom_code = static_cast<int>(dom_code);
+    key_up.dom_key = static_cast<int>(dom_key);
+    
+    rwh->ForwardKeyboardEvent(key_up);
   }
+}
 
-  return TabInfo(web_contents, tab_id);
+// Helper to type text into a focused element
+void Type(content::WebContents* web_contents,
+          const std::string& text) {
+  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
+  if (!rfh)
+    return;
+    
+  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
+  if (!rwh)
+    return;
+  
+  // Convert text to UTF16
+  std::u16string text16 = base::UTF8ToUTF16(text);
+  
+  // Add a small delay to ensure the element is focused after click
+  // Then send the text using ImeCommitText
+  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](content::RenderWidgetHost* rwh, const std::u16string& text) {
+            if (!rwh)
+              return;
+            
+            content::RenderWidgetHostImpl* rwhi = 
+                static_cast<content::RenderWidgetHostImpl*>(rwh);
+            
+            // Ensure the widget has focus
+            rwhi->Focus();
+            
+            // Try multiple approaches to input text
+            // 1. First try ImeSetComposition to simulate typing
+            rwhi->ImeSetComposition(text,
+                                   std::vector<ui::ImeTextSpan>(),
+                                   gfx::Range::InvalidRange(),
+                                   text.length(),  // selection_start at end
+                                   text.length()); // selection_end at end
+            
+            // 2. Then commit the text
+            rwhi->ImeCommitText(text,
+                                std::vector<ui::ImeTextSpan>(),
+                                gfx::Range::InvalidRange(),
+                                0);  // relative_cursor_pos = 0 means after the text
+            
+            // 3. Finish composing to ensure text is committed
+            rwhi->ImeFinishComposingText(false);
+            
+          },
+          rwh, text16),
+      base::Milliseconds(100));  // Increase delay to 100ms for better focus handling
+}
+
+// Helper to perform a click with change detection and retrying
+ChangeDetectionResult Click(content::WebContents* web_contents,
+                           const NodeInfo& node_info) {
+  // Create change detector and start monitoring
+  auto change_detector = std::make_unique<BrowserOSChangeDetector>(web_contents);
+  change_detector->StartMonitoring(node_info.ax_tree_id);
+  
+  // Perform the click action using coordinate-based click
+  gfx::PointF click_point(
+      node_info.bounds.x() + node_info.bounds.width() / 2.0f,
+      node_info.bounds.y() + node_info.bounds.height() / 2.0f);
+  PointClick(web_contents, click_point);
+  
+  // Wait for changes with timeout
+  ChangeDetectionResult change_result = 
+      change_detector->WaitForChanges(base::Milliseconds(500));
+  
+  // If no change detected via coordinate click, try HTML click as fallback
+  if (!change_result.detected) {
+    VLOG(1) << "No change detected with coordinate click, trying HTML click";
+    HtmlClick(web_contents, node_info);
+    
+    // Wait again for changes
+    change_result = change_detector->WaitForChanges(base::Milliseconds(300));
+  }
+  
+  return change_result;
 }
 
 }  // namespace api
-}  // namespace extensions
\ No newline at end of file
+}  // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
index 142874b3d0374..ab8eb164a11c3 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
@@ -5,40 +5,49 @@
 #ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
 #define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
 
-#include <optional>
 #include <string>
 
-#include "base/memory/raw_ptr.h"
+#include "base/functional/callback.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_change_detector.h"
+#include "ui/gfx/geometry/point_f.h"
 
 namespace content {
-class BrowserContext;
 class WebContents;
 }  // namespace content
 
 namespace extensions {
+namespace api {
 
-class WindowController;
+struct NodeInfo;
 
-namespace api {
+// Helper to create and dispatch mouse events for clicking
+void PointClick(content::WebContents* web_contents, 
+                  const gfx::PointF& point);
+
+// Helper to perform HTML-based click using JS (uses ID, class, or tag)
+void HtmlClick(content::WebContents* web_contents,
+                      const NodeInfo& node_info);
+
+// Helper to perform scroll actions using mouse wheel events
+void Scroll(content::WebContents* web_contents,
+                   int delta_x,
+                   int delta_y,
+                   bool precise = false);
+
+// Helper to send special key events
+void KeyPress(content::WebContents* web_contents,
+                    const std::string& key);
+
+// Helper to type text into a focused element
+void Type(content::WebContents* web_contents,
+          const std::string& text);
 
-// Result structure for tab retrieval
-struct TabInfo {
-  raw_ptr<content::WebContents> web_contents;
-  int tab_id;
-  
-  TabInfo(content::WebContents* wc, int id) 
-      : web_contents(wc), tab_id(id) {}
-};
-
-// Helper to get WebContents and tab ID from optional tab_id parameter
-// Returns nullptr if tab is not found, with error message set
-std::optional<TabInfo> GetTabFromOptionalId(
-    std::optional<int> tab_id_param,
-    content::BrowserContext* browser_context,
-    bool include_incognito_information,
-    std::string* error_message);
+// Helper to perform a click with change detection and retrying
+// This combines change detection logic with click actions (coordinate and HTML)
+ChangeDetectionResult Click(content::WebContents* web_contents,
+                           const NodeInfo& node_info);
 
 }  // namespace api
 }  // namespace extensions
 
-#endif  // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
\ No newline at end of file
+#endif  // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
index 0d83f4da95ba9..1b2f83a233844 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
@@ -7,25 +7,26 @@
 #include "base/hash/hash.h"
 #include "base/no_destructor.h"
 #include "base/strings/string_number_conversions.h"
-#include "content/public/browser/render_frame_host.h"
-#include "content/public/browser/render_widget_host.h"
-#include "content/public/browser/render_widget_host_view.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/extensions/window_controller.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "content/public/browser/web_contents.h"
-#include "components/input/native_web_keyboard_event.h"
-#include "third_party/blink/public/common/input/web_input_event.h"
-#include "third_party/blink/public/common/input/web_keyboard_event.h"
-#include "third_party/blink/public/common/input/web_mouse_event.h"
-#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
 #include "ui/accessibility/ax_role_properties.h"
-#include "ui/events/base_event_utils.h"
-#include "ui/events/keycodes/dom/dom_code.h"
-#include "ui/events/keycodes/dom/dom_key.h"
-#include "ui/events/keycodes/keyboard_codes.h"
-#include "ui/gfx/geometry/point_f.h"
 
 namespace extensions {
 namespace api {
 
+// NodeInfo implementation
+NodeInfo::NodeInfo() : ax_node_id(0), ax_tree_id() {}
+NodeInfo::~NodeInfo() = default;
+NodeInfo::NodeInfo(const NodeInfo&) = default;
+NodeInfo& NodeInfo::operator=(const NodeInfo&) = default;
+NodeInfo::NodeInfo(NodeInfo&&) = default;
+NodeInfo& NodeInfo::operator=(NodeInfo&&) = default;
+
 // Global node ID mappings storage
 // Use NoDestructor to avoid exit-time destructor
 std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>& 
@@ -35,52 +36,49 @@ GetNodeIdMappings() {
   return *g_node_id_mappings;
 }
 
-// Helper to create and dispatch mouse events for clicking
-void PerformClick(content::WebContents* web_contents, 
-                  const gfx::PointF& point) {
-  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
-  if (!rfh)
-    return;
-    
-  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
-  if (!rwh)
-    return;
-    
-  content::RenderWidgetHostView* rwhv = rwh->GetView();
-  if (!rwhv)
-    return;
+std::optional<TabInfo> GetTabFromOptionalId(
+    std::optional<int> tab_id_param,
+    content::BrowserContext* browser_context,
+    bool include_incognito_information,
+    std::string* error_message) {
+  content::WebContents* web_contents = nullptr;
+  int tab_id = -1;
+  
+  if (tab_id_param) {
+    // Get specific tab by ID
+    WindowController* controller = nullptr;
+    int tab_index = -1;
+    if (!ExtensionTabUtil::GetTabById(*tab_id_param, browser_context,
+                                      include_incognito_information,
+                                      &controller, &web_contents,
+                                      &tab_index)) {
+      if (error_message) {
+        *error_message = "Tab not found";
+      }
+      return std::nullopt;
+    }
+    tab_id = *tab_id_param;
+  } else {
+    // Get active tab
+    Browser* browser = chrome::FindLastActive();
+    if (!browser) {
+      if (error_message) {
+        *error_message = "No active browser";
+      }
+      return std::nullopt;
+    }
 
-  // Get viewport bounds for screen position calculation
-  gfx::Rect viewport_bounds = rwhv->GetViewBounds();
-  gfx::PointF viewport_origin(viewport_bounds.x(), viewport_bounds.y());
-  
-  // The coordinates are already in widget space (CSS pixels)
-  gfx::PointF widget_point = point;
+    web_contents = browser->tab_strip_model()->GetActiveWebContents();
+    if (!web_contents) {
+      if (error_message) {
+        *error_message = "No active tab";
+      }
+      return std::nullopt;
+    }
+    tab_id = ExtensionTabUtil::GetTabId(web_contents);
+  }
 
-  // Create mouse down event
-  blink::WebMouseEvent mouse_down;
-  mouse_down.SetType(blink::WebInputEvent::Type::kMouseDown);
-  mouse_down.button = blink::WebPointerProperties::Button::kLeft;
-  mouse_down.click_count = 1;
-  mouse_down.SetPositionInWidget(widget_point.x(), widget_point.y());
-  mouse_down.SetPositionInScreen(widget_point.x() + viewport_origin.x(), 
-                                widget_point.y() + viewport_origin.y());
-  mouse_down.SetTimeStamp(ui::EventTimeForNow());
-  mouse_down.SetModifiers(blink::WebInputEvent::kLeftButtonDown);
-  
-  // Create mouse up event
-  blink::WebMouseEvent mouse_up;
-  mouse_up.SetType(blink::WebInputEvent::Type::kMouseUp);
-  mouse_up.button = blink::WebPointerProperties::Button::kLeft;
-  mouse_up.click_count = 1;
-  mouse_up.SetPositionInWidget(widget_point.x(), widget_point.y());
-  mouse_up.SetPositionInScreen(widget_point.x() + viewport_origin.x(),
-                              widget_point.y() + viewport_origin.y());
-  mouse_up.SetTimeStamp(ui::EventTimeForNow());
-  
-  // Send the events
-  rwh->ForwardMouseEvent(mouse_down);
-  rwh->ForwardMouseEvent(mouse_up);
+  return TabInfo(web_contents, tab_id);
 }
 
 // Helper to determine if a node is interactive (clickable/typeable/selectable)
@@ -128,50 +126,6 @@ browser_os::InteractiveNodeType GetInteractiveNodeType(
   return browser_os::InteractiveNodeType::kOther;
 }
 
-// Helper to compute branch path hash for an AX node
-uint64_t ComputeBranchPath(const ui::AXNodeData& node_data,
-                           const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
-                           std::unordered_map<int32_t, uint64_t>& path_cache) {
-  // Check cache first
-  auto it = path_cache.find(node_data.id);
-  if (it != path_cache.end()) {
-    return it->second;
-  }
-  
-  // Build path from root to this node
-  std::vector<int32_t> path;
-  int32_t current_id = node_data.id;
-  
-  while (current_id != 0) {
-    path.push_back(current_id);
-    
-    // Find parent
-    bool found_parent = false;
-    for (const auto& [id, data] : node_map) {
-      if (std::find(data.child_ids.begin(), data.child_ids.end(), current_id) != 
-          data.child_ids.end()) {
-        current_id = id;
-        found_parent = true;
-        break;
-      }
-    }
-    
-    if (!found_parent) {
-      break;
-    }
-  }
-  
-  // Compute hash from path
-  std::string path_str;
-  for (auto path_it = path.rbegin(); path_it != path.rend(); ++path_it) {
-    path_str += base::NumberToString(*path_it) + "/";
-  }
-  
-  uint64_t hash = base::PersistentHash(path_str);
-  path_cache[node_data.id] = hash;
-  return hash;
-}
-
 // Helper to get the HTML tag name from AX role
 std::string GetTagFromRole(ax::mojom::Role role) {
   switch (role) {
@@ -209,169 +163,58 @@ std::string GetTagFromRole(ax::mojom::Role role) {
   }
 }
 
-// Helper to perform scroll actions using mouse wheel events
-void PerformScroll(content::WebContents* web_contents,
-                   int delta_x,
-                   int delta_y,
-                   bool precise) {
-  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
-  if (!rfh)
-    return;
-    
-  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
-  if (!rwh)
-    return;
-    
-  content::RenderWidgetHostView* rwhv = rwh->GetView();
-  if (!rwhv)
-    return;
-
-  // Get viewport bounds and center point
-  gfx::Rect viewport_bounds = rwhv->GetViewBounds();
-  gfx::PointF center_point(viewport_bounds.width() / 2.0f,
-                          viewport_bounds.height() / 2.0f);
-  
-  // Create mouse wheel event
-  blink::WebMouseWheelEvent wheel_event;
-  wheel_event.SetType(blink::WebInputEvent::Type::kMouseWheel);
-  wheel_event.SetPositionInWidget(center_point.x(), center_point.y());
-  wheel_event.SetPositionInScreen(center_point.x() + viewport_bounds.x(),
-                                 center_point.y() + viewport_bounds.y());
-  wheel_event.SetTimeStamp(ui::EventTimeForNow());
-  
-  // Set the scroll deltas
-  wheel_event.delta_x = delta_x;
-  wheel_event.delta_y = delta_y;
-  
-  // Set wheel tick values (120 = one notch)
-  wheel_event.wheel_ticks_x = delta_x / 120.0f;
-  wheel_event.wheel_ticks_y = delta_y / 120.0f;
-  
-  // Phase information for smooth scrolling
-  wheel_event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
-  
-  // Precise scrolling for touchpad, non-precise for mouse wheel
-  if (precise) {
-    // For precise scrolling, deltas are in pixels
-    wheel_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel;
-  } else {
-    // For non-precise scrolling, deltas are in lines
-    wheel_event.delta_units = ui::ScrollGranularity::kScrollByLine;
+// Helper to convert ChangeType enum to string
+std::string ChangeTypeToString(ChangeType change_type) {
+  switch (change_type) {
+    case ChangeType::kDomChanged:
+      return "dom_changed";
+    case ChangeType::kPopupOpened:
+      return "popup_opened";
+    case ChangeType::kNewTabOpened:
+      return "new_tab_opened";
+    case ChangeType::kDialogShown:
+      return "dialog_shown";
+    case ChangeType::kFocusChanged:
+      return "focus_changed";
+    case ChangeType::kElementExpanded:
+      return "element_expanded";
+    case ChangeType::kNone:
+    default:
+      return "unknown";
   }
-  
-  // Send the wheel event
-  rwh->ForwardWheelEvent(wheel_event);
-  
-  // Send phase ended event for smooth scrolling
-  wheel_event.phase = blink::WebMouseWheelEvent::kPhaseEnded;
-  wheel_event.delta_x = 0;
-  wheel_event.delta_y = 0;
-  wheel_event.wheel_ticks_x = 0;
-  wheel_event.wheel_ticks_y = 0;
-  rwh->ForwardWheelEvent(wheel_event);
 }
 
-// Helper to send special key events
-void SendSpecialKey(content::WebContents* web_contents,
-                    const std::string& key) {
-  content::RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
-  if (!rfh)
-    return;
+// Helper to convert ChangeDetectionResult to API response
+base::Value::Dict ChangeDetectionResultToDict(const ChangeDetectionResult& result) {
+  base::Value::Dict response;
+  response.Set("success", true);
+  response.Set("changeDetected", result.detected);
+  
+  if (result.detected) {
+    // Convert primary change type to string
+    response.Set("primaryChange", ChangeTypeToString(result.primary_change));
+    response.Set("timeToChangeMs", 
+                static_cast<int>(result.time_to_change.InMilliseconds()));
     
-  content::RenderWidgetHost* rwh = rfh->GetRenderWidgetHost();
-  if (!rwh)
-    return;
-
-  // Map key names to Windows key codes and DOM codes/keys
-  ui::KeyboardCode windows_key_code;
-  ui::DomCode dom_code;
-  ui::DomKey dom_key;
-  
-  // Use if-else chain to avoid static initialization
-  if (key == "Enter") {
-    windows_key_code = ui::VKEY_RETURN;
-    dom_code = ui::DomCode::ENTER;
-    dom_key = ui::DomKey::ENTER;
-  } else if (key == "Delete") {
-    windows_key_code = ui::VKEY_DELETE;
-    dom_code = ui::DomCode::DEL;
-    dom_key = ui::DomKey::DEL;
-  } else if (key == "Backspace") {
-    windows_key_code = ui::VKEY_BACK;
-    dom_code = ui::DomCode::BACKSPACE;
-    dom_key = ui::DomKey::BACKSPACE;
-  } else if (key == "Tab") {
-    windows_key_code = ui::VKEY_TAB;
-    dom_code = ui::DomCode::TAB;
-    dom_key = ui::DomKey::TAB;
-  } else if (key == "Escape") {
-    windows_key_code = ui::VKEY_ESCAPE;
-    dom_code = ui::DomCode::ESCAPE;
-    dom_key = ui::DomKey::ESCAPE;
-  } else if (key == "ArrowUp") {
-    windows_key_code = ui::VKEY_UP;
-    dom_code = ui::DomCode::ARROW_UP;
-    dom_key = ui::DomKey::ARROW_UP;
-  } else if (key == "ArrowDown") {
-    windows_key_code = ui::VKEY_DOWN;
-    dom_code = ui::DomCode::ARROW_DOWN;
-    dom_key = ui::DomKey::ARROW_DOWN;
-  } else if (key == "ArrowLeft") {
-    windows_key_code = ui::VKEY_LEFT;
-    dom_code = ui::DomCode::ARROW_LEFT;
-    dom_key = ui::DomKey::ARROW_LEFT;
-  } else if (key == "ArrowRight") {
-    windows_key_code = ui::VKEY_RIGHT;
-    dom_code = ui::DomCode::ARROW_RIGHT;
-    dom_key = ui::DomKey::ARROW_RIGHT;
-  } else if (key == "Home") {
-    windows_key_code = ui::VKEY_HOME;
-    dom_code = ui::DomCode::HOME;
-    dom_key = ui::DomKey::HOME;
-  } else if (key == "End") {
-    windows_key_code = ui::VKEY_END;
-    dom_code = ui::DomCode::END;
-    dom_key = ui::DomKey::END;
-  } else if (key == "PageUp") {
-    windows_key_code = ui::VKEY_PRIOR;
-    dom_code = ui::DomCode::PAGE_UP;
-    dom_key = ui::DomKey::PAGE_UP;
-  } else if (key == "PageDown") {
-    windows_key_code = ui::VKEY_NEXT;
-    dom_code = ui::DomCode::PAGE_DOWN;
-    dom_key = ui::DomKey::PAGE_DOWN;
-  } else {
-    return;  // Unsupported key
-  }
-  
-  // Create keyboard event
-  input::NativeWebKeyboardEvent key_down(
-      blink::WebInputEvent::Type::kKeyDown,
-      blink::WebInputEvent::kNoModifiers,
-      ui::EventTimeForNow());
-  
-  key_down.windows_key_code = windows_key_code;
-  key_down.native_key_code = windows_key_code;
-  key_down.dom_code = static_cast<int>(dom_code);
-  key_down.dom_key = static_cast<int>(dom_key);
-  
-  // Send key down
-  rwh->ForwardKeyboardEvent(key_down);
-  
-  // For most keys, also send key up
-  if (key != "Tab") {  // Tab usually doesn't need key up for focus change
-    input::NativeWebKeyboardEvent key_up(
-        blink::WebInputEvent::Type::kKeyUp,
-        blink::WebInputEvent::kNoModifiers,
-        ui::EventTimeForNow());
-    
-    key_up.windows_key_code = windows_key_code;
-    key_up.native_key_code = windows_key_code;
-    key_up.dom_code = static_cast<int>(dom_code);
-    key_up.dom_key = static_cast<int>(dom_key);
+    // Add all detected changes
+    base::Value::List all_changes;
+    for (const auto& change : result.all_changes) {
+      std::string change_str = ChangeTypeToString(change);
+      if (change_str != "unknown") {
+        all_changes.Append(change_str);
+      }
+    }
+    response.Set("allChanges", std::move(all_changes));
     
-    rwh->ForwardKeyboardEvent(key_up);
+    // Add action required hints
+    if (result.primary_change == ChangeType::kNewTabOpened) {
+      response.Set("actionRequired", "switch_to_new_tab");
+    } else if (result.primary_change == ChangeType::kPopupOpened) {
+      response.Set("actionRequired", "interact_with_popup");
+    }
   }
+  
+  return response;
 }
 
 }  // namespace api
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
index 2070807877455..403633772e2fe 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
@@ -5,56 +5,78 @@
 #ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
 #define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
 
+#include <optional>
 #include <string>
 #include <unordered_map>
 
+#include "base/memory/raw_ptr.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/api/browser_os/browser_os_change_detector.h"
 #include "chrome/common/extensions/api/browser_os.h"
 #include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_tree_id.h"
 #include "ui/gfx/geometry/rect_f.h"
 
 namespace content {
+class BrowserContext;
 class RenderWidgetHost;
 class WebContents;
 }  // namespace content
 
 namespace extensions {
+
+class WindowController;
+
 namespace api {
 
+// Result structure for tab retrieval
+struct TabInfo {
+  raw_ptr<content::WebContents> web_contents;
+  int tab_id;
+  
+  TabInfo(content::WebContents* wc, int id) 
+      : web_contents(wc), tab_id(id) {}
+};
+
 // Stores mapping information for a node
 struct NodeInfo {
+  NodeInfo();
+  ~NodeInfo();
+  NodeInfo(const NodeInfo&);
+  NodeInfo& operator=(const NodeInfo&);
+  NodeInfo(NodeInfo&&);
+  NodeInfo& operator=(NodeInfo&&);
+
   int32_t ax_node_id;
+  ui::AXTreeID ax_tree_id;  // Tree ID for change detection
   gfx::RectF bounds;  // Absolute bounds in CSS pixels
+  std::unordered_map<std::string, std::string> attributes;  // All computed attributes
 };
 
 // Global node ID mappings storage
 std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>& 
 GetNodeIdMappings();
 
-// Helper to create and dispatch mouse events for clicking
-void PerformClick(content::WebContents* web_contents, 
-                  const gfx::PointF& point);
+// Helper to get WebContents and tab ID from optional tab_id parameter
+// Returns nullptr if tab is not found, with error message set
+std::optional<TabInfo> GetTabFromOptionalId(
+    std::optional<int> tab_id_param,
+    content::BrowserContext* browser_context,
+    bool include_incognito_information,
+    std::string* error_message);
 
 // Helper to determine if a node is interactive (clickable/typable)
 browser_os::InteractiveNodeType GetInteractiveNodeType(
     const ui::AXNodeData& node_data);
 
-// Helper to compute branch path hash for an AX node
-uint64_t ComputeBranchPath(const ui::AXNodeData& node_data,
-                          const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
-                          std::unordered_map<int32_t, uint64_t>& path_cache);
-
 // Helper to get the HTML tag name from AX role
 std::string GetTagFromRole(ax::mojom::Role role);
 
-// Helper to perform scroll actions using mouse wheel events
-void PerformScroll(content::WebContents* web_contents,
-                   int delta_x,
-                   int delta_y,
-                   bool precise = false);
+// Helper to convert ChangeType enum to string
+std::string ChangeTypeToString(ChangeType change_type);
 
-// Helper to send special key events
-void SendSpecialKey(content::WebContents* web_contents,
-                    const std::string& key);
+// Helper to convert ChangeDetectionResult to API response
+base::Value::Dict ChangeDetectionResultToDict(const ChangeDetectionResult& result);
 
 }  // namespace api
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc b/chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc
new file mode 100644
index 0000000000000..7962fb78b6e48
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc
@@ -0,0 +1,271 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/extensions/api/browser_os/browser_os_change_detector.h"
+
+#include "base/functional/bind.h"
+#include "base/logging.h"
+#include "base/run_loop.h"
+#include "content/public/browser/focused_node_details.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "ui/accessibility/ax_enums.mojom.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_tree_update.h"
+#include "ui/accessibility/ax_updates_and_events.h"
+
+namespace extensions {
+namespace api {
+
+// ChangeDetectionResult implementation
+ChangeDetectionResult::ChangeDetectionResult() = default;
+ChangeDetectionResult::~ChangeDetectionResult() = default;
+ChangeDetectionResult::ChangeDetectionResult(const ChangeDetectionResult&) = default;
+ChangeDetectionResult& ChangeDetectionResult::operator=(const ChangeDetectionResult&) = default;
+ChangeDetectionResult::ChangeDetectionResult(ChangeDetectionResult&&) = default;
+ChangeDetectionResult& ChangeDetectionResult::operator=(ChangeDetectionResult&&) = default;
+
+BrowserOSChangeDetector::BrowserOSChangeDetector(
+    content::WebContents* web_contents)
+    : content::WebContentsObserver(web_contents) {}
+
+BrowserOSChangeDetector::~BrowserOSChangeDetector() {
+  LOG(INFO) << "BrowserOSChangeDetector destroyed";
+  StopMonitoring();
+}
+
+void BrowserOSChangeDetector::StartMonitoring(
+    const ui::AXTreeID& initial_tree_id) {
+  DCHECK(!monitoring_active_);
+  
+  monitoring_active_ = true;
+  change_detected_ = false;
+  initial_tree_id_ = initial_tree_id;
+  current_tree_id_ = initial_tree_id;
+  detected_changes_.clear();
+  start_time_ = base::TimeTicks::Now();
+  time_to_first_change_ = base::TimeDelta();
+  
+  VLOG(1) << "Started monitoring for changes, initial tree ID: " 
+          << initial_tree_id.ToString();
+}
+
+ChangeDetectionResult BrowserOSChangeDetector::WaitForChanges(
+    base::TimeDelta timeout) {
+  DCHECK(monitoring_active_);
+  
+  // If changes already detected, return immediately
+  if (change_detected_) {
+    return GetResult();
+  }
+  
+  // Set up a run loop to wait for changes or timeout
+  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
+  wait_callback_ = run_loop.QuitClosure();
+  
+  // Start timeout timer
+  timeout_timer_.Start(FROM_HERE, timeout,
+                      base::BindOnce(&BrowserOSChangeDetector::OnTimeout,
+                                    weak_factory_.GetWeakPtr()));
+  
+  // Wait for changes or timeout
+  run_loop.Run();
+  
+  // Clean up
+  timeout_timer_.Stop();
+  wait_callback_.Reset();
+  
+  return GetResult();
+}
+
+ChangeDetectionResult BrowserOSChangeDetector::GetResult() const {
+  ChangeDetectionResult result;
+  result.detected = change_detected_;
+  result.all_changes = detected_changes_;
+  result.new_tree_id = current_tree_id_;
+  result.time_to_change = time_to_first_change_;
+  
+  // Determine primary change type
+  if (!detected_changes_.empty()) {
+    VLOG(1) << "BrowserOSChangeDetector detected changes: "
+              << static_cast<int>(detected_changes_.size());
+    // Priority order for primary change
+    if (detected_changes_.count(ChangeType::kNewTabOpened)) {
+      result.primary_change = ChangeType::kNewTabOpened;
+    } else if (detected_changes_.count(ChangeType::kPopupOpened)) {
+      result.primary_change = ChangeType::kPopupOpened;
+    } else if (detected_changes_.count(ChangeType::kDialogShown)) {
+      result.primary_change = ChangeType::kDialogShown;
+    } else if (detected_changes_.count(ChangeType::kElementExpanded)) {
+      result.primary_change = ChangeType::kElementExpanded;
+    } else if (detected_changes_.count(ChangeType::kDomChanged)) {
+      result.primary_change = ChangeType::kDomChanged;
+    } else if (detected_changes_.count(ChangeType::kFocusChanged)) {
+      result.primary_change = ChangeType::kFocusChanged;
+    }
+  }
+  else {
+    LOG(INFO) << "BrowserOSChangeDetector empty detected changes";
+  }
+  
+  return result;
+}
+
+void BrowserOSChangeDetector::AccessibilityEventReceived(
+    const ui::AXUpdatesAndEvents& details) {
+  if (!monitoring_active_) {
+    return;
+  }
+  
+  ProcessAccessibilityEvent(details);
+}
+
+void BrowserOSChangeDetector::ProcessAccessibilityEvent(
+    const ui::AXUpdatesAndEvents& details) {
+  bool significant_change = false;
+  
+  // Process each tree update
+  for (size_t i = 0; i < details.updates.size(); ++i) {
+    const ui::AXTreeUpdate& update = details.updates[i];
+    
+    // Check if tree ID changed
+    if (update.has_tree_data && update.tree_data.tree_id != initial_tree_id_) {
+      current_tree_id_ = update.tree_data.tree_id;
+      significant_change = true;
+      VLOG(1) << "Tree ID changed from " << initial_tree_id_.ToString() 
+              << " to " << current_tree_id_.ToString();
+    }
+    
+    // Check for specific event types from the corresponding event
+    if (i < details.events.size()) {
+      const ui::AXEvent& event = details.events[i];
+      switch (event.event_type) {
+        case ax::mojom::Event::kChildrenChanged:
+        case ax::mojom::Event::kLayoutComplete:
+        case ax::mojom::Event::kLoadComplete:
+          detected_changes_.insert(ChangeType::kDomChanged);
+          significant_change = true;
+          break;
+          
+        case ax::mojom::Event::kFocus:
+        case ax::mojom::Event::kFocusContext:
+        case ax::mojom::Event::kDocumentSelectionChanged:
+          detected_changes_.insert(ChangeType::kFocusChanged);
+          significant_change = true;
+          break;
+          
+        case ax::mojom::Event::kExpandedChanged:
+        case ax::mojom::Event::kRowExpanded:
+        case ax::mojom::Event::kRowCollapsed:
+          detected_changes_.insert(ChangeType::kElementExpanded);
+          significant_change = true;
+          break;
+          
+        default:
+          break;
+      }
+    }
+    
+    // Check for popup/dialog indicators in node data
+    for (const auto& node : update.nodes) {
+      if (node.role == ax::mojom::Role::kDialog ||
+          node.role == ax::mojom::Role::kAlertDialog ||
+          node.role == ax::mojom::Role::kAlert) {
+        // Check if this is a new node (not in initial tree)
+        if (!node.IsInvisibleOrIgnored()) {
+          detected_changes_.insert(ChangeType::kPopupOpened);
+          significant_change = true;
+        }
+      }
+      
+      if (node.role == ax::mojom::Role::kMenu ||
+          node.role == ax::mojom::Role::kMenuBar ||
+          node.role == ax::mojom::Role::kMenuListPopup) {
+        if (!node.IsInvisibleOrIgnored()) {
+          detected_changes_.insert(ChangeType::kPopupOpened);
+          significant_change = true;
+        }
+      }
+    }
+  }
+  
+  if (significant_change && !change_detected_) {
+    change_detected_ = true;
+    time_to_first_change_ = base::TimeTicks::Now() - start_time_;
+    VLOG(1) << "Change detected after " << time_to_first_change_.InMilliseconds() << " ms";
+    
+    // If waiting, quit the run loop
+    if (wait_callback_) {
+      std::move(wait_callback_).Run();
+    }
+  }
+}
+
+void BrowserOSChangeDetector::DidOpenRequestedURL(
+    content::WebContents* new_contents,
+    content::RenderFrameHost* source_render_frame_host,
+    const GURL& url,
+    const content::Referrer& referrer,
+    WindowOpenDisposition disposition,
+    ui::PageTransition transition,
+    bool started_from_context_menu,
+    bool renderer_initiated) {
+  if (!monitoring_active_) {
+    return;
+  }
+  
+  if (disposition == WindowOpenDisposition::NEW_POPUP ||
+      disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB ||
+      disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB ||
+      disposition == WindowOpenDisposition::NEW_WINDOW) {
+    detected_changes_.insert(ChangeType::kNewTabOpened);
+    change_detected_ = true;
+    
+    if (!time_to_first_change_.is_positive()) {
+      time_to_first_change_ = base::TimeTicks::Now() - start_time_;
+    }
+    
+    VLOG(1) << "New tab/window detected with disposition: " 
+            << static_cast<int>(disposition);
+    
+    if (wait_callback_) {
+      std::move(wait_callback_).Run();
+    }
+  }
+}
+
+void BrowserOSChangeDetector::OnFocusChangedInPage(
+    content::FocusedNodeDetails* details) {
+  if (!monitoring_active_ || !details) {
+    return;
+  }
+  
+  detected_changes_.insert(ChangeType::kFocusChanged);
+  
+  if (!change_detected_) {
+    change_detected_ = true;
+    time_to_first_change_ = base::TimeTicks::Now() - start_time_;
+    
+    if (wait_callback_) {
+      std::move(wait_callback_).Run();
+    }
+  }
+}
+
+void BrowserOSChangeDetector::OnTimeout() {
+  VLOG(1) << "Change detection timeout reached";
+  
+  if (wait_callback_) {
+    std::move(wait_callback_).Run();
+  }
+}
+
+void BrowserOSChangeDetector::StopMonitoring() {
+  monitoring_active_ = false;
+  timeout_timer_.Stop();
+  wait_callback_.Reset();
+}
+
+}  // namespace api
+}  // namespace extensions
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_change_detector.h b/chrome/browser/extensions/api/browser_os/browser_os_change_detector.h
new file mode 100644
index 0000000000000..f4a902e1b4970
--- /dev/null
+++ b/chrome/browser/extensions/api/browser_os/browser_os_change_detector.h
@@ -0,0 +1,120 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CHANGE_DETECTOR_H_
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CHANGE_DETECTOR_H_
+
+#include <set>
+#include <string>
+
+#include "base/functional/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "base/timer/timer.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "ui/accessibility/ax_enums.mojom.h"
+#include "ui/accessibility/ax_tree_id.h"
+
+namespace content {
+class WebContents;
+}  // namespace content
+
+namespace ui {
+struct AXUpdatesAndEvents;
+}  // namespace ui
+
+namespace extensions {
+namespace api {
+
+// Types of changes that can be detected after user actions
+enum class ChangeType {
+  kNone,              // No change detected
+  kDomChanged,        // Regular DOM updates
+  kPopupOpened,       // Modal/dropdown/menu appeared
+  kNewTabOpened,      // New tab/window created
+  kDialogShown,       // JS alert/confirm/prompt
+  kFocusChanged,      // Focus moved to different element
+  kElementExpanded,   // Dropdown/accordion expanded
+};
+
+// Result of change detection
+struct ChangeDetectionResult {
+  ChangeDetectionResult();
+  ~ChangeDetectionResult();
+  ChangeDetectionResult(const ChangeDetectionResult&);
+  ChangeDetectionResult& operator=(const ChangeDetectionResult&);
+  ChangeDetectionResult(ChangeDetectionResult&&);
+  ChangeDetectionResult& operator=(ChangeDetectionResult&&);
+
+  bool detected = false;
+  ChangeType primary_change = ChangeType::kNone;
+  std::set<ChangeType> all_changes;
+  ui::AXTreeID new_tree_id;
+  int new_tab_id = -1;
+  std::string dialog_type;
+  int popup_node_id = -1;
+  base::TimeDelta time_to_change;
+};
+
+// Detects changes in the DOM after user actions using accessibility events
+class BrowserOSChangeDetector : public content::WebContentsObserver {
+ public:
+  explicit BrowserOSChangeDetector(content::WebContents* web_contents);
+  ~BrowserOSChangeDetector() override;
+
+  BrowserOSChangeDetector(const BrowserOSChangeDetector&) = delete;
+  BrowserOSChangeDetector& operator=(const BrowserOSChangeDetector&) = delete;
+
+  // Start monitoring for changes with a specific tree ID
+  void StartMonitoring(const ui::AXTreeID& initial_tree_id);
+
+  // Wait for changes with timeout, returns result
+  ChangeDetectionResult WaitForChanges(base::TimeDelta timeout);
+
+  // Check if changes were detected (non-blocking)
+  bool HasChangesDetected() const { return change_detected_; }
+
+  // Get the result without waiting
+  ChangeDetectionResult GetResult() const;
+
+ private:
+  // WebContentsObserver overrides
+  void AccessibilityEventReceived(
+      const ui::AXUpdatesAndEvents& details) override;
+  void DidOpenRequestedURL(content::WebContents* new_contents,
+                          content::RenderFrameHost* source_render_frame_host,
+                          const GURL& url,
+                          const content::Referrer& referrer,
+                          WindowOpenDisposition disposition,
+                          ui::PageTransition transition,
+                          bool started_from_context_menu,
+                          bool renderer_initiated) override;
+  void OnFocusChangedInPage(content::FocusedNodeDetails* details) override;
+
+  // Helper methods
+  void OnTimeout();
+  void ProcessAccessibilityEvent(const ui::AXUpdatesAndEvents& details);
+  void StopMonitoring();
+
+  // State tracking
+  bool monitoring_active_ = false;
+  bool change_detected_ = false;
+  ui::AXTreeID initial_tree_id_;
+  ui::AXTreeID current_tree_id_;
+  std::set<ChangeType> detected_changes_;
+  base::TimeTicks start_time_;
+  base::TimeDelta time_to_first_change_;
+  
+  // Timer for timeout handling
+  base::OneShotTimer timeout_timer_;
+  base::OnceClosure wait_callback_;
+
+  // Weak pointer factory
+  base::WeakPtrFactory<BrowserOSChangeDetector> weak_factory_{this};
+};
+
+}  // namespace api
+}  // namespace extensions
+
+#endif  // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CHANGE_DETECTOR_H_
\ No newline at end of file
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
index 7523bf7881787..ee9da99ed9bc7 100644
--- a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
@@ -235,6 +235,7 @@ struct SnapshotProcessor::ProcessingContext
   std::unordered_map<int32_t, int32_t> parent_map;  // child_id -> parent_id  
   std::unordered_map<int32_t, std::vector<int32_t>> children_map;  // parent_id -> child_ids
   int tab_id;
+  ui::AXTreeID tree_id;  // Tree ID for change detection
   base::TimeTicks start_time;
   size_t total_nodes;
   size_t processed_batches;
@@ -401,6 +402,12 @@ void PopulateNodeAttributes(
     std::string html_id = node_data.GetStringAttribute(ax::mojom::StringAttribute::kHtmlId);
     attributes["id"] = SanitizeStringForOutput(html_id);
   }
+  
+  // Add HTML class names
+  if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kClassName)) {
+    std::string class_name = node_data.GetStringAttribute(ax::mojom::StringAttribute::kClassName);
+    attributes["class"] = SanitizeStringForOutput(class_name);
+  }
 }
 
 // Process a batch of nodes
@@ -488,10 +495,12 @@ void SnapshotProcessor::OnBatchProcessed(
     std::vector<ProcessedNode> batch_results) {
   // Process batch results
   for (const auto& node_data : batch_results) {
-    // Store mapping from our nodeId to AX node ID and bounds
+    // Store mapping from our nodeId to AX node ID, bounds, and attributes
     NodeInfo info;
     info.ax_node_id = node_data.node_data->id;
+    info.ax_tree_id = context->tree_id;  // Store tree ID for change detection
     info.bounds = node_data.absolute_bounds;
+    info.attributes = node_data.attributes;  // Store all computed attributes
     GetNodeIdMappings()[context->tab_id][node_data.node_id] = info;
     
     // Log the mapping for debugging
@@ -595,6 +604,11 @@ void SnapshotProcessor::ProcessAccessibilityTree(
   context->children_map = std::move(children_map);
   context->start_time = start_time;
   
+  // Store the tree ID for change detection
+  if (tree_update.has_tree_data) {
+    context->tree_id = tree_update.tree_data.tree_id;
+  }
+  
   // Convert viewport size to document viewport bounds
   // Find the root node and get its scroll offset
   gfx::Rect doc_viewport_bounds;
diff --git a/chrome/common/extensions/api/browser_os.idl b/chrome/common/extensions/api/browser_os.idl
index c34c96484ccbf..6934ee144987d 100644
--- a/chrome/common/extensions/api/browser_os.idl
+++ b/chrome/common/extensions/api/browser_os.idl
@@ -72,10 +72,19 @@ namespace browserOS {
     boolean isPageComplete;
   };
 
+  // Response from click action with change detection
+  dictionary ClickResponse {
+    boolean success;
+    boolean changeDetected;
+    DOMString? primaryChange;
+    long? timeToChangeMs;
+    DOMString[]? allChanges;
+    DOMString? actionRequired;
+  };
 
   callback GetAccessibilityTreeCallback = void(AccessibilityTree tree);
   callback GetInteractiveSnapshotCallback = void(InteractiveSnapshot snapshot);
-  callback ClickCallback = void();
+  callback ClickCallback = void(ClickResponse response);
   callback InputTextCallback = void();
   callback ClearCallback = void();
   callback GetPageLoadStatusCallback = void(PageLoadStatus status);
-- 
2.49.0

