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
- Update opencli / the library to the latest version so the Save-button regex matches X's current DOM
- Retry once — the dialog may simply not have finished rendering; a re-run re-scrapes the DOM after fresh load
- 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, 保存, 完成, 儲存)
- 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
- Keep the opencli Chrome extension and library up to date (X DOM changes break selectors)
- Use an x.com UI language whose Save/Done label the scraper recognizes (English, Japanese, Traditional Chinese)
- Don't scroll, click, or resize the browser while the removal flow runs
- If it recurs, capture the dialog HTML and file a bug with the current button markup
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
- Booking.com page declared results but no property cards were
- Failed to fill rename input.
- Codex picker did not open after typing the command.
- Ctrip bus rows rendered but parser did not find required sch
- Ctrip package cards rendered but parser did not find require
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/e732ccacd94b7be1.
Report an issue: GitHub.