{"record":{"id":"e732ccacd94b7be1","repo":"jackwener/OpenCLI","slug":"save-button-not-found-in-dialog","errorCode":null,"errorMessage":"Save button not found in dialog.","messagePattern":"Save button not found in dialog\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/list-remove-core.js","lineNumber":230,"sourceCode":"                    saveClickY: saveRect ? Math.round(saveRect.top + saveRect.height / 2) : null,\n                    mutationsBefore: window.__opencliListMutations.length,\n                };\n            } catch (e) {\n                return { ok: false, message: 'UI error: ' + (e?.message || String(e)) };\n            }\n        })()`));\n\n        if (!uiResult.ok) {\n            throw new CommandExecutionError(`Failed to remove @${username} from list ${listId}: ${uiResult.message}`);\n        }\n\n        let verifiedBy = null;\n        if (uiResult.needsNativeInteraction) {\n            if (typeof page.nativeClick !== 'function') {\n                throw new CommandExecutionError('Requires up-to-date Chrome extension (nativeClick).');\n            }\n            if (!uiResult.saveClickX) {\n                throw new CommandExecutionError('Save button not found in dialog.');\n            }\n            const memberCountBefore = Number(targetList.members) || 0;\n            await page.nativeClick(uiResult.rowClickX, uiResult.rowClickY);\n            await new Promise((r) => setTimeout(r, 800));\n            await page.nativeClick(uiResult.saveClickX, uiResult.saveClickY);\n            await new Promise((r) => setTimeout(r, 3500));\n            const listsAfter = unwrapBrowserResult(await page.evaluate(`async () => {\n                const r = await fetch(${JSON.stringify(listsUrl)}, { headers: ${headers}, credentials: 'include' });\n                if (!r.ok) return { __error: 'HTTP ' + r.status };\n                return await r.json();\n            }`));\n            if (listsAfter && listsAfter.__error) {\n                throw new CommandExecutionError(`Could not verify list removal: ${listsAfter.__error}`);\n            }\n            if (!getListsManagementInstructions(listsAfter)) {\n                throw new CommandExecutionError('Could not verify list removal: unexpected lists payload shape');\n            }\n            const parsedAfter = parseListsManagement(listsAfter, new Set());","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/list-remove-core.js#L212-L248","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (library-side heuristic)\nconst saveButton = [...dialog.querySelectorAll('[role=\"button\"], button')].find(b => /^(Save|Done|保存|完成|儲存)$/i.test((b.innerText||'').trim()));\n// after (caller-side retry)\ntry { await listRemoveUser(page, { listId, username }); }\ncatch (e) {\n  if (String(e.message).includes('Save button not found')) await new Promise(r=>setTimeout(r,2000)), await listRemoveUser(page, { listId, username });\n  else throw e;\n}","handlingStrategy":"retry","validationCode":"// best pre-check: none from caller (DOM-internal), but ensure extension support\nif (typeof page.nativeClick !== 'function') throw new Error('Update the Chrome extension before list removals.');","typeGuard":null,"tryCatchPattern":"try {\n  await listRemoveUser(page, { listId, username });\n} catch (e) {\n  if (String(e.message).includes('Save button not found')) {\n    await sleep(2000); // let dialog finish rendering, retry once\n    return listRemoveUser(page, { listId, username });\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["dom-automation","selector-mismatch","twitter-ui"],"backgroundTag":"dom-selector-not-found","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}