jackwener/OpenCLI · error · CommandExecutionError
Could not open the reply composer.
Error message
Could not open the reply composer.
What it means
openReplyComposer navigates to the dedicated compose route (https://x.com/compose/post?in_reply_to=<id>) and, if the composer textarea never appears, falls back to the tweet page and clicks the Reply button. If both attempts fail, the command throws this CommandExecutionError (with the composer's failure message when available, plus the hint to open the tweet manually). It means no reply text box could be opened at all.
Source
Thrown at clis/twitter/reply.js:283
throw new CommandExecutionError('Use either --image or --image-url, not both.');
}
let localImagePath;
let cleanupDir;
try {
if (kwargs.image) {
localImagePath = resolveImagePath(kwargs.image);
} else if (kwargs['image-url']) {
const downloaded = await downloadRemoteImage(kwargs['image-url']);
localImagePath = downloaded.absPath;
cleanupDir = downloaded.cleanupDir;
}
// Dedicated composer is normally more reliable than the inline
// tweet page reply box, but X occasionally leaves that route on the
// Home timeline behind a loading dialog. openReplyComposer falls
// back to the target tweet's visible Reply action.
const composer = await openReplyComposer(page, kwargs.url);
if (!composer?.ok) {
throw new CommandExecutionError(composer?.message ?? 'Could not open the reply composer.', 'Open the tweet in the browser and check whether the reply box is available.');
}
if (localImagePath) {
await page.wait({ selector: COMPOSER_FILE_INPUT_SELECTOR, timeout: 20 });
await attachComposerImage(page, localImagePath);
}
const result = await submitReply(page, kwargs.text);
if (result.unconfirmed) {
throw new TimeoutError('twitter reply', SUBMIT_TIMEOUT_MS / 1000, `${result.message} Check the tweet before retrying; the reply may already be live.`);
}
if (!result.ok) {
throw new CommandExecutionError(result.message, 'Nothing was posted. Open the tweet in the browser and retry.');
}
return [{
status: 'success',
message: result.message,
text: kwargs.text,
...(result.url ? { url: result.url } : {}),
...(kwargs.image ? { image: kwargs.image } : {}),View on GitHub (pinned to 49907e53dc)
Solutions
- Re-authenticate in the automation browser profile and retry — a login redirect is the most common cause.
- Open the tweet manually in that browser and confirm the Reply button is present and enabled (the tweet may disallow replies).
- Retry later or slow down automation — transient X loading-dialog issues often resolve on retry.
- Update COMPOSER_SELECTOR / [data-testid="reply"] selectors if X changed its DOM.
Example fix
// before
const composer = await openReplyComposer(page, kwargs.url);
if (!composer?.ok) throw new CommandExecutionError(composer?.message ?? 'Could not open the reply composer.', ...);
// after
const composer = await openReplyComposer(page, kwargs.url);
if (!composer?.ok) {
await page.reload({ waitUntil: 'load' }); // one retry after transient dialog
const retry = await openReplyComposer(page, kwargs.url);
if (!retry?.ok) throw new CommandExecutionError(retry?.message ?? 'Could not open the reply composer.', ...);
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-checks before the command: session exists and tweet URL is a valid permalink
if (!isTweetPermalink(tweetUrl)) throw new Error('Invalid tweet url');
// Optionally pre-navigate and confirm [data-testid="reply"] exists
await page.goto(tweetUrl);
const replyBtn = await page.$('[data-testid="reply"]');
if (!replyBtn) throw new Error('Reply not available for this tweet'); Type guard
function canOpenComposer(composer) {
return !!composer && composer.ok === true;
} Try / catch
try {
await cli('twitter', 'reply', { url, text });
} catch (e) {
if (String(e.message).includes('Could not open the reply composer')) {
await sleep(5000);
await cli('twitter', 'reply', { url, text }); // one retry for transient dialogs
} else throw e;
} Prevention
- Keep the session authenticated — login redirects are the top cause.
- Confirm the tweet actually allows replies before automating.
- Retry with backoff during X outages or slow loads.
- Pin/update selectors ([data-testid="reply"], tweetTextarea_0) after X UI changes.
When it happens
Trigger: X leaves /compose/post stuck on the Home timeline behind a loading dialog AND the fallback tweet page has no enabled visible [data-testid="reply"] button (clicked.ok false) — composer?.ok is falsy so the throw fires at clis/twitter/reply.js:283.
Common situations: Logged-out or half-authenticated session so X redirects to login; rate limits / X outages leaving the compose route on a spinner; the tweet is deleted, protected, or blocks replies; heavy client-side UI changes breaking data-testid selectors.
Related errors
- Failed to delete list ${listId}: ${deleteResult?.message ||
- Unexpected 12306 probe: ${JSON.stringify(probe)}
- Waiting for 12306 tk auth cookie
- Could not switch to ${wantModel} model
- Claude composer is not available on the current page.
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/9911603fab281473.
Report an issue: GitHub.