<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mxhlix</title>
    <description>The latest articles on DEV Community by Mxhlix (@mxhlix).</description>
    <link>https://dev.to/mxhlix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024719%2F0e97f1ff-508f-4e20-aeab-13ce2a7cdcca.png</url>
      <title>DEV Community: Mxhlix</title>
      <link>https://dev.to/mxhlix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mxhlix"/>
    <language>en</language>
    <item>
      <title>My agent filled in every field on the form. It could not attach the three images.</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Wed, 29 Jul 2026 01:02:08 +0000</pubDate>
      <link>https://dev.to/mxhlix/my-agent-filled-in-every-field-on-the-form-it-could-not-attach-the-three-images-23kh</link>
      <guid>https://dev.to/mxhlix/my-agent-filled-in-every-field-on-the-form-it-could-not-attach-the-three-images-23kh</guid>
      <description>&lt;p&gt;I had an AI agent filling out a listing form on a web service. Every text field went in. It clicked Save Draft. All that was left was attaching three PNGs.&lt;/p&gt;

&lt;p&gt;That part never worked.&lt;/p&gt;

&lt;p&gt;I assumed that if it could fill the form, it could attach the files. Those turned out to be two different capabilities, gated in two different places, and the gate behaves differently depending on how you're running Claude Code. Same machine, same settings, same browser extension, different answer.&lt;/p&gt;

&lt;p&gt;My setup: the Claude Code desktop app on Windows 11, the Claude in Chrome extension, and the computer-use desktop automation server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sanctioned route is closed first
&lt;/h2&gt;

