From cf78fce64633a9fefed3220afdc81af2d8fe6208 Mon Sep 17 00:00:00 2001 From: George Zhang Date: Tue, 6 Jan 2026 17:19:49 +0800 Subject: [PATCH] feat: improve text output with article preview, quote tweets, and media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance the text output format to show more useful information: - Article tweets: Show 📰 indicator with title + preview text in feeds, full body when reading single tweets - Quote tweets: Display quoted content with ┌─ QT @user formatting - Media: Show 🖼️/🎬/🔄 indicators with URLs for photos/videos/GIFs Also refactors single tweet read to use shared printTweets function for consistent formatting. --- src/cli/shared.ts | 40 ++++++++++++++++++++++++++++++++- src/commands/read.ts | 6 +---- src/lib/twitter-client-types.ts | 5 +++++ src/lib/twitter-client-utils.ts | 25 +++++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/cli/shared.ts b/src/cli/shared.ts index 806de34..475aefd 100644 --- a/src/cli/shared.ts +++ b/src/cli/shared.ts @@ -320,7 +320,45 @@ export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEn } for (const tweet of tweets) { console.log(`\n@${tweet.author.username} (${tweet.author.name}):`); - console.log(tweet.text); + + // Display tweet text, with article indicator if present + if (tweet.article) { + // For articles: show preview if text is just the title (home timeline), + // otherwise show full text (single tweet detail) + const isFullBody = tweet.text.length > tweet.article.title.length + 50; + if (isFullBody) { + console.log(`📰 ${tweet.text}`); + } else { + console.log(`📰 ${tweet.article.title}`); + if (tweet.article.previewText) { + console.log(` ${tweet.article.previewText}`); + } + } + } else { + console.log(tweet.text); + } + + // Display media attachments + if (tweet.media && tweet.media.length > 0) { + for (const m of tweet.media) { + const label = m.type === 'video' ? '🎬' : m.type === 'animated_gif' ? '🔄' : '🖼️'; + console.log(`${label} ${m.url}`); + } + } + + // Display quoted tweet + if (tweet.quotedTweet) { + console.log(`┌─ QT @${tweet.quotedTweet.author.username}:`); + const qtText = tweet.quotedTweet.article ? `📰 ${tweet.quotedTweet.article.title}` : tweet.quotedTweet.text; + // Indent and truncate quoted tweet text + const maxLen = 280; + const truncated = qtText.length > maxLen ? `${qtText.slice(0, maxLen)}...` : qtText; + for (const line of truncated.split('\n').slice(0, 4)) { + console.log(`│ ${line}`); + } + console.log(`└─ https://x.com/${tweet.quotedTweet.author.username}/status/${tweet.quotedTweet.id}`); + } + if (tweet.createdAt) { console.log(`${l('date')}${tweet.createdAt}`); } diff --git a/src/commands/read.ts b/src/commands/read.ts index 3a1056d..5a5ebb6 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -36,11 +36,7 @@ export function registerReadCommands(program: Command, ctx: CliContext): void { if (cmdOpts.json || cmdOpts.jsonFull) { console.log(JSON.stringify(result.tweet, null, 2)); } else { - console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`); - console.log(result.tweet.text); - if (result.tweet.createdAt) { - console.log(`\n${ctx.l('date')}${result.tweet.createdAt}`); - } + ctx.printTweets([result.tweet], { showSeparator: false }); console.log(formatStatsLine(result.tweet, ctx.getOutput())); } } else { diff --git a/src/lib/twitter-client-types.ts b/src/lib/twitter-client-types.ts index b0118a7..dca792c 100644 --- a/src/lib/twitter-client-types.ts +++ b/src/lib/twitter-client-types.ts @@ -236,6 +236,11 @@ export interface TweetData { quotedTweet?: TweetData; // Media attachments (photos, videos, GIFs) media?: TweetMedia[]; + // Article metadata (for Twitter Articles / long-form posts) + article?: { + title: string; + previewText?: string; + }; // Raw GraphQL tweet result (only when includeRaw is enabled) _raw?: GraphqlTweetResult; } diff --git a/src/lib/twitter-client-utils.ts b/src/lib/twitter-client-utils.ts index 7730396..6ca6275 100644 --- a/src/lib/twitter-client-utils.ts +++ b/src/lib/twitter-client-utils.ts @@ -153,6 +153,29 @@ export function extractTweetText(result: GraphqlTweetResult | undefined): string return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text); } +export function extractArticleMetadata( + result: GraphqlTweetResult | undefined, +): { title: string; previewText?: string } | undefined { + const article = result?.article; + if (!article) { + return undefined; + } + + const articleResult = article.article_results?.result ?? article; + const title = firstText(articleResult.title, article.title); + if (!title) { + return undefined; + } + + // preview_text is available in home timeline responses + const previewText = firstText( + (articleResult as { preview_text?: string }).preview_text, + (article as { preview_text?: string }).preview_text, + ); + + return { title, previewText }; +} + export function extractMedia(result: GraphqlTweetResult | undefined): TweetMedia[] | undefined { // Prefer extended_entities (has video info), fall back to entities const rawMedia = result?.legacy?.extended_entities?.media ?? result?.legacy?.entities?.media; @@ -261,6 +284,7 @@ export function mapTweetResult( } const media = extractMedia(result); + const article = extractArticleMetadata(result); const tweetData: TweetData = { id: result.rest_id, @@ -278,6 +302,7 @@ export function mapTweetResult( authorId: userId, quotedTweet, media, + article, }; if (includeRaw) {