Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Fetch any URL via Chrome CDP and convert the rendered page to clean markdown with YouTube transcript support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/lib/adapters/x/thread-loader.ts
1import type { AdapterContext } from "../types";2import { extractThreadTweetsFromPayloads } from "./thread";3import { collectXJsonPayloads, getRelevantXThreadEntries, prefetchRelevantXThreadBodies } from "./payloads";45interface ClickTextResult {6clicked: boolean;7text?: string;8}910interface ScrollStepResult {11moved: boolean;12atTop: boolean;13atBottom: boolean;14}1516interface ThreadProgress {17tweetCount: number;18firstTweetId?: string;19lastTweetId?: string;20requestCount: number;21tweetDetailCount: number;22}2324interface TopProbeState {25requestCount: number;26tweetDetailCount: number;27scrollHeight: number;28}2930function sleep(ms: number): Promise<void> {31return new Promise((resolve) => setTimeout(resolve, ms));32}3334async function waitForXNetworkSettle(context: AdapterContext, reason: string): Promise<void> {35try {36await context.network.waitForIdle({37idleMs: 650,38timeoutMs: Math.min(context.timeoutMs, 5_000),39});40} catch {41context.log.debug(`Network idle timed out after ${reason}.`);42}43}4445async function captureTopProbeState(context: AdapterContext): Promise<TopProbeState> {46const entries = getRelevantXThreadEntries(context);47const scrollHeight = await context.browser.evaluate<number>(`48(() => {49const scrollRoot = document.scrollingElement ?? document.documentElement ?? document.body;50return scrollRoot.scrollHeight;51})()52`);5354return {55requestCount: entries.length,56tweetDetailCount: entries.filter((entry) => entry.url.includes("TweetDetail")).length,57scrollHeight,58};59}6061async function waitForTopProbe(context: AdapterContext): Promise<boolean> {62const initial = await captureTopProbeState(context);63const deadline = Date.now() + 1_200;6465while (Date.now() < deadline) {66try {67await context.network.waitForIdle({68idleMs: 250,69timeoutMs: 350,70});71} catch {72// Keep polling until the shorter top-probe budget expires.73}7475await prefetchRelevantXThreadBodies(context);76const next = await captureTopProbeState(context);77if (78next.requestCount > initial.requestCount ||79next.tweetDetailCount > initial.tweetDetailCount ||80next.scrollHeight > initial.scrollHeight + 481) {82context.log.debug("Observed additional X thread activity while probing the page top.");83return true;84}8586await sleep(120);87}8889return false;90}9192async function scrollThreadToTop(context: AdapterContext): Promise<void> {93let settledTopChecks = 0;9495while (settledTopChecks < 2) {96const scroll = await context.browser.evaluate<ScrollStepResult>(`97(() => {98const scrollRoot = document.scrollingElement ?? document.documentElement ?? document.body;99const before = window.scrollY;100window.scrollTo({ top: 0, left: 0, behavior: "instant" });101const after = window.scrollY;102return {103moved: after !== before,104atTop: after <= 4,105atBottom: window.innerHeight + after >= scrollRoot.scrollHeight - 4,106};107})()108`);109await sleep(140);110await waitForXNetworkSettle(context, "scrolling X thread to top");111await prefetchRelevantXThreadBodies(context);112113if (scroll.moved) {114settledTopChecks = 0;115continue;116}117118const observedTopActivity = await waitForTopProbe(context);119if (observedTopActivity) {120settledTopChecks = 0;121continue;122}123124settledTopChecks += 1;125}126}127128async function clickVisibleShowReplies(context: AdapterContext): Promise<ClickTextResult> {129return context.browser.evaluate<ClickTextResult>(`130(() => {131const normalize = (value) => value.replace(/\\s+/g, " ").trim();132const matches = [133/^Show replies$/i,134/^Show more replies$/i,135/^Show additional replies$/i,136/^显示回复$/,137/^展开回复$/,138];139const isVisible = (element) => {140if (!(element instanceof HTMLElement)) {141return false;142}143const rect = element.getBoundingClientRect();144const style = window.getComputedStyle(element);145return (146rect.width > 0 &&147rect.height > 0 &&148style.visibility !== "hidden" &&149style.display !== "none"150);151};152153const selectors = [154"a",155"button",156'[role="button"]',157'[role="link"]',158];159160for (const element of document.querySelectorAll(selectors.join(","))) {161if (!isVisible(element)) {162continue;163}164const text = normalize(element.textContent ?? "");165if (!text || !matches.some((pattern) => pattern.test(text))) {166continue;167}168element.scrollIntoView({ block: "center", inline: "nearest" });169if (element instanceof HTMLElement) {170element.click();171return { clicked: true, text };172}173}174175return { clicked: false };176})()177`);178}179180async function expandVisibleShowReplies(context: AdapterContext): Promise<number> {181let clickCount = 0;182183while (clickCount < 8) {184const result = await clickVisibleShowReplies(context).catch<ClickTextResult>(() => ({ clicked: false }));185if (!result.clicked) {186break;187}188189clickCount += 1;190context.log.debug(`Expanded X thread replies via "${result.text ?? "Show replies"}".`);191await sleep(250);192await waitForXNetworkSettle(context, "expanding Show replies");193await prefetchRelevantXThreadBodies(context);194}195196return clickCount;197}198199async function scrollThreadBy(context: AdapterContext, stepPx: number): Promise<ScrollStepResult> {200const result = await context.browser.evaluate<ScrollStepResult>(`201(() => {202const scrollRoot = document.scrollingElement ?? document.documentElement ?? document.body;203const before = window.scrollY;204window.scrollBy({ top: ${stepPx}, left: 0, behavior: "instant" });205const after = window.scrollY;206return {207moved: after !== before,208atTop: after <= 4,209atBottom: window.innerHeight + after >= scrollRoot.scrollHeight - 4,210};211})()212`);213214await sleep(140);215await waitForXNetworkSettle(context, "scrolling X thread");216await prefetchRelevantXThreadBodies(context);217return result;218}219220async function captureThreadProgress(context: AdapterContext, statusId: string): Promise<ThreadProgress> {221const entries = getRelevantXThreadEntries(context);222const payloads = await collectXJsonPayloads(context);223const tweets = extractThreadTweetsFromPayloads(payloads, statusId, context.input.url.toString());224return {225tweetCount: tweets.length,226firstTweetId: tweets[0]?.id,227lastTweetId: tweets[tweets.length - 1]?.id,228requestCount: entries.length,229tweetDetailCount: entries.filter((entry) => entry.url.includes("TweetDetail")).length,230};231}232233export async function loadFullXThread(context: AdapterContext, statusId: string): Promise<void> {234await scrollThreadToTop(context);235236let progress = await captureThreadProgress(context, statusId);237let stagnantRounds = 0;238let roundsWithoutMovement = 0;239let distanceWithoutThreadActivityPx = 0;240241for (let round = 0; ; round += 1) {242const stepPx = round < 12 ? 1_200 : 1_600;243let expandedCount = await expandVisibleShowReplies(context);244const scroll = await scrollThreadBy(context, stepPx);245expandedCount += await expandVisibleShowReplies(context);246const nextProgress = await captureThreadProgress(context, statusId);247const grew =248nextProgress.tweetCount > progress.tweetCount ||249nextProgress.firstTweetId !== progress.firstTweetId ||250nextProgress.lastTweetId !== progress.lastTweetId ||251nextProgress.requestCount > progress.requestCount ||252nextProgress.tweetDetailCount > progress.tweetDetailCount;253254if (grew) {255context.log.debug(256`X thread progress: ${nextProgress.tweetCount} tweets (${nextProgress.firstTweetId ?? "unknown"} -> ${nextProgress.lastTweetId ?? "unknown"}), ${nextProgress.requestCount} requests, ${nextProgress.tweetDetailCount} TweetDetail.`,257);258stagnantRounds = 0;259distanceWithoutThreadActivityPx = 0;260} else if (expandedCount > 0) {261stagnantRounds = 0;262distanceWithoutThreadActivityPx = 0;263} else {264stagnantRounds += 1;265distanceWithoutThreadActivityPx += stepPx;266}267268roundsWithoutMovement = scroll.moved ? 0 : roundsWithoutMovement + 1;269progress = nextProgress;270271if (scroll.atBottom && stagnantRounds >= 6) {272context.log.debug("Stopping X thread scroll after reaching page bottom with no further thread progress.");273break;274}275276if (roundsWithoutMovement >= 2 && stagnantRounds >= 4) {277context.log.debug("Stopping X thread scroll after repeated downward scrolls no longer move the page.");278break;279}280281if (distanceWithoutThreadActivityPx >= 24_000 && stagnantRounds >= 12) {282context.log.debug("Stopping X thread scroll after a long stretch with no thread-related progress.");283break;284}285}286}287