Skip to content

fix(snapshot): escape line breaks in UI tree names and values - #615

Open
tanishqbaweja wants to merge 1 commit into
CursorTouch:mainfrom
tanishqbaweja:fix/snapshot-line-breaks
Open

tanishqbaweja wants to merge 1 commit into
CursorTouch:mainfrom
tanishqbaweja:fix/snapshot-line-breaks

Conversation

@tanishqbaweja

Copy link
Copy Markdown
Contributor

Problem

Snapshot's UI tree (semantic_tree_to_string() in tree/views.py) prints each element's name and value as the walk stored them, and the walk strips only their ends (CachedName.strip(), value.strip()). When a name or value spans lines, its element spans several lines of the tree. The lines after the first have no tree prefix, coordinates or action, so they read like other entries and the agent cannot tell where the element ends. A multi-line field is the common case: for an edit or document element the walk reads its value, which is the text in the field.

Those lines also shift the tree in the response. build_snapshot_response() passes the UI Tree: section through dedent(), and a line that starts at column 0 leaves it nothing to remove, so the header and every tree line move four spaces right.

The text selection already avoids this: #551 escaped its CR and LF "so it stays on its element's tree line".

Reproduction

At 106b50c on Windows 11 (build 22631) with Python 3.14.9, with mocks only. A script built a window out of MagicMock UIA elements that hold the cached properties the walk reads: an edit "Comments" whose value is "Works well.\r\nOne crash on resume." and a button "Submit". It ran them through the real Tree.get_state() and tree_traversal(), with ControlFromHandle, the cache requests and the children lookup patched, then through the real capture_desktop_state() and build_snapshot_response() with a stand-in desktop. The end of the response on main:

    UI Tree:
    desktop
    └── window "Feedback"
        ├── (320,120) edit "Comments"  [action: fill]  [value:"Works well.
One crash on resume."]
        └── (570,215) button "Submit"  [action: click]

With a one-line value the section starts at column 0, as below. semantic_tree_to_string() returns 5 lines for these 4 nodes with CRLF, with LF alone and with CR alone (counted with splitlines()); dedent() only counts LF, so a lone CR does not shift the section.

Fix

tree/views.py: _render_semantic_node() escapes CR and LF in each tree line as \r and \n. The escape is #551's, moved into _escape_line_breaks(), which _selection_preview() now calls. The reproduction then gives:

UI Tree:
desktop
└── window "Feedback"
    ├── (320,120) edit "Comments"  [action: fill]  [value:"Works well.\r\nOne crash on resume."]
    └── (570,215) button "Submit"  [action: click]
  • Every kind of tree line gets it: window, container, element, scrollable area and page text. Lines without a line break render exactly as before.
  • Only the rendering changes. The walk still stores names and values as they are, so WaitFor and Scrape see the same text. The flat lists (interactive_elements_to_string(), scrollable_elements_to_string()) are unchanged; Snapshot does not print them.
  • As for the selection, backslashes are left alone so paths read normally; a literal \n in a name looks like an escaped break.
  • Long values are still printed whole. Nothing in the walk or the tree shortens names or values (only the selection preview is cut, at 100 characters). If you want long values cut as well, I can do that separately.

Tests

New TestLineBreaks in tests/test_tree_views.py, which builds the dataclasses only:

  • test_multiline_value_stays_on_its_element_line: an edit whose value is one\r\ntwo\rthree\nfour (CRLF, CR and LF). The tree keeps 3 lines and the edit's line ends with [value:"one\r\ntwo\rthree\nfour"].
  • test_multiline_names_stay_on_their_element_lines: a window, a group, a button, a scrollable pane and a text with that name. The tree keeps one line per node, each with the escaped name.

I ran each file on its own, in the project's environment:

python -m pytest tests/test_tree_views.py -v
python -m pytest tests/test_tree_service.py
python -m pytest tests/test_surrogate_repair.py
python -m pytest tests/test_snapshot_display_filter.py

On main, both new tests fail on the line count: assert 6 == 3 and assert 21 == 6. Both pass on this branch, as do the file's 29 other tests, #551's test_multiline_selection_stays_on_one_tree_line among them. These pass on main and on this branch with the same counts: tests/test_tree_service.py (50 passed), tests/test_surrogate_repair.py (18) and tests/test_snapshot_display_filter.py (37). I did not run the whole suite.

ruff check is clean on both changed files. Neither file is formatted on main; this change adds nothing to their ruff format --diff.

Not tested

A real app: everything ran against mocks. To check it on a real machine, type two lines into a multi-line text box (Notepad's, say) and call Snapshot. On main, the box's value should continue at the start of the next line of the UI tree, with the UI Tree: section indented four spaces; with this change the value should stay on the element's line, with \r\n or \r where the text breaks.

semantic_tree_to_string() printed each element's name and value as the
walk stored them, and the walk strips only their ends. A name or value
that spans lines, such as the text of a multi-line field, split its
element across several lines of the UI tree. The lines after the first
had no tree prefix, so they read like separate entries, and in the
Snapshot response they also kept dedent() from removing the template's
indentation, which shifted the whole UI Tree section four spaces right.

Escape CR and LF in each rendered tree line as \r and \n, as the text
selection already is, through one helper that both now use. Only the
rendering changes: the walk still stores names and values as they are,
so WaitFor and Scrape see the same text, and lines without line breaks
render exactly as before.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant