Read structure instead of layout - #1
Conversation
Moli keeps page structure in memory and renders only when a picture is needed, so geometry is a snapshot from the last render while the DOM moves on. Measured on Google Flights: 140 of 146 interactive elements report a zero-sized box, and innerText returns 25 characters where textContent returns 89,091. So nothing here consults layout. Liveness comes from semantics, text from textContent, and actions dispatch on the element rather than at a coordinate. Two jobs the viewport cull was doing implicitly had to be replaced: - Bounding the candidate list. Every link on a Wikipedia article is now a candidate, which exceeds the decision API's 255-choice limit. Replaced with a relevance cull: fields and dropdowns first, then controls the goal names, then the rest in document order. - Disambiguating same-named controls. Google's date picker has four buttons reading 'Done' and only one commits the date; on screen there was only ever one. Labels now carry the control's nearest landmark. Verified on Moli: Wikipedia article navigation, Python docs navigation and GitHub tab navigation all complete in two steps. Heavy single-page apps that swap a field for a popup mid-interaction are not yet reliable. Signed-off-by: waple0820 <feng.wang@lexmount.com>
|
Claude finished @waple0820's task in 1m 35s —— View job Code Review
Three bugs and one security concern. Bug 1 — Uncaught
try:
step.outcome = self.browser.act(action, text)
except PageChanged as error:
...
continue
after = self.browser.observe() # ← no try/except here
try:
after = self.browser.observe()
except PageChanged:
yield self.run_state
continueBug 2 — CSS selector crash on element IDs containing
const owner = panel.id && document.querySelector(
`[aria-controls="${panel.id}"],[aria-owns="${panel.id}"]`
);An const owner = panel.id && (() => {
try {
return document.querySelector(
`[aria-controls="${CSS.escape(panel.id)}"],[aria-owns="${CSS.escape(panel.id)}"]`
);
} catch { return null; }
})();Bug 3 —
targets = cdp("Target.getTargets")["targetInfos"]
page = next((t for t in targets if t["type"] == "page"), targets[0])If if not targets:
raise RuntimeError("No browser targets found in session")
page = next((t for t in targets if t["type"] == "page"), targets[0])Security — Prompt injection from page content into the text model
{"role": "user", "content":
f"Goal: {goal}\nField: {action.label}\n"
f"Current value: {action.value or '(empty)'}\n"
f"Already done: {history[-5:]}"},
|
There was a problem hiding this comment.
Automated Claude review completed. Review run: https://github.com/lexmount/jev-nolayout/actions/runs/35604132214. Please check Claude's inline feedback and summary comment for details.
Bug 1 -- act() swallows PageChanged inside settle(), so a navigation still in flight when the 700 ms settle timer expires surfaced on the observe() immediately after, uncaught, killing the generator. The action had already happened, so it is now recorded as taken and the loop reads the page it landed on next time round. Bug 2 -- panel.id was interpolated raw into a CSS attribute selector. An id containing a quote or bracket throws a SyntaxError that takes the whole snapshot down, and the caller reads that as 'the document is navigating' -- a healthy page misdiagnosed as a broken one. Escaped with CSS.escape and guarded. Bug 3 -- an empty targetInfos made the fallback targets[0] raise a bare IndexError. Now says what actually went wrong. Security -- a field label is scraped from the page, and the text model's system prompt never said so. A page can name an element anything, including text shaped like an instruction. The prompt now states that everything outside the goal is data rather than instructions, and page-derived values are wrapped in tags so the boundary is explicit. Verified after the changes: Wikipedia, Python docs and GitHub navigation all still complete and land on the expected URL. Signed-off-by: waple0820 <feng.wang@lexmount.com>
|
Claude finished @waple0820's task in 2m 20s —— View job Code Review
All four issues from the prior review are fixed and the fixes are correct. One residual concern in the security fix is worth noting. Bug 1 (agent.py Bug 2 (snapshot.js CSS selector crash) — Fixed correctly. Bug 3 (session.py empty Security (TEXT_VALUE prompt injection) — Substantially improved but not fully closed. Residual prompt injection via tag injection
f"<field>{action.label}</field>\n"
f"<value>{action.value or '(empty)'}</value>\n"
f"<history>{history[-5:]}</history>"},The delimiter defence assumes page content cannot contain the closing tags. An element whose Fix with import html as _html
f"<field>{_html.escape(action.label)}</field>\n"
f"<value>{_html.escape(action.value or '(empty)')}</value>\n"
f"<history>{_html.escape(str(history[-5:]))}</history>"},
|
|
All four addressed in Bug 1 — uncaught Bug 2 — unescaped Bug 3 — Security — page-controlled labels reaching the text model. This was the real one. Re-verified after the changes — all three land on the expected URL:
The Google Flights limitation in the README's Status section is unchanged — still the open problem. |
There was a problem hiding this comment.
Automated Claude review completed. Review run: https://github.com/lexmount/jev-nolayout/actions/runs/35605760838. Please check Claude's inline feedback and summary comment for details.
Moli keeps page structure and interaction state in memory and renders only when a picture is actually needed. Geometry there is a snapshot from the last render while the DOM moves on — so an extractor that filters on
getBoundingClientRect()is asking the wrong question.Measured on Google Flights, same page and same selectors, only the test changed:
The DOM is identical in both cases — 146 interactive elements — but 140 report a zero-sized box.
What this adds
snapshot.js— reads a page with no call togetBoundingClientRect,checkVisibility,elementFromPointorinnerTextbrowser.py— dispatches on the element rather than at a coordinate, so a stale layout cannot misdirect a clickmodel.py— Jev's two-stage operation/target questions, one round trip per stepagent.py,cli.py,examples/flights.pyTwo jobs the viewport cull was doing implicitly
Removing geometry broke two things that nobody had written down, because geometry had been handling them for free.
Bounding the candidate list. Every link on a Wikipedia article becomes a candidate, and the decision API rejects the request outright:
Replaced with a relevance cull: fields and dropdowns first, then controls the goal names, then the rest in document order.
Disambiguating same-named controls. Google's date picker has four buttons reading
Doneand only one commits the date; on screen there was only ever one candidate. Labels now carry the control's nearest landmark:Verified on Moli
Each lands on the expected URL. The same agent runs against standard Chrome with
--browser normal.Not yet working
examples/flights.pydoes not pass. Heavy single-page applications that swap a field for a popup mid-interaction confuse the loop: fillingWhere from?opens an autocomplete list that replaces the field, the next observation no longer shows the typed value, and the model reads that as "the text did not take" and types it again.The fill itself works — eight Zürich suggestions appear. What is missing is a way to tell the model that its action revealed something rather than failed. This is the next problem to solve and is called out in the README's Status section rather than left for someone to discover.