---
name: x
description: X.com/Twitter browser automation operating notes.
type: instruction
builtin: true
readonly: true
autoLoad: x
autoCleanupHosts: x.com,twitter.com
---

# X.com Built-in Skill

This skill is packaged with dipclaw and is read-only. Do not save, overwrite,
delete, install over, or auto-improve it.

Use this skill when the task mentions X.com, Twitter, tweets/posts, reposts,
quote posts, replies, follows, likes, X search, or an X profile/status URL.

## Stable Selectors

- Search input: `[data-testid="SearchBox_Search_Input"]`.
- Result/timeline cells: `[data-testid="cellInnerDiv"]`.
- Tweet/article container: `article[data-testid="tweet"]`.
- Tweet text: `[data-testid="tweetText"]`.
- Compose/reply editor: `[data-testid="tweetTextarea_0"]`.
- Submit buttons: `[data-testid="tweetButton"]` in dialogs,
  `[data-testid="tweetButtonInline"]` inline.
- Common action buttons inside an article: `[data-testid="like"]`,
  `[data-testid="reply"]`, `[data-testid="retweet"]`,
  `[data-testid="unretweet"]`.
- Explore link: `[data-testid="AppTabBar_Explore_Link"]`.
- Latest tab on search pages: `a[role="tab"][href*="f=live"]`.

Prefer these selectors over snapshot refs because X React rerenders often make
refs stale after focus, dialog open/close, tab changes, and hover cards.

## Search Flow

- Default to the human search flow: open Explore with
  `[data-testid="AppTabBar_Explore_Link"]`, type in
  `[data-testid="SearchBox_Search_Input"]`, submit with Enter, then choose
  Latest when needed. Do not use direct search URLs as the first path.
- For Explore, search input, and Latest tab, use selectors (not refs) and an
  `expect` block. Do not include DOM fallback options by default; if a mouse
  click fails, take a fresh snapshot, use a better selector, scroll the target
  into a stable viewport, or use another visible UI path.
- Explore example: click `[data-testid="AppTabBar_Explore_Link"]` with
  `expect: { selectorVisible: "[data-testid=\"SearchBox_Search_Input\"]" }`.
- Search submit example: type into `[data-testid="SearchBox_Search_Input"]`
  with `clear: true`, `submit: true`, and
  `expect: { urlContains: "/search", selectorVisible: "[data-testid=\"cellInnerDiv\"]" }`.
- Do not loop on repeated mouse event-miss failures. After one failed mouse
  click on Explore, a tab, or a menu item, do not retry with
  `dispatch: "dom"` or `fallbackDispatch: "dom"` unless the operator has
  explicitly enabled unsafe evaluate fallbacks. Prefer a fresh snapshot and a
  stable selector for the intended state.
- For search submission, verify both the URL/query state and visible results:
  `expect: { urlContains: "/search", selectorVisible: "[data-testid=\"cellInnerDiv\"]" }`.
- If X refuses to focus the search input through `type` after the input is
  visible, first close/hide blocking overlays per this skill, then retry
  `type`. Direct search URL navigation is a last resort only when the
  search-box path repeatedly fails and the user task allows a non-human path:
  `https://x.com/search?q=<encoded>&src=typed_query&f=live`.

## Blocking UI

- X can leave a "Save post?" draft dialog mounted after a failed reply or
  quote attempt. It appears as `[data-testid="confirmationSheetDialog"]` and
  traps both pointer events and keyboard focus before the compose editor can
  be cleared. Discard the stale draft with
  `[data-testid="confirmationSheetConfirm"]`; do not choose Save, because Save
  preserves the draft and the same dialog reappears on the next compose.
- When closing a compose or reply editor that contains text, X may show that
  same Save post? confirmation instead of closing immediately. If
  `[data-testid="confirmationSheetDialog"]` appears, click
  `[data-testid="confirmationSheetConfirm"]` to discard it. Before opening a
  new composer, verify that the old editor is gone with
  `expect: { selectorHidden: "[data-testid=\"tweetTextarea_0\"]" }` or a
  standalone `verify`; do not rely on broad selectors such as `[role="dialog"]`
  alone because hidden/stale dialogs can coexist with the visible composer.
