jackwener/OpenCLI · error · CommandExecutionError

Save button not found in dialog.

Error message

Save button not found in dialog.

What it means

listRemoveUser automates removing a user from an X (Twitter) list via the Edit Members dialog. The in-page DOM scraper locates the dialog's Save/Done button and reports its click coordinates; when no button matching /^(Save|Done|保存|完成|儲存)$/i is found inside the dialog, saveClickX is null and the library throws this error rather than clicking blindly.

Source

Thrown at clis/twitter/list-remove-core.js:230

                    saveClickY: saveRect ? Math.round(saveRect.top + saveRect.height / 2) : null,
                    mutationsBefore: window.__opencliListMutations.length,
                };
            } catch (e) {
                return { ok: false, message: 'UI error: ' + (e?.message || String(e)) };
            }
        })()`));

        if (!uiResult.ok) {
            throw new CommandExecutionError(`Failed to remove @${username} from list ${listId}: ${uiResult.message}`);
        }

        let verifiedBy = null;
        if (uiResult.needsNativeInteraction) {
            if (typeof page.nativeClick !== 'function') {
                throw new CommandExecutionError('Requires up-to-date Chrome extension (nativeClick).');
            }
            if (!uiResult.saveClickX) {
                throw new CommandExecutionError('Save button not found in dialog.');
            }
            const memberCountBefore = Number(targetList.members) || 0;
            await page.nativeClick(uiResult.rowClickX, uiResult.rowClickY);
            await new Promise((r) => setTimeout(r, 800));
            await page.nativeClick(uiResult.saveClickX, uiResult.saveClickY);
            await new Promise((r) => setTimeout(r, 3500));
            const listsAfter = unwrapBrowserResult(await page.evaluate(`async () => {
                const r = await fetch(${JSON.stringify(listsUrl)}, { headers: ${headers}, credentials: 'include' });
                if (!r.ok) return { __error: 'HTTP ' + r.status };
                return await r.json();
            }`));
            if (listsAfter && listsAfter.__error) {
                throw new CommandExecutionError(`Could not verify list removal: ${listsAfter.__error}`);
            }
            if (!getListsManagementInstructions(listsAfter)) {
                throw new CommandExecutionError('Could not verify list removal: unexpected lists payload shape');
            }
            const parsedAfter = parseListsManagement(listsAfter, new Set());

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Update opencli / the library to the latest version so the Save-button regex matches X's current DOM
  2. Retry once — the dialog may simply not have finished rendering; a re-run re-scrapes the DOM after fresh load
  3. Check the UI language of the logged-in x.com account; switch to English or a language whose Save/Done label is supported (Save, Done, 保存, 完成, 儲存)
  4. Inspect the live dialog DOM and report/patch the selector in list-remove-core.js (dialog.querySelectorAll('[role=button], button') text match) if X changed the markup

Example fix

// before (library-side heuristic)
const saveButton = [...dialog.querySelectorAll('[role="button"], button')].find(b => /^(Save|Done|保存|完成|儲存)$/i.test((b.innerText||'').trim()));
// after (caller-side retry)
try { await listRemoveUser(page, { listId, username }); }
catch (e) {
  if (String(e.message).includes('Save button not found')) await new Promise(r=>setTimeout(r,2000)), await listRemoveUser(page, { listId, username });
  else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// best pre-check: none from caller (DOM-internal), but ensure extension support
if (typeof page.nativeClick !== 'function') throw new Error('Update the Chrome extension before list removals.');

Try / catch

try {
  await listRemoveUser(page, { listId, username });
} catch (e) {
  if (String(e.message).includes('Save button not found')) {
    await sleep(2000); // let dialog finish rendering, retry once
    return listRemoveUser(page, { listId, username });
  }
  throw e;
}

Prevention

When it happens

Trigger: The dialog was open and a member row was found (needsNativeInteraction true), but no [role=button]/button element inside the dialog had exact text Save, Done, 保存, 完成, or 儲存 — e.g. X changed the button label/role, the button hadn't rendered yet, or the selector matched a dialog that is not the edit-members dialog.

Common situations: X front-end A/B changes renaming the Save button ('Apply', an icon-only button, or a localized label not in the regex); slow loading so the footer button hadn't mounted when the DOM was scraped; scraping a stale/hidden dialog left over from a prior operation; running in a non-supported UI language.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/e732ccacd94b7be1. Report an issue: GitHub.