Skip to content

Commit dee4623

Browse files
nearestnaborsclaude
andcommitted
Add pagination (10 items) and debug logging for reply filtering
- Implement pagination in x_get_conversations (limit/offset params) - Default to showing 10 conversations initially - Show "Load more (X remaining)" button when more items available - Add debug logging to diagnose why replied-to tweets aren't filtered - Log user tweets response structure and referenced_tweets field Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b322d72 commit dee4623

3 files changed

Lines changed: 116 additions & 27 deletions

File tree

src/server.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,19 @@ const TOOLS: Tool[] = [
124124
},
125125
{
126126
name: 'x_get_conversations',
127-
description: 'Internal tool to fetch conversation data for the UI.',
127+
description: 'Internal tool to fetch conversation data for the UI with pagination.',
128128
inputSchema: {
129129
type: 'object',
130-
properties: {},
130+
properties: {
131+
limit: {
132+
type: 'number',
133+
description: 'Maximum number of conversations to return (default: 10)',
134+
},
135+
offset: {
136+
type: 'number',
137+
description: 'Number of conversations to skip (for pagination)',
138+
},
139+
},
131140
required: [],
132141
},
133142
// Hidden from model - only callable by UI apps

src/tools/conversations.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,19 @@ interface ConversationItem {
8383
* Fetches unreplied mentions and returns data for the conversation list UI
8484
*/
8585
type FetchResult =
86-
| { success: true; data: { conversations: ConversationItem[]; username: string } }
86+
| { success: true; data: { conversations: ConversationItem[]; username: string; totalCount: number; hasMore: boolean } }
8787
| { success: false; content: unknown };
8888

89+
interface FetchOptions {
90+
limit?: number;
91+
offset?: number;
92+
}
93+
94+
const DEFAULT_LIMIT = 10;
95+
8996
// Internal function that fetches and returns conversation data
90-
async function fetchConversations(): Promise<FetchResult> {
97+
async function fetchConversations(options: FetchOptions = {}): Promise<FetchResult> {
98+
const { limit = DEFAULT_LIMIT, offset = 0 } = options;
9199
// Clean up expired dismissals
92100
const prunedDismissals = pruneExpiredDismissals();
93101
if (prunedDismissals > 0) {
@@ -140,6 +148,8 @@ async function fetchConversations(): Promise<FetchResult> {
140148
data: {
141149
conversations: [],
142150
username,
151+
totalCount: 0,
152+
hasMore: false,
143153
},
144154
};
145155
}
@@ -156,16 +166,30 @@ async function fetchConversations(): Promise<FetchResult> {
156166
const userTweets = userTweetsResponse.data || [];
157167
console.error(`[ASSA] Found ${userTweets.length} user tweets`);
158168

169+
// Debug: log user tweets response structure
170+
debugLog('User tweets response structure', {
171+
hasData: !!userTweetsResponse.data,
172+
tweetCount: userTweets.length,
173+
sampleTweet: userTweets[0] ? {
174+
id: userTweets[0].id,
175+
text: userTweets[0].text?.slice(0, 50),
176+
referenced_tweets: userTweets[0].referenced_tweets,
177+
in_reply_to_user_id: userTweets[0].in_reply_to_user_id,
178+
} : null,
179+
});
180+
159181
// 3. Build a set of tweet IDs that the user has replied to
160182
const repliedToIds = new Set<string>();
161183
for (const tweet of userTweets) {
162184
// Check if this tweet is a reply to another tweet
163185
const replyRef = tweet.referenced_tweets?.find((ref) => ref.type === 'replied_to');
164186
if (replyRef) {
165187
repliedToIds.add(replyRef.id);
188+
debugLog(`Found reply: ${tweet.id} replied to ${replyRef.id}`);
166189
}
167190
}
168191
console.error(`[ASSA] User has replied to ${repliedToIds.size} tweets`);
192+
debugLog('Replied to IDs', Array.from(repliedToIds));
169193

170194
// 4. Build user lookup map for display names
171195
const userMap = new Map<
@@ -270,11 +294,20 @@ async function fetchConversations(): Promise<FetchResult> {
270294
// Update last checked timestamp
271295
updateLastChecked();
272296

297+
// Apply pagination
298+
const totalCount = conversations.length;
299+
const paginatedConversations = conversations.slice(offset, offset + limit);
300+
const hasMore = offset + limit < totalCount;
301+
302+
debugLog('Pagination', { totalCount, offset, limit, returning: paginatedConversations.length, hasMore });
303+
273304
return {
274305
success: true,
275306
data: {
276-
conversations,
307+
conversations: paginatedConversations,
277308
username,
309+
totalCount,
310+
hasMore,
278311
},
279312
};
280313
} catch (error) {
@@ -358,10 +391,13 @@ export async function xConversations(): Promise<unknown> {
358391

359392
/**
360393
* Tool: x_get_conversations (for UI only, hidden from model)
361-
* Returns full conversation data as JSON
394+
* Returns full conversation data as JSON with pagination support
362395
*/
363-
export async function xGetConversations(): Promise<unknown> {
364-
const result = await fetchConversations();
396+
export async function xGetConversations(params?: { limit?: number; offset?: number }): Promise<unknown> {
397+
const result = await fetchConversations({
398+
limit: params?.limit,
399+
offset: params?.offset,
400+
});
365401

366402
if (!result.success) {
367403
// Return JSON error so UI can parse it
@@ -374,6 +410,8 @@ export async function xGetConversations(): Promise<unknown> {
374410
message: 'Please authenticate with x_auth_status first',
375411
conversations: [],
376412
username: '',
413+
totalCount: 0,
414+
hasMore: false,
377415
}),
378416
},
379417
],

ui-apps/conversation-list.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ interface ConversationItem {
3434
interface ConversationsData {
3535
conversations: ConversationItem[];
3636
username: string;
37+
totalCount: number;
38+
hasMore: boolean;
3739
}
3840

3941
// Enable autoResize to let the SDK handle iframe height automatically
@@ -46,6 +48,8 @@ const conversationsListEl = document.getElementById('conversationsList')!;
4648
const loadMoreBtn = document.getElementById('loadMoreBtn') as HTMLButtonElement;
4749

4850
let currentData: ConversationsData | null = null;
51+
let currentOffset = 0;
52+
const PAGE_SIZE = 10;
4953

5054
// Height is now managed automatically by the MCP Apps SDK (autoResize: true)
5155

@@ -70,34 +74,45 @@ function formatRelativeTime(dateStr: string): string {
7074
return date.toLocaleDateString();
7175
}
7276

73-
function updateLoadMoreButton(count: number): void {
74-
if (count === 0) {
77+
function updateLoadMoreButton(data: ConversationsData): void {
78+
const displayedCount = document.querySelectorAll('.conversation-card').length;
79+
80+
if (displayedCount === 0) {
7581
// Show button only when empty - lets user check for new mentions
7682
loadMoreBtn.textContent = 'Check for new mentions';
7783
loadMoreBtn.classList.add('all-clear');
7884
loadMoreBtn.classList.remove('hidden');
85+
} else if (data.hasMore) {
86+
// Show load more button when there are more items
87+
const remaining = data.totalCount - displayedCount;
88+
loadMoreBtn.textContent = `Load more (${remaining} remaining)`;
89+
loadMoreBtn.classList.remove('all-clear', 'hidden');
7990
} else {
80-
// Hide button when conversations are present
91+
// Hide button when all loaded
8192
loadMoreBtn.classList.add('hidden');
8293
}
8394
}
8495

85-
function renderConversations(data: ConversationsData): void {
96+
function renderConversations(data: ConversationsData, append = false): void {
8697
currentData = data;
8798
loadingEl.classList.add('hidden');
8899
contentEl.classList.remove('hidden');
89100

90-
const count = data.conversations.length;
91-
updateLoadMoreButton(count);
101+
// Reset offset if not appending
102+
if (!append) {
103+
currentOffset = data.conversations.length;
104+
}
92105

93-
if (count === 0) {
106+
// Check for empty state using totalCount (0 total means nothing to show)
107+
if (data.totalCount === 0) {
94108
conversationsListEl.innerHTML = `
95109
<div class="empty-state">
96110
<div class="empty-icon">✨</div>
97111
<h3>All caught up!</h3>
98112
<p>No conversations need your attention right now.</p>
99113
</div>
100114
`;
115+
updateLoadMoreButton(data);
101116
return;
102117
}
103118

@@ -111,7 +126,7 @@ function renderConversations(data: ConversationsData): void {
111126
.slice(0, 2);
112127
}
113128

114-
conversationsListEl.innerHTML = data.conversations
129+
const cardsHtml = data.conversations
115130
.map((conv) => {
116131
const relativeTime = formatRelativeTime(conv.created_at);
117132
const initials = getInitials(conv.author_display_name || conv.author_username || '?');
@@ -155,6 +170,15 @@ function renderConversations(data: ConversationsData): void {
155170
})
156171
.join('');
157172

173+
if (append) {
174+
// Append new cards to existing list
175+
conversationsListEl.insertAdjacentHTML('beforeend', cardsHtml);
176+
currentOffset += data.conversations.length;
177+
} else {
178+
// Replace entire list
179+
conversationsListEl.innerHTML = cardsHtml;
180+
}
181+
158182
// Attach event listeners to action buttons
159183
attachEventListeners();
160184

@@ -174,6 +198,9 @@ function renderConversations(data: ConversationsData): void {
174198
}
175199
});
176200
}, 100);
201+
202+
// Update load more button state
203+
updateLoadMoreButton(data);
177204
}
178205

179206
function attachEventListeners(): void {
@@ -200,9 +227,14 @@ function attachEventListeners(): void {
200227
card.style.opacity = '0.5';
201228
setTimeout(() => card.remove(), 300);
202229

203-
// Update button count
230+
// Update button state based on remaining items
204231
const remaining = document.querySelectorAll('.conversation-card').length - 1;
205-
updateLoadMoreButton(remaining);
232+
if (currentData) {
233+
// Update the data to reflect the removal
234+
currentData.totalCount = Math.max(0, currentData.totalCount - 1);
235+
currentData.hasMore = currentOffset < currentData.totalCount;
236+
updateLoadMoreButton(currentData);
237+
}
206238
} catch (error) {
207239
btn.textContent = 'Dismiss';
208240
(btn as HTMLButtonElement).disabled = false;
@@ -324,9 +356,12 @@ function attachEventListeners(): void {
324356
card.style.opacity = '0.5';
325357
setTimeout(() => card.remove(), 500);
326358

327-
// Update button count
328-
const remaining = document.querySelectorAll('.conversation-card').length - 1;
329-
updateLoadMoreButton(remaining);
359+
// Update button state based on remaining items
360+
if (currentData) {
361+
currentData.totalCount = Math.max(0, currentData.totalCount - 1);
362+
currentData.hasMore = currentOffset < currentData.totalCount;
363+
updateLoadMoreButton(currentData);
364+
}
330365
} catch {
331366
// Ignore dismiss errors
332367
}
@@ -367,25 +402,32 @@ app.ontoolresult = async () => {
367402
}
368403
};
369404

370-
// Check for new mentions button (only visible when list is empty)
405+
// Load more button - either refreshes when empty or loads next page
371406
loadMoreBtn.addEventListener('click', async () => {
407+
const isLoadingMore = currentOffset > 0 && currentData?.hasMore;
372408
loadMoreBtn.disabled = true;
373-
loadMoreBtn.textContent = 'Checking...';
409+
loadMoreBtn.textContent = isLoadingMore ? 'Loading...' : 'Checking...';
374410

375411
try {
376412
const result = await app.callServerTool({
377413
name: 'x_get_conversations',
378-
arguments: {},
414+
arguments: isLoadingMore ? { limit: PAGE_SIZE, offset: currentOffset } : {},
379415
});
380416

381417
const textContent = result.content?.find((c: { type: string }) => c.type === 'text');
382418
if (textContent && 'text' in textContent) {
383419
const data = JSON.parse(textContent.text as string) as ConversationsData;
384-
renderConversations(data);
420+
if (isLoadingMore) {
421+
// Append new conversations
422+
renderConversations(data, true);
423+
} else {
424+
// Fresh load
425+
renderConversations(data, false);
426+
}
385427
}
386428
} catch (error) {
387-
console.error('[ASSA] Failed to check for mentions:', error);
388-
loadMoreBtn.textContent = 'Check for new mentions';
429+
console.error('[ASSA] Failed to load conversations:', error);
430+
loadMoreBtn.textContent = currentOffset > 0 ? 'Error - try again' : 'Check for new mentions';
389431
} finally {
390432
loadMoreBtn.disabled = false;
391433
}

0 commit comments

Comments
 (0)