- X can leave Grok drawers or floating overlays mounted. A common symptom is
  repeated `click event did not reach target` with `event landed on <(none)>`
  even though hit testing finds the target.
- First try to close the overlay with a visible close button. If that is not
  available, do not hide it with `evaluate` by default. Take a fresh snapshot,
  scroll away/back, use visible close controls, or report that the blocking UI
  prevented the action unless unsafe evaluate fallbacks are explicitly enabled.

## Automatic Cleanup

These legacy recipes use page `evaluate` and DOM click/style mutation. The
runtime skips them by default because `browser.allowEvaluateFallbacks` defaults
to false. They run only when an operator explicitly enables unsafe evaluate
fallbacks for compatibility.

```js dipclaw:auto-cleanup:before-edit
(() => {
  const sheet = document.querySelector('[data-testid="confirmationSheetDialog"]');
  if (!sheet) return { handled: false, reason: "no-x-draft-dialog" };

  const discard =
    sheet.querySelector('[data-testid="confirmationSheetConfirm"]') ||
    Array.from(sheet.querySelectorAll("button")).find((button) =>
      /discard|don'?t save/i.test(button.textContent || "")
    );

  if (!discard) {
    return { handled: false, reason: "x-draft-dialog-without-discard" };
  }

  discard.click();
  return { handled: true, reason: "discarded-x-draft-dialog" };
})()
```

```js dipclaw:auto-cleanup:after-click-event-miss
(() => {
  const handled = [];

  const sheet = document.querySelector('[data-testid="confirmationSheetDialog"]');
  if (sheet) {
    const discard =
      sheet.querySelector('[data-testid="confirmationSheetConfirm"]') ||
      Array.from(sheet.querySelectorAll("button")).find((button) =>
        /discard|don'?t save/i.test(button.textContent || "")
      );
    if (discard) {
      discard.click();
      handled.push("discarded-x-draft-dialog");
    }
  }

  for (const sel of ['[data-testid="GrokDrawer"]', '[data-testid="chat-drawer-main"]']) {
    for (const drawer of document.querySelectorAll(sel)) {
      const close =
        drawer.querySelector('[aria-label="Close"]') ||
        drawer.querySelector('[data-testid="app-bar-close"]') ||
        Array.from(drawer.querySelectorAll('button,[role="button"]')).find((button) =>
          /close/i.test(button.getAttribute("aria-label") || button.textContent || "")
        );
      if (close) {
        close.click();
        handled.push(`closed-${sel}`);
      } else {
        const el = drawer;
        el.style.display = "none";
        el.style.visibility = "hidden";
        el.style.pointerEvents = "none";
        handled.push(`hid-${sel}`);
      }
    }
  }

  return handled.length > 0
    ? { handled: true, reason: handled.join(",") }
    : { handled: false, reason: "no-x-click-blocker" };
})()
```

## Timeline And Candidate Extraction

- Prefer `act` kind `extractTweetCards` on X timelines/search/profile pages
  when available. It returns author, text, timestamp/status URL, and action
  button selectors without writing ad-hoc DOM scraping code.
- Do not use free-form `evaluate` for extraction by default. Prefer
  `extractTweetCards`, `extractVisibleText`, `queryByText`, and
  `extractLinks`.
- Skip pinned old posts and pure retweets when the task asks for fresh original
  posts.

## Hover Cards And Profiles

- For author qualification, hover the avatar or user link, wait briefly, then
  snapshot the hover card or use the structured extraction helpers.

- **Selector strategy for hover (critical for stability):** use
  **content-anchored** selectors that include the post's status ID, e.g.
  `[data-testid="cellInnerDiv"]:has(a[href$="/<user>/status/<statusId>"]) a[href="/<user>"]`.
  Never use **positional** selectors like `:nth-of-type(N)`, `:nth-child(N)`,
  or bare `article[data-testid="tweet"]` without a `:has()` qualifier — X.com
  virtualizes its timeline, so positions reshuffle every time the feed reflows
  or you scroll, and a positional ref captured two snapshots ago will silently
  point at the wrong post (or nothing).

