chromeflow 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -126,6 +126,17 @@ use `take_and_copy_screenshot()` — it saves a PNG to ~/Downloads and copies it
126
126
  - `set_file_input` accepts CSS selectors as the hint (e.g. `#import-problem-file`,
127
127
  `.upload-input`) in addition to label text. Use selectors when file inputs are hidden
128
128
  behind custom UIs and have no visible label.
129
+ - **Replacing an already-uploaded file**: after `set_file_input` succeeds, the input
130
+ becomes invisible and a "Remove" span/button typically appears near the upload area.
131
+ To replace the file: `click_element("Remove", nth=N)` (the right `nth` if there are
132
+ multiple), then call `set_file_input(hint, newPath)` again — the same hidden input is
133
+ recycled and accepts the new file. Verify with `get_form_fields()` between the two
134
+ steps so you're sure the input has reappeared.
135
+ - **Forcing auto-save on idempotent text edits** (e.g. keep-alive loop on an
136
+ auto-saving DataAnnotation form): some auto-save logic diffs against the last-saved
137
+ value and skips no-op writes. To force a real save on each tick without changing
138
+ visible content, toggle a trailing space — add when absent, remove when present.
139
+ `fill_input` value comparison handles both directions transparently.
129
140
  - After any radio/checkbox click that reveals new fields, call `get_form_fields()` again —
130
141
  the inventory will include the new fields and warn if more hidden ones still exist.
131
142
  - If a form has collapsible sections, expand them all before calling `get_form_fields()` so
@@ -144,6 +155,12 @@ use `take_and_copy_screenshot()` — it saves a PNG to ~/Downloads and copies it
144
155
  - `switch_to_tab("1")` switches by tab number; `switch_to_tab("form")` matches by URL or title substring.
145
156
  - Before navigating away from a partially-filled form, call `save_page_state()` so the form
146
157
  can be restored if the tab reloads or the page loses its state on return.
158
+ - **In long-lived self-rescheduling loops**, the active tab can silently drift mid-session
159
+ (the user navigates manually while AFK, or another tab steals focus). At the start of
160
+ every loop iteration, call `list_tabs` and verify the active tab's URL matches your
161
+ expected target — if not, `switch_to_tab(<URL or title substring>)` before running
162
+ `execute_script` or any other tab-scoped tool. Without this guard, scripts run on the
163
+ wrong tab and fail with confusing "undefined" errors that look like page bugs.
147
164
 
148
165
  ## Error handling
149
166
 
