Skip to content

Compact the error a failed command reports, instead of Playwright's whole retry log - #132

Merged
DavertMik merged 2 commits into
mainfrom
fix/condense-playwright-call-log
Aug 22, 2026
Merged

Compact the error a failed command reports, instead of Playwright's whole retry log#132
DavertMik merged 2 commits into
mainfrom
fix/condense-playwright-call-log

Conversation

@DavertMik

@DavertMik DavertMik commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

A failed browser command handed the model Playwright's entire call log — ~850 characters for one disabled Save button: every wait and retry line, element is not enabled once per attempt, a class attribute broken across six lines, and ANSI color codes around all of it.

Before

TimeoutError: click: Timeout 3000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Save' }).first()
  - locator resolved to <button disabled type="button" class="secondary-btn
      btn-text-and-icon

      btn-lg

      disabled">…</button>
  - attempting click action
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
    - waiting 20ms
    … 8 more lines

After (192 chars)

TimeoutError: click: Timeout 3000ms exceeded. - locator resolved to <button disabled type="button" class="secondary-btn btn-text-and-icon btn-lg disabled">…</button> - element is not enabled

The implementation

export function compactErrorMessage(error: unknown): string {
  let text = stripAnsi(String(error));
  for (const strip of STRIP_STRATEGIES) {
    text = strip(text);
  }
  return truncate(text, MAX_COMPACT_ERROR);
}

In src/utils/strings.ts, next to the normalizeInlineText and truncate it reuses. It takes anything and stringifies inside, so no call site wraps its error. No regex, no new file, no new module.

stripCallLog is the only strategy today: split on the marker, drop waiting / retrying / attempting lines, dedupe the rest with a Set, join. Another error shape — a network failure, a stack trace — is a second function in STRIP_STRATEGIES, and both the marker and its noise list stay local to the strategy that uses them.

The locator line goes with the other waiting lines because the tool result already carries command and locator; repeating it costs tokens and tells the model nothing new. Errors with no Call log: section (MultipleElementsFound and its element listing) come back unchanged.

The seven raw lastError.toString() / String(lastError) sites in click, hover, pressKey, form, back and reset now route through errorText(), so that stringification exists in one place.

clickFailureSuggestion() reads this text to tell the model whether the element was disabled, covered, hidden or absent. All four markers sit on lines the filter keeps.

Natural next caller

navigator.ts:476 trims a failed attempt inline — first non-at line, capped at 220. That is a stack-strip strategy waiting to move into this list, but it changes navigator's output, so it is not in this PR.

Tests

bun test tests/integration/ 81 pass. Unit suite passes except the 12 renderCall failures from the asLocator lookup on main, which #131 fixes.

🤖 Generated with Claude Code

@DenysKuchma DenysKuchma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix tests

@DavertMik
DavertMik force-pushed the fix/condense-playwright-call-log branch from 48e38ea to c25c125 Compare August 22, 2026 00:36
A failed browser command handed the model Playwright's entire call log: every
wait and retry line, the same "element is not enabled" once per attempt, a class
attribute broken across six lines, and terminal color codes around all of it.
The one fact that decides what to do next -- the element resolved but was
disabled -- sat buried in ~850 characters of bookkeeping.

compactErrorMessage() strips ANSI, runs the text through the strip strategies
and truncates. One strategy today: the call log loses its waiting/retrying/
attempting lines, the rest is deduped, leaving the element that was resolved and
why it could not be clicked. The locator is not repeated -- the tool result
already carries the command. Errors with no call log pass through untouched.

The seven raw lastError.toString()/String(lastError) sites in click, hover,
pressKey, form, back and reset now route through errorText(), so that
stringification exists once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavertMik
DavertMik force-pushed the fix/condense-playwright-call-log branch from c25c125 to c479cd7 Compare August 22, 2026 00:43
@DavertMik DavertMik changed the title Give the AI the reason a click failed, not Playwright's retry log Compact the error a failed command reports, instead of Playwright's whole retry log Aug 22, 2026
@DavertMik
DavertMik merged commit 52f34ee into main Aug 22, 2026
2 checks passed
@DavertMik
DavertMik deleted the fix/condense-playwright-call-log branch August 22, 2026 10:08
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.

2 participants