- **If hover errors with `locator.scrollIntoViewIfNeeded: Timeout 5000ms exceeded`,**
  the target element has been virtualized off-screen or unmounted. Do NOT
  retry the same selector — identical args + identical error message will
  trip the loop detector's `no_progress` pattern (5 consecutive same-sig
  same-result calls → forced stop). Recovery, in order:
  1. `act` kind `scroll` with wheel input to bring more tweets into view.
  2. Re-snapshot to get fresh refs from the now-loaded DOM.
  3. If the same target still fails after one re-snapshot, abandon that
     candidate and move on — looping on one author burns iterations and
     trips loop detection. The task usually has more candidates available.

- Hover cards may also fail to render even when the hover call itself
  returned ok. If snapshot after a successful hover shows no popup card,
  retry the hover once with a longer wait (e.g., 3-5s). Falling back to
  navigating to the profile URL works only when the task allows non-feed
  navigation; some tasks explicitly forbid entering profile pages — in that
  case skip the candidate instead of looping.

## Like, Follow, Reply, Quote

- For like/follow/reply/retweet buttons, normal mouse dispatch is preferred.
  If the button is visible but mouse dispatch returns event-miss, do not use
  DOM dispatch by default. Take a fresh snapshot, use a more stable selector,
  scroll/reposition the target, and verify the final state.
- For quote flow, click the article retweet button, then choose the `Quote`
  menu item from a fresh snapshot or a stable role ref such as `menuitem|Quote`.
- Compose/reply editors are rich editors. Use `type` with
  `method: "paste"` and the editor selector.
- If `type` cannot focus the editor because of X event-miss, clear or hide the
  blocking overlay first and retry. Do not write raw JavaScript insertion
  code; use the browser tool's `type` action so the runtime handles focus,
  insertion, verification, and recovery consistently.

## Verification

- Every post, reply, quote, like, follow, search, or publish action needs an
  `expect` or a standalone `verify` before reporting success.
- For submit buttons, useful success signals include: compose/dialog hidden,
  visible new item, changed action state/count, `unretweet` present after
  quote/repost, or a permalink/status URL.
- X quote/reply dialogs may keep the editor mounted after a successful post.
  In that case a cleared editor is a valid success signal:
  `expect: { inputValue: { selector: "[data-testid=\"tweetTextarea_0\"]", equals: "" } }`.
- If a button click succeeds but verification fails, do not declare success.
  Take a fresh snapshot or run `verify` with the expected final state.

## DOM Dispatch And Evaluate

- Do not write free-form `evaluate` scripts that click controls, submit search,
  or insert post text. Use the browser tool's `click` / `type` actions.
- Tool-level `dispatch: "dom"`, `fallbackDispatch: "dom"`, and act
  `evaluate` are disabled by default via `browser.allowEvaluateFallbacks=false`.
  Treat them as unsafe compatibility tools, not normal recovery paths.
- Prefer high-level helpers (`extractTweetCards`, `extractVisibleText`,
  `queryByText`, `findClickableNearText`, `extractLinks`) and visible browser
  actions.

## Dedup checks for quote / reply tasks

Use the store tool (see its description for the full API) for any "have I
already quoted / replied to / interacted with this post?" check. Never use
memory.read for this.

Collection naming on X:
- "x-quoted"   — statusIds you've already quoted
- "x-replied"  — statusIds you've already replied to
- "x-liked"    — statusIds you've already liked
- "x-followed" — usernames you've already followed

Pattern before posting:

  store.has("x-quoted", "<statusId>")

  → "yes": skip this candidate, pick another
  → "no":  proceed with the quote/reply

Pattern after a successful action (compose dialog closed AND tweetButton
disappeared — see "Verifying actions actually worked" rules):

  store.put("x-quoted", "<statusId>", undefined,
            { url: "https://x.com/<author>/status/<statusId>",
              author: "@<handle>",
              ts: "<ISO timestamp>",
              source: "DipcoinLabs" | "fallback-trending" | ... })

Use the statusId (the digit string in the URL after /status/) as the id, not
the full URL. statusIds are unique per X post and stable.