&lt;p&gt;The extension hands the agent a tool called &lt;code&gt;file_upload&lt;/code&gt;. Its description tells you exactly how to use it, and what not to do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Upload one or multiple files to a file input element on the page. Do not click on file upload buttons or file inputs — clicking opens a native file picker dialog that you cannot see or interact with. Instead, use read_page or find to locate the file input element, then use this tool with its ref to upload files directly. Only files the user has shared with this session (attachments, the session's outputs/uploads folders, or folders the user has connected) can be uploaded; other paths will be rejected. The combined size of all files in a single call must stay under 10 MB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first three sentences are the procedure, and the procedure worked immediately: locate the input, pass the ref and a path. The fourth sentence is where everything stopped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot upload "...": only files the user has shared with this session can be uploaded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried four ways of "sharing" a file. A directory that was in the session's allowed list from the moment it started. The session's own temp folder. A folder approved through the directory-connect tool. A folder added to &lt;code&gt;additionalDirectories&lt;/code&gt; in &lt;code&gt;settings.local.json&lt;/code&gt;. All four came back with the same sentence.&lt;/p&gt;

&lt;p&gt;The first one deserves a second look. That allowed directory is where the session reads and writes; I had the agent generate the test PNG there. &lt;strong&gt;A session cannot upload a file it wrote itself, thirty seconds earlier, in a directory it is explicitly allowed to write to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Attaching the images to the chat doesn't help either, because on this surface a chat attachment never becomes a file. The model receives the image data, but nothing lands on disk. I scanned the temp tree, the config tree, and the app's data folder: zero new PNGs. There's a separate image-upload tool, but it requires an &lt;code&gt;imageId&lt;/code&gt;, and chat attachments don't carry one.&lt;/p&gt;

&lt;p&gt;None of this is a bug. The parenthetical in that description limits "shared" to exactly three things: chat attachments, the session's outputs/uploads folders, and folders the user connected.&lt;/p&gt;

&lt;p&gt;Fine — then put the file in one of the three. Except the session directory has no &lt;code&gt;outputs&lt;/code&gt; or &lt;code&gt;uploads&lt;/code&gt; folder in it. So I created them, dropped a PNG in each, and tried again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot upload "...\outputs\test-out.png": only files the user has shared with this session can be uploaded.
Cannot upload "...\uploads\test-up.png": only files the user has shared with this session can be uploaded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matching the names changes nothing. And "folders the user has connected" can't mean the directory-connect tool, because that one is already on the rejected list. On this surface, all three of the listed escape hatches are decorative.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't reach the native dialog
&lt;/h2&gt;

&lt;p&gt;The obvious next thought: the button opens a file picker, so drive the file picker.&lt;/p&gt;

&lt;p&gt;That window isn't File Explorer. It's the common file dialog, owned by &lt;code&gt;chrome.exe&lt;/code&gt;. Desktop automation decides permissions per owning process of the frontmost window, so as far as it's concerned, that dialog &lt;em&gt;is&lt;/em&gt; Chrome.&lt;/p&gt;

&lt;p&gt;And browsers are pinned to the "read" tier in computer-use. Screenshots yes, clicks and keystrokes no. Request access (it returns &lt;code&gt;granted tier:"read"&lt;/code&gt;), bring Chrome to the front, try a click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Chrome" is granted at tier "read" — visible in screenshots only, no clicks or typing.
... never use AppleScript, System Events, shell commands, or any other method
to send clicks or keystrokes to this app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first half is the state. The second half enumerates the workarounds you were about to think of and then closes the list with "or any other method," which also kills route three: File Explorer &lt;em&gt;is&lt;/em&gt; controllable at full tier, so dragging a file from Explorer onto the browser's drop zone looks viable, right up until you notice it's a way of sending input to Chrome.&lt;/p&gt;

&lt;p&gt;Bonus trap: if you do click the button, the dialog you opened is now unclosable. Chrome is read-tier, so you can't even send it an Escape. It sits there being modal until a human walks over and dismisses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same tool name, different gate
&lt;/h2&gt;

&lt;p&gt;With all three routes dead I went to the docs, and found a version requirement: local file upload landed in Claude Code v2.1.211.&lt;sup id="fnref1"&gt;1&lt;/sup&gt; Three restrictions come with it — the session must be allowed to read the file, 10 MB total per call, and files with multiple hard links are refused.&lt;/p&gt;

&lt;p&gt;So I ran the same task from the CLI (&lt;code&gt;claude --chrome&lt;/code&gt;) on a version past that line. The rejection message is different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;only files this session is allowed to read ... add its folder with /add-dir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test moved from "was it shared" to "can you read it." I ran &lt;code&gt;/add-dir&lt;/code&gt; on my Downloads folder and &lt;code&gt;file_upload&lt;/code&gt; went straight through. Three PNGs, 731 KB total, page JavaScript rendered the previews, all three slots consumed.&lt;/p&gt;

&lt;p&gt;Meanwhile, on the same machine, the desktop app still refuses. It sits in the same project, reads the same settings file that the CLI just used, and returns the old "only files the user has shared with this session." Typing the fix into the app gets you this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/add-dir isn't available in this environment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension is identical in both cases (Claude in Chrome 1.0.81), so the gate isn't in the extension. It's in Claude Code, implemented per surface.&lt;/p&gt;

&lt;p&gt;Now the part that is documentation rather than measurement. The published permission rule reads like my case should work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Permissions: Claude can upload a file only when the session is allowed to read it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read it, upload it. My measurements say otherwise. But before calling that a spec violation — that page opens by scoping itself to browser automation "from the CLI or the &lt;a href="https://code.claude.com/docs/en/vs-code" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;." The desktop app is never mentioned. I had been reading the manual for the surface next to mine.&lt;/p&gt;

&lt;p&gt;The desktop app does have its own manual.&lt;sup id="fnref2"&gt;2&lt;/sup&gt; It even explains the &lt;code&gt;/add-dir&lt;/code&gt; result: commands that open an interactive terminal panel behave differently in the Code tab, and the ones with no argument form answer &lt;code&gt;isn't available in this environment&lt;/code&gt;. The message I'd been squinting at was documented behavior all along.&lt;/p&gt;

&lt;p&gt;That manual has a browser section too — but it describes the Browser pane, the app's own built-in browser, which is not the Chrome extension. The Browser pane has no file attachment capability at all. The word "upload" does not appear on the page.&lt;/p&gt;

&lt;p&gt;And then the same page sends you here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you want Claude to act as you in your logged-in sessions, use the Claude in Chrome extension instead, which shares your browser's login state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Follow that advice and you arrive at the page that opened by telling you it's about the CLI and the VS Code extension. Two help desks, each politely pointing at the other, neither of them saying anything false.&lt;/p&gt;

&lt;p&gt;So: not a documented condition being violated. An undocumented surface. It took reading both manuals to see that.&lt;/p&gt;

&lt;p&gt;One footnote to save you an hour: if you're in the desktop app and you ask the agent what version you're on, it will shell out to &lt;code&gt;claude --version&lt;/code&gt; and report your npm CLI. I got fooled by this. The shell said 2.1.215; the app was actually running the 2.1.219 it ships inside &lt;code&gt;AppData\Roaming\Claude\claude-code\&lt;/code&gt;. There's no desktop-side version command, so the only reliable answer is the executable path of the running process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clipboard route works
&lt;/h2&gt;

&lt;p&gt;The goal was still reachable, just not through the front door. OS clipboard plus a trusted paste event:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Set-Clipboard -LiteralPath &amp;lt;file&amp;gt;&lt;/code&gt; in PowerShell (this puts CF_HDROP on the clipboard)&lt;/li&gt;
&lt;li&gt;Install a &lt;code&gt;paste&lt;/code&gt; listener on the page&lt;/li&gt;
&lt;li&gt;Send a real &lt;code&gt;Ctrl+V&lt;/code&gt; through browser automation&lt;/li&gt;
&lt;li&gt;Take the File off &lt;code&gt;clipboardData.files&lt;/code&gt;, put it into &lt;code&gt;input[type=file].files&lt;/code&gt; via &lt;code&gt;DataTransfer&lt;/code&gt;, dispatch &lt;code&gt;change&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran this against the real form's image slot. A 307 KB PNG arrived in &lt;code&gt;clipboardData.files&lt;/code&gt; with name, size, and MIME intact, the page's JavaScript accepted it, the preview rendered, and the save went through. No question was asked about where the file came from.&lt;/p&gt;

&lt;p&gt;Two things cost me time here. Specifying the modifier as a separate parameter does nothing useful — you get a literal &lt;code&gt;v&lt;/code&gt; typed into the page; the key has to be passed as the string &lt;code&gt;ctrl+v&lt;/code&gt;. And coordinate clicks land nowhere unless you take a screenshot first, because the coordinate space is defined by the screenshot.&lt;/p&gt;

&lt;p&gt;Working and recommendable are different things, and this route has four problems. It routes around the origin restriction on &lt;code&gt;file_upload&lt;/code&gt; — a restriction that presumably exists to stop an agent from pushing local files onto the web without anyone looking, which is not the kind of thing you disable because it's in your way. It clobbers the clipboard, so whatever the user had copied is gone, and when an agent does that silently it reads as a malfunction. It depends on how the page is built: fine if the page watches &lt;code&gt;input.files&lt;/code&gt;, dead if a custom uploader only listens for &lt;code&gt;drop&lt;/code&gt;. And putting CF_HDROP on the clipboard is Windows-specific. On top of all that, it isn't a supported path, so an update could close it without notice.&lt;/p&gt;

&lt;p&gt;Which brings me to a question. If you know the sanctioned route on the desktop app, or a way to do this without the four problems above, I'd like to hear it. Two candidates I haven't tested: running &lt;code&gt;claude --chrome&lt;/code&gt; in the app's built-in terminal, and using the VS Code extension. Both should land on the CLI side of the gate, but I haven't confirmed either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What half a day bought
&lt;/h2&gt;

&lt;p&gt;One tool name, &lt;code&gt;file_upload&lt;/code&gt;, two behaviors: the CLI accepts anything the session can read, the desktop app interrogates where the file came from and says no. When I design work to hand off to an agent, I'd been treating permissions as a property of the settings file. They're also a property of which binary is running.&lt;/p&gt;

&lt;p&gt;Practically, that leaves three options for form work that involves attachments: run it from the CLI, keep a clipboard procedure on hand for the desktop app, or leave the attachment step to a human. My own runbook already said the reliable route was manual. It took half a day of building a fourth route to believe it. A known two-minute manual step beats an unknown fully-automated one, and apparently that's a lesson with a tuition fee.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't verified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What "folders the user has connected" actually refers to. I've only established that the directory-connect tool's approval isn't it.&lt;/li&gt;
&lt;li&gt;Whether chat attachments count as shared on surfaces where they do materialize as files (the browser version, presumably). That's how the description reads, but I haven't tested it.&lt;/li&gt;
&lt;li&gt;Which Claude Code version the desktop app bundled on the day I first hit this. The number I recorded then was the npm CLI's, and the app deletes the old version folder when it updates.&lt;/li&gt;
&lt;li&gt;Along the way I also tried injecting via a local HTTP server, and hit a case where a &lt;code&gt;fetch&lt;/code&gt; from an HTTPS page to &lt;code&gt;http://127.0.0.1&lt;/code&gt; neither succeeds nor errors — it just hangs forever. I suspect the browser's local network access check swallowed it, but I didn't confirm that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the big caveat: everything here is bound to a version and a surface. The CLI already passes cleanly. There's a good chance this whole post is a historical document within six months.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified July 2026. Environment: Windows 11, Claude Code desktop app (bundled version 2.1.219), npm CLI 2.1.215, Claude in Chrome 1.0.81, computer-use. Desktop-app re-measurement and the clipboard route were both run on 2026-07-26.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/chrome" rel="noopener noreferrer"&gt;Use Claude Code with Chrome&lt;/a&gt;, "Upload files to web pages."&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/desktop" rel="noopener noreferrer"&gt;Desktop application&lt;/a&gt;, sections "Browse external sites," "Choose between the Browser and the Chrome extension," and "What's not available in Desktop."&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My coding agent always stopped at 80%. The spec I gave it was why.</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Sun, 26 Jul 2026 01:37:36 +0000</pubDate>
      <link>https://dev.to/mxhlix/specs-rot-running-code-doesnt-how-i-restructured-my-ai-agent-skills-around-live-references-2eae</link>
      <guid>https://dev.to/mxhlix/specs-rot-running-code-doesnt-how-i-restructured-my-ai-agent-skills-around-live-references-2eae</guid>
      <description>&lt;p&gt;I run about 600 browser-based web tools, each shipped in five locales, all built by Claude Code. For a long time, adding a new one meant handing the agent a spec: design conventions, component rules, forbidden patterns. It would read the spec, produce something plausible, and stop at roughly 80% quality. The last 20% took rounds of me pointing at things.&lt;/p&gt;

&lt;p&gt;"Roughly 80%" was a feeling, not a measurement. I only got a number after I changed the setup.&lt;/p&gt;

&lt;p&gt;Two things changed. The agent stopped reading specs and started copying production code. And the "build a new tool" skill got split from the "fix a broken tool" skill. Below is what that actually looks like in the repo, with the files and commits it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaces the spec
&lt;/h2&gt;

&lt;p&gt;A spec starts drifting from reality the moment you write it. Every small decision made during implementation — this pattern gets that padding, this one case is an exception — never makes it back into the document. Nobody keeps that up. So the agent fills in the unwritten parts its own way, differently every time.&lt;/p&gt;

&lt;p&gt;Production code has all of those decisions baked in. So the skill file became a thin wrapper, and the real content moved into a catalog: 263 lines indexing 15 production references across 5 implementation patterns.&lt;/p&gt;

&lt;p&gt;The catalog opens with a decision tree. Three questions, plus one for the specialized case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q1. Is the output PNG or SVG?
  ├─ SVG only          → §6 Pure SVG
  └─ PNG (primary)     → Q2

Q2. Does the preview composite a user-supplied image (a photo, etc.)?
  ├─ Yes → §3 html2canvas (background image + text overlay)
  │        * If objects are dragged / rotated / edited individually,
  │          use §2 Canvas 2D (ita-bag-badge-layout-calculator) instead
  └─ No  → Q3

Q3. Is text rendering precision decisive (typography-critical)?
       (name badges, price tags, labels, certificates, address cards,
        vertical Japanese text, print offset)
  ├─ Yes (1px of glyph drift is visible) → §2 Canvas 2D direct draw
  └─ No  (just image-ify the DOM)        → §4 dom-to-image-more

Q4 (Specialized). QR / barcode?
  └─ Yes → §5 dedicated library (QRCode.js / JsBarcode)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whichever branch it lands on has a table of production references. Canvas 2D has nine. Three rows of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Template path&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;DL handler line&lt;/th&gt;
&lt;th&gt;Draw core line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shugibukuro-address-template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templates/visual/shugibukuro-address-template.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vertical text / rotated glyphs / auto line-size&lt;/td&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wedding-place-card-maker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templates/visual/wedding-place-card-maker.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;honorifics / font auto-fit / multi-page A4&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;td&gt;214&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ita-bag-badge-layout-calculator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templates/calc/ita-bag-badge-layout-calculator.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;drag &amp;amp; drop editing / tiling / coverage ratio / one renderer shared by editor and PNG export&lt;/td&gt;
&lt;td&gt;994&lt;/td&gt;
&lt;td&gt;387&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent picks two: one to use as the base, one to diff against. It doesn't read either file end to end — the table says where the download handler is and where the render core starts, so it reads around those line numbers. Then it copies the base template as a file and reshapes it around the new tool's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it work
&lt;/h2&gt;

&lt;p&gt;One tool has before/after numbers, because I instrumented it while validating the approach. The measurement is the gap between where text sits in the DOM preview and where it lands in the downloaded PNG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;broken state:
  scaleFactor: 0.397
  hasJustifyCenter: true
  Canvas vs DOM drift (text top): event 0px / name -7px / role -6px / org -11px

skill applied:
  scaleFactor: 0.733
  hasJustifyCenter: false
  Canvas vs DOM drift: event +0.8px / name +2.5px / role +1px / org +0.8px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worst-case drift went from 11px to 2.5px. That's the specific class of defect I used to catch by eye and send back.&lt;/p&gt;

&lt;p&gt;The honest caveat: this was measured while &lt;strong&gt;repairing an existing tool&lt;/strong&gt;, not while building a new one. The handoff note from that week lists "validate the skill on a from-scratch build" as untested, and it stayed that way. My claim that new builds got better is operational — the correction rounds mostly stopped — not instrumented.&lt;/p&gt;

&lt;p&gt;One side effect worth mentioning: the catalog records convergence. Five patterns exist, but 9 of the 15 production references are Canvas 2D. The line in the catalog that says "when in doubt, pick Canvas 2D" is writable only because that 60% is sitting right above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split that lasted 94 minutes
&lt;/h2&gt;

&lt;p&gt;The second decision was separating new construction from repair. I used to explain it as "one skill would bloat." What actually happened is simpler, and the commit timestamps tell it better than I can.&lt;/p&gt;

&lt;p&gt;The prototype was a single 83-line skill with build steps and repair steps living together. At 21:11 I split it in two: 71 lines of build, 85 lines of repair. A clean bisection.&lt;/p&gt;

&lt;p&gt;At 22:45, 94 minutes later, the repair skill I had just created was demoted. Its whole SKILL.md became &lt;code&gt;qa-audit/references/visual.md&lt;/code&gt;, one leaf under a new skill that routes across six symptom categories.&lt;/p&gt;

&lt;p&gt;The reason it collapsed that fast is that the two sides branch on different axes. Build branches on &lt;strong&gt;implementation pattern&lt;/strong&gt;: Canvas 2D, html2canvas, dom-to-image, dedicated library, pure SVG. Repair branches on &lt;strong&gt;symptom category&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Open when symptom matches&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/layout.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LayoutPatternValidator flagged, fullscreen button overlap, download / reset / copy button position wrong&lt;/td&gt;
&lt;td&gt;✅ Implemented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/visual.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Image-export tool: preview ≠ downloaded image, clipped text, filename &lt;code&gt;----.png&lt;/code&gt; for CJK input, scaler off-center&lt;/td&gt;
&lt;td&gt;✅ Implemented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/i18n.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FR / PT translation reads as machine-translated, hardcoded JA/EN UI strings&lt;/td&gt;
&lt;td&gt;⏳ Stub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/interactive.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reset button doesn't reset, button state stuck, tap feedback missing on mobile&lt;/td&gt;
&lt;td&gt;⏳ Stub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those axes are orthogonal. Layout breakage and bad translations happen regardless of whether the tool draws with Canvas 2D or html2canvas. And "how to build a Canvas 2D tool" has no entry point until someone reports a symptom, which is exactly what never happens on a new build. Keep both in one skill and every read costs the agent a pass over the axis it doesn't care about. The build side is 82 lines today. The repair side is 495 (85 in SKILL.md, 410 across six reference files). Merged, every new tool would have walked past all of it.&lt;/p&gt;

&lt;p&gt;Unbuilt categories are stubs that point at the canonical doc instead of describing a procedure, so the agent can't invent one that doesn't exist. The promotion rule got written into the same commit: a stub becomes a real reference when the same bug recurs across two or more tools and the existing pointer doesn't already contain the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills advise. Hooks enforce.
&lt;/h2&gt;

&lt;p&gt;A skill is a document. It has no power. The agent skims.&lt;/p&gt;

&lt;p&gt;So the violations that must never ship are blocked at write time. Here's one. If &lt;code&gt;flex-1&lt;/code&gt; or &lt;code&gt;min-h-[Npx]&lt;/code&gt; survives on a preview element, it silently overrides the height JS sets on that element. It shows up two ways: empty checkerboard padding above and below the preview, or the inner card punching out through the bottom of the tool container. Same root cause, and from the symptom you would not guess they're related.&lt;/p&gt;

&lt;p&gt;Writing "delete this class" in the skill did not stop it. It came back on a second tool. Then this went in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# [P-068] preview-stage / preview-wrapper has flex-1 or min-h-[N] while JS controls height&lt;/span&gt;
&lt;span class="c"&gt;# Past recurrences: conference-name-badge-maker (Wave 9), perspective-grid-generator (2026-05-27).&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'id="(preview-stage|preview-wrapper|preview-area|preview-container)"[^&amp;gt;]*(flex-1|min-h-\[)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;⚠ P-068: ... Canonical class is 'relative w-full overflow-hidden' and let JS own the height."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Warning text trimmed. The recurrence list stays in the comment because it's the evidence for promoting this from advice to a block.)&lt;/p&gt;

&lt;p&gt;One grep. That stopped it. The build skill also grew a line telling the agent to strip the class if it finds it in the reference it's copying from, because references are not automatically clean.&lt;/p&gt;

&lt;p&gt;"It's in the skill but the agent ignored it" is not a writing problem. It's what happens when you expect enforcement from advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Reference quality is the output ceiling. You still have to build the first one properly, by hand, with the taste applied. That is more up-front work than writing a spec, not less.&lt;/p&gt;

&lt;p&gt;The catalog needs maintenance. Line numbers drift when a template gets reworked, so the catalog declares a tolerance: ±5 lines, update only after a major rewrite. That doesn't save you from the other failure. While pulling quotes for this post I found that the skill file still summarizes Canvas 2D as having 8 references while the catalog itself lists 9. I added a reference, updated the catalog, and never touched the summary table in the skill.&lt;/p&gt;

&lt;p&gt;Which is the part I'd push back on if someone else wrote this post. Production code doesn't rot while it keeps running. The index pointing at it rots like any other document. What reference-driven buys you is that the rot is now visible and cheap — a stale line number sends the agent five lines off target, instead of a stale rule sending it down a design that no tool has used in months.&lt;/p&gt;

&lt;p&gt;And this only pays off because I'm mass-producing. There's no reason to build a catalog for a project with one tool in it. Somewhere around the third one, when you catch yourself thinking "I wrote this already," is when it's worth naming the first one a reference.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified April–June 2026. Environment: Claude Code with custom skills, running 600+ web tools across 5 locales. Line counts and code excerpts taken from the live files in July 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>4 ways canvas text rendering breaks in multilingual apps (that en/ja testing will never catch)</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Thu, 23 Jul 2026 00:47:52 +0000</pubDate>
      <link>https://dev.to/mxhlix/4-ways-canvas-text-rendering-breaks-in-multilingual-apps-that-enja-testing-will-never-catch-409n</link>
      <guid>https://dev.to/mxhlix/4-ways-canvas-text-rendering-breaks-in-multilingual-apps-that-enja-testing-will-never-catch-409n</guid>
      <description>&lt;p&gt;I run a large fleet of "preview it, then download it as PNG" web tools — name tags, certificate generators, price cards, badges — in five languages: Japanese, English, Spanish, French, Portuguese.&lt;/p&gt;

&lt;p&gt;Canvas 2D text rendering looks correct as long as you only test Japanese and English.&lt;/p&gt;

&lt;p&gt;It breaks when you run es/fr/pt through it.&lt;/p&gt;

&lt;p&gt;After stepping on these repeatedly, the failures collapse into four patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise: Latin languages run 1.4–2× longer than Japanese
&lt;/h2&gt;

&lt;p&gt;Design data first. The same label, measured across five locales:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;ja&lt;/th&gt;
&lt;th&gt;en&lt;/th&gt;
&lt;th&gt;es&lt;/th&gt;
&lt;th&gt;fr&lt;/th&gt;
&lt;th&gt;pt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool name&lt;/td&gt;
&lt;td&gt;22 chars&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Standard" button&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: fr/pt come out 1.4–1.7× longer than ja; es can balloon to nearly 3×. A font size and maxWidth tuned to fit Japanese will not fit the Latin locales. All four failure patterns grow from this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: hand-rolled wrapping via &lt;code&gt;text.split(/\s+/)&lt;/code&gt; collapses on CJK
&lt;/h2&gt;

&lt;p&gt;The classic snippet — split on spaces, wrap word by word — does nothing for Japanese or Chinese, where words aren't space-delimited. An entire sentence becomes one unbreakable token and clips at the canvas edge.&lt;/p&gt;

&lt;p&gt;Test with real Japanese input and check that the final line renders to its last character. "Most of it showed up" is not a pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: an ASCII-only tokenizer splits words at accented characters
&lt;/h2&gt;

&lt;p&gt;Fix pattern 1 with a character-class tokenizer like &lt;code&gt;[A-Za-z0-9'\-_]&lt;/code&gt; and you've traded one regression for another: &lt;code&gt;ç&lt;/code&gt; &lt;code&gt;é&lt;/code&gt; &lt;code&gt;ã&lt;/code&gt; &lt;code&gt;ó&lt;/code&gt; &lt;code&gt;ñ&lt;/code&gt; aren't in that class, so &lt;code&gt;produção&lt;/code&gt; fragments into &lt;code&gt;produ&lt;/code&gt; / &lt;code&gt;ç&lt;/code&gt; / &lt;code&gt;ão&lt;/code&gt; mid-word.&lt;/p&gt;

&lt;p&gt;An English test will never catch this.&lt;/p&gt;

&lt;p&gt;Generate actual PNGs with fr/es/pt samples and eyeball the area around accented characters. I never found another detection method — string-comparison tests can't see a rendering-level split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: the important word at the end vanishes into "…"
&lt;/h2&gt;

&lt;p&gt;Since fr/pt run 1.4–1.7× longer than the ja the layout was tuned for, text overflows its two lines and gets ellipsized. The cruel part: what disappears is the tail of the phrase — often the semantically critical word, like &lt;code&gt;production&lt;/code&gt; / &lt;code&gt;produção&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix was splitting fields into two classes. Fields that may truncate (descriptions) wrap-and-ellipsize. Fields that must never truncate (titles, names) get auto-fit: measure on an offscreen canvas with &lt;code&gt;measureText&lt;/code&gt;, search for a font size that fits, render at that size. A title with an ellipsis is a broken deliverable, so never mix the two strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: the preview and the downloaded PNG break lines in different places
&lt;/h2&gt;

&lt;p&gt;The sneakiest one. Implement pattern 3's auto-fit only on the canvas side while the DOM preview keeps its fixed font size, and the line breaks you see on screen won't match the line breaks in the PNG. From the user's perspective, the tool's core promise — "download what you see" — is broken.&lt;/p&gt;

&lt;p&gt;The fix: centralize the auto-fit font-size decision in one place (the offscreen canvas measurement) and apply the resulting size to both the canvas render and the preview DOM's &lt;code&gt;style.fontSize&lt;/code&gt;. My pass condition: preview line count equals PNG line count, in all five locales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-release checklist
&lt;/h2&gt;

&lt;p&gt;For every new image-output tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;ja&lt;/strong&gt;: CJK renders to the last character of the final line, no edge clipping&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;fr / es / pt&lt;/strong&gt;: generate real PNGs; no word splits around &lt;code&gt;ç&lt;/code&gt; &lt;code&gt;é&lt;/code&gt; &lt;code&gt;ã&lt;/code&gt; &lt;code&gt;ó&lt;/code&gt; &lt;code&gt;ñ&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;fr / pt&lt;/strong&gt;: no ellipsis eating the important trailing words&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;all five locales&lt;/strong&gt;: preview (DOM) and downloaded PNG agree on line breaks and line count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Declaring "multilingual support: done" after checking only en and ja was the single biggest trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and caveats
&lt;/h2&gt;

&lt;p&gt;The character-count ratios are measured from my own fleet (utility-tool names, descriptions, UI labels). Different content genres will shift the ratios.&lt;/p&gt;

&lt;p&gt;On automation limits: patterns 2 and 4 resist static analysis entirely. Generating PNGs and looking at them was the only detection that worked. Five locales × generation checks is a real cost — whatever you skip surfaces in production instead.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified: April–May 2026. Environment: Canvas 2D API / vanilla JS / ja·en·es·fr·pt locales in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>i18n</category>
      <category>canvas</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Giving an AI QA agent a Bayesian prior: predicting where the bugs are before looking</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:04:31 +0000</pubDate>
      <link>https://dev.to/mxhlix/giving-an-ai-qa-agent-a-bayesian-prior-predicting-where-the-bugs-are-before-looking-1o10</link>
      <guid>https://dev.to/mxhlix/giving-an-ai-qa-agent-a-bayesian-prior-predicting-where-the-bugs-are-before-looking-1o10</guid>
      <description>&lt;p&gt;Hand an AI agent a QA checklist and here's what you get: every item inspected top to bottom with perfectly uniform attention. And with attention spread uniformly, the places that actually break get missed.&lt;/p&gt;

&lt;p&gt;Running QA across 600+ multilingual web tools with AI agents, I ended up at a design I'd describe as giving QA a prior probability. The entire implementation is markdown files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: static checklists distribute attention evenly
&lt;/h2&gt;

&lt;p&gt;A veteran human QA engineer doesn't read a checklist evenly. They carry a heuristic — "this type of tool usually breaks here" — and aim their attention at the suspicious spots first.&lt;/p&gt;

&lt;p&gt;Give an AI a checklist (mine catalogs 70+ known violation patterns) and that gradient of attention doesn't exist. It dutifully verifies everything, burns context doing it, and still walks past the frequent offenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: accumulate violation history, predict before inspecting
&lt;/h2&gt;

&lt;p&gt;What I did is almost embarrassingly simple.&lt;/p&gt;

&lt;p&gt;Every violation found in QA gets logged to a knowledge base (a markdown file): which category of tool, which violation pattern. That's the accumulation side.&lt;/p&gt;

&lt;p&gt;Then at QA start, before touching the checklist, the agent predicts the top 3–5 likely violations from the tool's category × historical frequency. My category-level predictions look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculators: overexposed "real-time calculation" marketing copy / missing unit toggles / broken output-card layout&lt;/li&gt;
&lt;li&gt;Visual tools: initialization flicker / preview overflow / export-element size breakage&lt;/li&gt;
&lt;li&gt;Text tools: multilingual label overflow / hardcoded UI strings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The predictions miss sometimes. Even so, just aiming the first pass of visual attention at "places that actually broke before" cut the misses noticeably. In Bayesian terms: stop starting QA from a uniform distribution; start from a prior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two designs that compounded the effect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Kill everything grep can catch before visual QA starts
&lt;/h3&gt;

&lt;p&gt;Some violation patterns are mechanically detectable: banned words in visible copy, inline onclick, hardcoded UI strings. Run those as one batch grep sweep before any visual inspection. The expensive check (eyes on pixels) should only cover what eyes alone can catch. Cheap checks first, expensive checks second — obvious sequencing that AI agents skip unless you force it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate check and fix into different sessions and branches
&lt;/h3&gt;

&lt;p&gt;Let the QA agent fix things in the same session and you contaminate the verdict: the desire to have "fixed it" bleeds into the pass/fail judgment. I once got a QA record where "PASS" and a list of unresolved issues coexisted in the same file.&lt;/p&gt;

&lt;p&gt;Since then: the check role detects and reports, and never touches code. Fixes happen in a separate intent, separate branch, separate session. If the same finding survives three rounds, escalate to the human instead of continuing to fix (shelving is a human-only decision).&lt;/p&gt;

&lt;p&gt;Worth noting: the "pass + unresolved issues" record corruption may be less likely with today's tooling. But the design principle — QA objectivity is guaranteed by not holding the fix permissions — stays valid after any specific cause is patched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation cost
&lt;/h2&gt;

&lt;p&gt;No special tools. The violation catalog is one markdown file. The KB (violation history) is one markdown file, layered into always-read and dig-when-needed sections. Prediction and routing live in the skill file (the procedure doc) handed to the agent.&lt;/p&gt;

&lt;p&gt;So: it's all just markdown. No training, no external database. One extra step — "consult history, predict" — inserted into the procedure. That one step made the QA qualitatively different from a static checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and caveats
&lt;/h2&gt;

&lt;p&gt;A prior needs accumulation. Early on the KB is empty and this does nothing; it starts working after a few dozen QA runs of logged violations.&lt;/p&gt;

&lt;p&gt;Sloppy categories produce sloppy predictions. I started with roughly "calculator / visual / text" granularity, and that was enough.&lt;/p&gt;

&lt;p&gt;There's a decay risk in trusting predictions too much — the agent stops looking beyond the predicted 3–5. The predictions only reorder attention; the full sweep never gets skipped.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified: April–June 2026. Environment: Claude Code + custom skills/knowledge base (markdown) / QA across a multilingual web-tool fleet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My AI QA agent said "all features working." The canvas was blank. Here's what it was actually seeing.</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Fri, 17 Jul 2026 02:12:49 +0000</pubDate>
      <link>https://dev.to/mxhlix/my-ai-qa-agent-said-all-features-working-the-canvas-was-blank-heres-what-it-was-actually-1pl5</link>
      <guid>https://dev.to/mxhlix/my-ai-qa-agent-said-all-features-working-the-canvas-was-blank-heres-what-it-was-actually-1pl5</guid>
      <description>&lt;p&gt;Back when I first delegated QA to an AI agent, it signed off on a tool with "all features working, pass." I opened the tool myself. The canvas was blank.&lt;/p&gt;

&lt;p&gt;The AI wasn't lying. In the environment it was looking at, the tool genuinely appeared to work.&lt;/p&gt;

&lt;p&gt;I run visual QA across a large fleet of web tools using Claude + Chrome MCP, and this class of false positive traced back to exactly two causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: requestAnimationFrame stops in hidden tabs
&lt;/h2&gt;

&lt;p&gt;Chrome MCP typically operates in hidden (background) tabs. And browsers throttle &lt;code&gt;requestAnimationFrame&lt;/code&gt; aggressively in hidden tabs to save power.&lt;/p&gt;

&lt;p&gt;QA a canvas or animation feature in that environment and here's what happens: the JS executes, no errors fire, event handlers respond — and the render finishes at zero frames. The AI looks at a screenshot plus clean JS results and concludes "animation started, no errors, pass." Code executing and pixels rendering are different events, and a hidden tab erases the distinction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reproduction test before publishing (July 10, 2026)
&lt;/h3&gt;

&lt;p&gt;I didn't want this to be a "back in my day" post, so I re-ran the measurement right before publishing. Opened a tab via Chrome MCP, ran an rAF counter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;document.visibilityState&lt;/td&gt;
&lt;td&gt;hidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rAF fires in 3.37s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setInterval(100ms) fires, same window&lt;/td&gt;
&lt;td&gt;4 (expected: 33)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setTimeout(3000ms) actual delay&lt;/td&gt;
&lt;td&gt;3373ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;rAF wasn't throttled. It was stopped — zero frames.&lt;/p&gt;

&lt;p&gt;And as a bonus finding: &lt;code&gt;setInterval&lt;/code&gt; was decimated to roughly 1/8 of its expected rate, and even &lt;code&gt;setTimeout&lt;/code&gt; drifted. So it's not just rAF-based animation; timer-driven logic in general cannot be trusted in a hidden tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open tabs with &lt;code&gt;active: true&lt;/code&gt; so they're visible&lt;/li&gt;
&lt;li&gt;Trigger the interaction in a visible tab, wait a few seconds, then screenshot and confirm the rendered output actually exists&lt;/li&gt;
&lt;li&gt;If you must evaluate in a hidden tab, the report has to say "dynamic rendering not visually observed." Never silently pass it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cause 2: confusing "healthy JS state" with "the feature works"
&lt;/h2&gt;

&lt;p&gt;The second trap shows up in AI QA reports as pass rationale like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;onclick wiring confirmed. Zero JS errors. Library loaded. → PASS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All of that describes code-path health, not feature behavior. Wiring can be correct while nothing renders (Cause 1). Errors can be absent while the downloaded file is empty.&lt;/p&gt;

&lt;p&gt;My fix: grep the template for dynamic features first, then require a behavior check for every hit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Found in code&lt;/th&gt;
&lt;th&gt;Required behavior check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interact in a visible tab → screenshot after a delay shows rendered content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upload a dummy file → preview src is non-empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;download&lt;/code&gt; attr / &lt;code&gt;toBlob&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Press the button → observe Blob creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mousedown&lt;/code&gt; / &lt;code&gt;touchstart&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fire the event → state/transform actually changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transform values differ after time passes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QA reports must include a feature-coverage table, and "exists in code but behavior unverified" can never be graded as pass. That's the whole rule.&lt;/p&gt;

&lt;p&gt;That alone cut the false positives dramatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: screenshot-vs-DOM contradiction checks
&lt;/h2&gt;

&lt;p&gt;A side benefit of visual QA: cross-checking what the code claims against what the pixels show. "The class is &lt;code&gt;text-secondary&lt;/code&gt; (cyan) but it renders gray" — cascade and override bugs get caught with evidence attached. Humans tend to stall at "the color feels off somehow."&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and caveats
&lt;/h2&gt;

&lt;p&gt;Hidden-tab throttling is a browser power-saving behavior, so Chrome could change it. My operating principle isn't "hidden tabs are bad" — it's "dynamic rendering must be verified in a visible state."&lt;/p&gt;

&lt;p&gt;Behavior checks cost time and tokens. Running them on every feature of every tool is too heavy; I only require them for tools where the grep finds dynamic features.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified: April–June 2026 in production, re-reproduced July 10, 2026. Environment: Claude + Chrome MCP / Windows 11.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>qa</category>
      <category>chrome</category>
    </item>
    <item>
      <title>Automating an app with no DOM: driving Flutter/canvas editors with coordinates only</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:35:36 +0000</pubDate>
      <link>https://dev.to/mxhlix/automating-an-app-with-no-dom-driving-fluttercanvas-editors-with-coordinates-only-5eci</link>
      <guid>https://dev.to/mxhlix/automating-an-app-with-no-dom-driving-fluttercanvas-editors-with-coordinates-only-5eci</guid>
      <description>&lt;p&gt;In my last post I said that for normal HTML pages, element-based automation (&lt;code&gt;find&lt;/code&gt; / &lt;code&gt;read_page&lt;/code&gt;) beats coordinates every time. This post is about the apps where that advice is useless.&lt;/p&gt;

&lt;p&gt;Flutter Web apps. Canvas-rendered editors. Every button and panel you can see on screen doesn't exist in the DOM — it's all pixels painted onto a single canvas. &lt;code&gt;find&lt;/code&gt; returns nothing. &lt;code&gt;read_page&lt;/code&gt;'s accessibility tree is effectively empty.&lt;/p&gt;

&lt;p&gt;I got Claude to drive the Rive editor (an animation tool built with Flutter) all the way through selecting assets and exporting them. Here's the procedure that survived contact with reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step zero: confirm you're actually in this situation
&lt;/h2&gt;

&lt;p&gt;Coordinate automation is fragile, so you should only accept it after ruling out the alternative. The test is quick: run &lt;code&gt;read_page&lt;/code&gt;. If the visible UI has almost no corresponding nodes, you're looking at a canvas-rendered app, and coordinates are the only interface you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Wait for the window size to settle before anything else
&lt;/h3&gt;

&lt;p&gt;Same failure mode as my previous post: right after load, the viewport hasn't reached its final width (I measured 1664→1920 over 2–3 seconds), and clicks based on an early screenshot land to the right of the target. Read &lt;code&gt;innerWidth&lt;/code&gt; via &lt;code&gt;javascript_tool&lt;/code&gt; twice; only proceed when two consecutive reads match. But matching &lt;code&gt;innerWidth&lt;/code&gt; alone isn't enough — also confirm &lt;code&gt;devicePixelRatio&lt;/code&gt; hasn't changed since the screenshot you're about to act on (a follow-up to my previous post surfaced this: when DPI or scaling changes, the whole coordinate space rescales the same way, but the new values stabilize immediately, so an &lt;code&gt;innerWidth&lt;/code&gt;-only check can't catch it). Canvas apps deserve extra paranoia here, because there is no element-based fallback when a click misses.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read text by zooming, not by extracting
&lt;/h3&gt;

&lt;p&gt;Text painted on canvas can't be pulled out of the DOM. To read a menu item or panel label, &lt;code&gt;zoom&lt;/code&gt; into that region and read the enlarged screenshot as an image. Full-page screenshots make small text easy to misread — make "zoom before you read" a reflex.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Demand visual evidence after every click
&lt;/h3&gt;

&lt;p&gt;A missed coordinate click produces no error. Nothing. And if you proceed without noticing, every subsequent step operates on a false premise.&lt;/p&gt;

&lt;p&gt;So: click → screenshot → confirm the visual evidence of selection (in Rive, the blue highlight; in other apps, a selection outline or color change) → only then continue. No evidence, no progress — re-derive the coordinates and retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Build double-clicks from two rapid single clicks
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;double_click&lt;/code&gt; action can exceed the app's double-click threshold and degrade into two single clicks. Canvas editors love double-click-to-enter-hierarchy interactions, so this will block you. Fire two &lt;code&gt;left_click&lt;/code&gt;s at the same coordinates inside one &lt;code&gt;browser_batch&lt;/code&gt; — the gap tightens enough to register.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run (Rive editor)
&lt;/h2&gt;

&lt;p&gt;Putting it together, "select an asset and export it" looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the editor → wait until &lt;code&gt;innerWidth&lt;/code&gt; stabilizes&lt;/li&gt;
&lt;li&gt;Full screenshot → locate the target panel&lt;/li&gt;
&lt;li&gt;Rapid double-fire on the object → screenshot → confirm blue highlight&lt;/li&gt;
&lt;li&gt;Click the menu coordinates → zoom the menu region → read the item labels&lt;/li&gt;
&lt;li&gt;Click Export → screenshot → confirm the dialog appeared&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every step has a verification wedged after it, so the operation count is easily double what a human would need.&lt;/p&gt;

&lt;p&gt;Slow, but it keeps moving.&lt;/p&gt;

&lt;p&gt;Chasing speed with coordinate automation produces chains of unnoticed misses, and those cost far more time than the checks do. That was the actual lesson of running this in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and caveats
&lt;/h2&gt;

&lt;p&gt;This approach assumes a stable screen layout. Pages that reflow constantly (ads, notifications) degrade coordinate reliability even further.&lt;/p&gt;

&lt;p&gt;Any specific app's UI layout changes with updates — Rive's included. What transfers is the procedure, not the pixels: settle-wait → zoom-read → evidence-check → rapid double-fire.&lt;/p&gt;

&lt;p&gt;Verified on Windows / 1920×1080. Different DPI scaling may change coordinate behavior.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified: June 2026. Environment: Windows 11 / Chrome + Claude in Chrome extension / Rive editor (Flutter/canvas).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Claude in Chrome keeps missing clicks. I measured why — there are exactly two causes.</title>
      <dc:creator>Mxhlix</dc:creator>
      <pubDate>Sat, 11 Jul 2026 05:43:04 +0000</pubDate>
      <link>https://dev.to/mxhlix/claude-in-chrome-keeps-missing-clicks-i-measured-why-there-are-exactly-two-causes-2m8p</link>
      <guid>https://dev.to/mxhlix/claude-in-chrome-keeps-missing-clicks-i-measured-why-there-are-exactly-two-causes-2m8p</guid>
      <description>&lt;p&gt;If you use Claude in Chrome (Chrome MCP) for browser automation, you have probably hit some of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clicks land next to the target, not on it&lt;/li&gt;
&lt;li&gt;The pointer drifts right of the element, and retrying never fixes it&lt;/li&gt;
&lt;li&gt;You take a screenshot, and moments later the page dimensions have changed&lt;/li&gt;
&lt;li&gt;Double-clicks don't enter edit mode — you get a row selection instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run 600+ web tools built and QA'd through AI-driven browser automation, so I hit these constantly. I burned hours suspecting the page elements. They were fine.&lt;/p&gt;

&lt;p&gt;The real problem: the assumptions behind coordinate-based clicking were breaking. It comes down to exactly two causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: the window width settles late, so coordinates drift right
&lt;/h2&gt;

&lt;p&gt;Right after a page loads, &lt;code&gt;innerWidth&lt;/code&gt; can report a smaller value than the real window. On my machine it started at 1664. Even after &lt;code&gt;document.readyState&lt;/code&gt; hits &lt;code&gt;complete&lt;/code&gt;, it keeps widening for 2–3 seconds until it reaches the true width (1920). During that window, &lt;code&gt;outerWidth&lt;/code&gt; / &lt;code&gt;outerHeight&lt;/code&gt; sometimes return 0.&lt;/p&gt;

&lt;p&gt;Now take a screenshot during that settling period and click using its coordinates. The page has widened since the capture, so your click lands to the right of the target. 1664→1920 is a 1.15× stretch — the further right the element, the bigger the miss.&lt;/p&gt;

&lt;p&gt;That's the whole mystery: "the page changed size after my screenshot" and "the pointer keeps drifting" are the same bug.&lt;/p&gt;

&lt;p&gt;This is not limited to exotic canvas apps. Ordinary HTML pages with lazy loading or dynamic layout do it too. Some of my "this input just won't click" sessions were nothing more than coordinates swinging at empty air.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to diagnose it
&lt;/h3&gt;

&lt;p&gt;Read the viewport with &lt;code&gt;javascript_tool&lt;/code&gt; and check whether it has stabilized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readyState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;dpr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;innerW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;innerH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;outerW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outerWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outerH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outerHeight&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four rules. If &lt;code&gt;outerW&lt;/code&gt; is 0, the window size hasn't settled — do not interact yet. Read again after a moment; if &lt;code&gt;innerW&lt;/code&gt; changed, it's still settling — keep waiting. Two consecutive identical &lt;code&gt;innerW&lt;/code&gt; readings with a non-zero &lt;code&gt;outerW&lt;/code&gt; and the size is clear. The fourth rule guards against a different failure entirely: &lt;strong&gt;&lt;code&gt;dpr&lt;/code&gt; has to match whatever it was when you took your screenshot.&lt;/strong&gt; Drag the window onto a monitor with different DPI scaling and every coordinate rescales — same math as the 1664→1920 stretch — except nothing ever looks unsettled, because the new values are correct and stable from the very first read. An innerW-only check waves it straight through. (Pointed out by &lt;a class="mentioned-user" href="https://dev.to/skillselion"&gt;@skillselion&lt;/a&gt; in the comments — see the update note below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: &lt;code&gt;double_click&lt;/code&gt; fires too slowly and becomes two single clicks
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;computer&lt;/code&gt; tool's &lt;code&gt;double_click&lt;/code&gt; action can leave a gap between the two clicks that exceeds the app's double-click threshold. The app interprets it as two single clicks — so instead of entering text-edit mode, you get a row selection or nothing.&lt;/p&gt;

&lt;p&gt;The nasty part: the tool believes it sent a double-click. The logs look correct.&lt;/p&gt;

&lt;p&gt;You cannot see this failure in any log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Plan A (preferred): stop using coordinates, use element references
&lt;/h3&gt;

&lt;p&gt;On a normal HTML page this is the reliable path. Drop coordinate clicks entirely. Use &lt;code&gt;find&lt;/code&gt; (natural-language element search) or &lt;code&gt;read_page&lt;/code&gt; (accessibility tree) to get element references, then click and fill through those references (&lt;code&gt;form_input&lt;/code&gt; for values). Element references survive layout shifts.&lt;/p&gt;

&lt;p&gt;They're not a free pass, though. A ref is a pointer to one specific node and nothing more — it knows nothing about where that node sits on the page or what it currently means to the user. Replace the node in a rerender and the ref dies with an explicit error. Move the node, or let it take on a different role, and the ref keeps right on working: it reports success and clicks whatever that node has become, wherever it now lives. That second case is a silent miss with a clean log. Both reproduce in plain vanilla JS, so there's no framework magic involved — a React rerender just does the replacing for you. The fix is the same either way: &lt;strong&gt;treat refs as disposable. Grab one right before you use it, use it once, and never carry it across a navigation or a form submit&lt;/strong&gt; (credit to &lt;a class="mentioned-user" href="https://dev.to/skillselion"&gt;@skillselion&lt;/a&gt; in the comments for the single-use rule). One more caveat I'd rather admit than bury: I've watched a perfectly fresh ref click no-op silently while the window was still settling, which tells me ref clicks get resolved to coordinates somewhere under the hood. Plan A shrinks the coordinate problem. It does not make it go away.&lt;/p&gt;

&lt;p&gt;The exception: canvas/Flutter apps (the Rive editor, for example). Their UI doesn't exist as HTML elements, so &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;read_page&lt;/code&gt; return nothing. Then Plan B is your only option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan B: if you must use coordinates, pair "wait for settle" with "rapid double-fire"
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Wait for the size to settle before doing anything — use the diagnostic snippet above until &lt;code&gt;innerW&lt;/code&gt; stabilizes, then take your screenshot&lt;/li&gt;
&lt;li&gt;Don't let time pass between screenshot and click. Capture → click on those fresh coordinates, as one unit. If anything intervened, re-capture&lt;/li&gt;
&lt;li&gt;Never use &lt;code&gt;double_click&lt;/code&gt;. Build your own with two rapid &lt;code&gt;left_click&lt;/code&gt;s at the same coordinates via &lt;code&gt;browser_batch&lt;/code&gt; — the gap gets tight enough to pass the threshold:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;browser_batch&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;computer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left_click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;computer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left_click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;coordinate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;tabId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;After every click, &lt;code&gt;zoom&lt;/code&gt; into the target and verify the expected state change (selection, edit mode, highlight) before moving on. One unnoticed miss desyncs everything after it&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First suspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clicks miss&lt;/td&gt;
&lt;td&gt;Size not settled (Cause 1) — wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double-click doesn't work&lt;/td&gt;
&lt;td&gt;Switch to rapid double-fire (Cause 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stuck on a normal HTML page&lt;/td&gt;
&lt;td&gt;Switch to Plan A (element refs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ref click errors out or hits the wrong element&lt;/td&gt;
&lt;td&gt;Stale ref — re-find immediately before the interaction (refs are single-use)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canvas/Flutter app&lt;/td&gt;
&lt;td&gt;Plan B only — there are no elements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limits and caveats
&lt;/h2&gt;

&lt;p&gt;The numbers here (1664→1920, 2–3 seconds) are from my setup (Windows, 1920×1080). Display config and DPI scaling will change them. Take the principle, not the numbers: wait for settle before you capture.&lt;/p&gt;

&lt;p&gt;Claude in Chrome ships updates frequently. If the tool ever starts waiting for size-settle itself, this whole workaround becomes unnecessary. And coordinate-based automation is inherently fragile — whenever element references are available, they win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update (2026-07-11)
&lt;/h2&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/skillselion"&gt;@skillselion&lt;/a&gt; left a comment below that made this article better, and I've revised Plan A and the settle rules accordingly. I didn't take either point on faith — both got re-verified in plain vanilla JS before I touched the text. Refs really do track node identity and nothing else: kill the node and the ref dies loud, move it and the ref follows it silently into whatever it has become. And the innerW-only settle check really is blind to DPI changes — by design, since a rescaled-but-stable viewport is indistinguishable from a settled one if all you compare is innerW. I couldn't physically test the cross-monitor move (one display here), but my own setup argued the point for me: the same physical 1920px screen that settled at innerWidth 1920 in June now reports 1745 at a fractional dpr of 1.1. The full back-and-forth is in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified: June 2026; Plan A and settle conditions re-verified July 2026 (dpr 1.1 / 110% scaling environment). Environment: Windows 11 / Chrome + Claude in Chrome extension / 1920×1080, including automation of the Rive editor (Flutter/canvas).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>chrome</category>
      <category>automation</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