@@ -194,7 +211,12 @@ set_file_input("Photos", "/path/2.jpg", verify_selector=".photo-thumbnail:nth-of
194
211
  ```
195
212
  The page-level file count is reported in the response — use it to spot uploaders that consume-and-reset the input vs uploaders that keep the file there.
196
213
 
197
- **Waiting for async results** (build, save, deploy): `wait_for_selector(selector, timeout)` — never poll with screenshots.
214
+ **Waiting for async results** (build, save, deploy): `wait_for_selector(selector, timeout)` — never poll with screenshots. `wait_for_selector` pierces open shadow roots, so a selector inside a web component (Outlier task UI, Lit/Stencil widget) matches without ceremony.
215
+
216
+ **Waiting for a shadow host's tree to attach** (e.g. SPA route flips where `<my-host>` appears 10s before its shadow content hydrates, and `wait_for_selector("my-host")` resolves while `host.shadowRoot` is still null): pass `shadow_root=true`. The wait then requires the matched element's `.shadowRoot` to be non-null, not just for the host element to exist.
217
+ ```
218
+ wait_for_selector("iframe", shadow_root=true) — wait until the iframe both exists AND has an attached shadowRoot
219
+ ```
198
220
 
199
221
  **Waiting for an existing region to update** (e.g. click Save, then get the confirmation toast; send a chat message, then get the reply): `wait_for_change(selector)` uses a MutationObserver on the element's subtree and returns its new text content as soon as the mutation settles. Prefer this over `wait_for_selector` + `get_page_text` when the element already exists and you just need its next state — one call instead of two, no polling.
200
222
 
@@ -265,10 +287,25 @@ document.body.style.zoom = '1';
265
287
  1. Retry the exact same `execute_script` call
266
288
  2. If still failing, use `find_and_highlight` to show the user a download button to click manually
267
289
 
268
- **Shadow DOM `[role=radio]` / custom radios silently no-op**: On sites like Outlier,
269
- `element.click()` on a shadow-DOM radio often doesn't flip `aria-checked`. Two things
270
- must be true: (a) the element must be scrolled into view FIRST (`scrollIntoView({block:'center'})`),
271
- and (b) the full pointer-event chain must firenot just `click()`:
290
+ **React-controlled native radios/checkboxes that don't update `checked`**: `click_element`
291
+ auto-handles this for native `<input type=radio>` and `<input type=checkbox>` inputs
292
+ (including labels that wrap or `for=` reference them). The flow:
293
+ - If a radio is already `checked=true`, `click_element` skips the click re-clicking can
294
+ toggle it OFF on React forms whose `onChange` interprets the click as a deselect. The
295
+ response says `"X — radio already checked, click skipped"`.
296
+ - If the standard click fires but the input's `checked` state didn't change as expected
297
+ (radio still unchecked, or checkbox didn't toggle), `click_element` automatically
298
+ dispatches the full pointer-event chain (`pointerdown → mousedown → pointerup → mouseup
299
+ → click`) on the input. The response says `"now checked (after pointer-chain fallback)"`.
300
+
301
+ You only need to drop into `execute_script` for the no-native-input case below.
302
+
303
+ **Shadow DOM `[role=radio]` / role-only custom radios silently no-op**: On sites like
304
+ Outlier where the radio is a `[role=radio]` div with no underlying `<input>`,
305
+ `click_element`'s native-input fallback can't help — the click target has no `.checked`
306
+ property to verify. Two things must be true: (a) the element must be scrolled into view
307
+ FIRST (`scrollIntoView({block:'center'})`), and (b) the full pointer-event chain must
308
+ fire — not just `click()`:
272
309
  ```js
273
310
  ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t =>
274
311
  el.dispatchEvent(new MouseEvent(t, {bubbles: true, cancelable: true}))
@@ -92,7 +92,15 @@ If the click causes page navigation, this resolves when the new page finishes lo
92
92
  Examples: wait for a build to finish, a success/error message to appear, a modal to open.
93
93
  After it resolves, use get_page_text to read the result rather than taking a screenshot.
94
94
  For long-running server-side processes (e.g. a query job that may take minutes), set poll_interval
95
- to 15 seconds so the page is checked gently rather than hammered every 500ms.`,
95
+ to 15 seconds so the page is checked gently rather than hammered every 500ms.
96
+
97
+ Pierces open shadow roots automatically \u2014 selectors for elements inside web components
98
+ (Outlier task UI, Lit/Stencil widgets) match without needing a shadow-DOM-aware caller.
99
+
100
+ Pass \`shadow_root: true\` when the matched element is itself a shadow host whose tree
101
+ hasn't attached yet \u2014 common after SPA route transitions where the host element appears
102
+ seconds before its shadow content hydrates. Without this, wait_for_selector("the-host")
103
+ resolves on the empty host and the next execute_script(host.shadowRoot) returns null.`,
96
104
  {
97
105
  selector: z.string().describe(
98
106
  `CSS selector to wait for (e.g. '.deploy-ready', '[data-status="error"]', '.toast-error')`
@@ -100,14 +108,21 @@ to 15 seconds so the page is checked gently rather than hammered every 500ms.`,
100
108
  timeout: z.number().optional().describe("Max seconds to wait (default 30)"),
101
109
  poll_interval: z.number().optional().describe(
102
110
  "How often to check for the selector, in seconds (default 0.5). Set to 15 when waiting for a slow server-side process."
111
+ ),
112
+ shadow_root: z.boolean().optional().describe(
113
+ "If true, also require the matched element to have an attached shadowRoot (not null). Use after SPA navigations where the shadow host appears before its tree hydrates. Default false."
103
114
  )
104
115
  },
105
- async ({ selector, timeout = 30, poll_interval }) => {
116
+ async ({ selector, timeout = 30, poll_interval, shadow_root }) => {
106
117
  const timeoutMs = timeout * 1e3;
107
118
  const pollMs = poll_interval ? poll_interval * 1e3 : void 0;
108
- await bridge.request({ type: "wait_for_selector", selector, timeout: timeoutMs, refresh: pollMs }, timeoutMs + 5e3);
119
+ await bridge.request(
120
+ { type: "wait_for_selector", selector, timeout: timeoutMs, refresh: pollMs, shadow_root },
121
+ timeoutMs + 5e3
122
+ );
123
+ const suffix = shadow_root ? " (with attached shadowRoot)" : "";
109
124
  return {
110
- content: [{ type: "text", text: `Selector "${selector}" found on page.` }]
125
+ content: [{ type: "text", text: `Selector "${selector}" found on page${suffix}.` }]
111
126
  };
112
127
  }
113
128
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromeflow",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Browser guidance MCP server for Claude Code — highlights, clicks, fills, and captures from the web so you don't have to.",
5
5
  "type": "module",
6
6
  "bin": {