TestDebuggerTab.tsx32.5 KBView on GitHub /**
* Test Tab Component
* Provides UI for testing sync functionality with controlled parameters
*/
import { TestTube, Play, Loader2, Bell } from 'lucide-react';
import { useTRPC, useTRPCClient } from '@/providers/query-provider';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useState, useEffect, useMemo } from 'react';
import { useNotify } from '@/app/hooks/useNotify';
import { isElectron } from '@/lib/is-electron';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
export function TestDebuggerTab() {
const trpc = useTRPC();
const notify = useNotify();
const trpcClient = useTRPCClient();
const [maxThreads, setMaxThreads] = useState(200);
const [workflowId, setWorkflowId] = useState<string | null>(null);
const [isSyncing, setIsSyncing] = useState(false);
const [threadId, setThreadId] = useState('');
const [isSyncingThread, setIsSyncingThread] = useState(false);
const [threadSyncResult, setThreadSyncResult] = useState<{
success: boolean;
message?: string;
error?: string;
} | null>(null);
const [isSyncingLoadingThread, setIsSyncingLoadingThread] = useState(false);
const [loadingThreadSyncResult, setLoadingThreadSyncResult] = useState<{
success: boolean;
message?: string;
error?: string;
threadId?: string;
} | null>(null);
const [selectedLabels, setSelectedLabels] = useState<Set<string>>(new Set());
const [labelSyncWorkflows, setLabelSyncWorkflows] = useState<Map<string, string>>(new Map());
const [isSyncingLabels, setIsSyncingLabels] = useState(false);
// Search-based sync state
const [searchQuery, setSearchQuery] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [isExecutingSearchResults, setIsExecutingSearchResults] = useState(false);
const [searchResults, setSearchResults] = useState<Array<{ id: string; subject?: string }>>([]);
const [executionProgress, setExecutionProgress] = useState<{
current: number;
total: number;
completed: number;
failed: number;
} | null>(null);
const [executionResults, setExecutionResults] = useState<
Array<{
threadId: string;
success: boolean;
error?: string;
}>
>([]);
// Fetch user labels
const labelsQuery = useQuery(trpc.labels.list.queryOptions(void 0));
// Poll for sync progress
const { data: progressData } = useQuery({
queryKey: ['mail', 'getSyncProgress', { workflowId }],
queryFn: () => trpcClient.mail.getSyncProgress.query({ workflowId: workflowId! }),
enabled: !!workflowId,
refetchInterval: 2000,
});
const syncMutation = useMutation({
mutationFn: (input: {
maxThreads: number;
folder?: string;
bypassAgentExecutionEnabledCheck?: boolean;
}) => trpcClient.mail.triggerSyncThreads.mutate(input),
onSuccess: (data) => {
// The route returns workflowId: null when the sync workflow binding is unavailable.
setWorkflowId(data.workflowId);
setIsSyncing(!!data.workflowId);
},
onError: (_error: Error) => {
console.error('Sync failed:', _error);
setIsSyncing(false);
},
});
const syncLabelsMutation = useMutation({
mutationFn: (input: {
maxThreads: number;
folder: string;
bypassAgentExecutionEnabledCheck?: boolean;
}) => trpcClient.mail.triggerSyncThreads.mutate(input),
onSuccess: (data, variables) => {
if (!data.workflowId) return;
setLabelSyncWorkflows((prev) => {
const newMap = new Map(prev);
newMap.set(variables.folder, data.workflowId);
return newMap;
});
},
onError: (error: Error, variables) => {
console.error(`Sync failed for label ${variables.folder}:`, error);
},
});
const syncThreadMutation = useMutation({
mutationFn: (input: {
threadId: string;
bypassAgentExecutionEnabledCheck?: boolean;
reexecute?: boolean;
}) => trpcClient.mail.syncThread.mutate(input),
onSuccess: (data) => {
setThreadSyncResult(data);
setIsSyncingThread(false);
},
onError: (error: Error) => {
console.error('Thread sync failed:', error);
setThreadSyncResult({
success: false,
error: error.message || 'Unknown error occurred',
});
setIsSyncingThread(false);
},
});
const handleSyncSentFolder = async () => {
setIsSyncing(true);
try {
const result = await syncMutation.mutateAsync({
maxThreads,
folder: 'SENT',
bypassAgentExecutionEnabledCheck: true, // Always bypass enabled check when called from debugger
});
setWorkflowId(result.workflowId);
} catch (_error) {
console.error('Failed to start sync:', _error);
setIsSyncing(false);
}
};
const handleSyncSelectedLabels = async () => {
if (selectedLabels.size === 0) {
return;
}
setIsSyncingLabels(true);
const workflows = new Map<string, string>();
// Sync each selected label
const syncPromises = Array.from(selectedLabels).map(async (labelId) => {
try {
const result = await syncLabelsMutation.mutateAsync({
maxThreads,
folder: labelId,
bypassAgentExecutionEnabledCheck: true,
});
if (result.workflowId) workflows.set(labelId, result.workflowId);
} catch (error) {
console.error(`Failed to sync label ${labelId}:`, error);
}
});
await Promise.allSettled(syncPromises);
setLabelSyncWorkflows((prev) => {
const newMap = new Map(prev);
workflows.forEach((workflowId, labelId) => {
newMap.set(labelId, workflowId);
});
return newMap;
});
setIsSyncingLabels(false);
};
const handleLabelToggle = (labelId: string, checked: boolean) => {
setSelectedLabels((prev) => {
const newSet = new Set(prev);
if (checked) {
newSet.add(labelId);
} else {
newSet.delete(labelId);
}
return newSet;
});
};
// Filter out system labels and only show user labels
const userLabels = useMemo(() => {
if (!labelsQuery.data) return [];
return labelsQuery.data.filter((label) => label.type === 'user');
}, [labelsQuery.data]);
const handleSyncThread = async () => {
if (!threadId.trim()) {
setThreadSyncResult({
success: false,
error: 'Please enter a thread ID',
});
return;
}
setIsSyncingThread(true);
setThreadSyncResult(null);
try {
await syncThreadMutation.mutateAsync({
threadId: threadId.trim(),
bypassAgentExecutionEnabledCheck: true, // Always bypass enabled check when called from debugger
});
} catch (_error) {
console.error('Failed to sync thread:', _error);
setIsSyncingThread(false);
}
};
const handleSyncLoadingThread = async () => {
setIsSyncingLoadingThread(true);
setLoadingThreadSyncResult(null);
try {
// Query threads from SENT folder (same as the sync sent section)
const result = await trpcClient.mail.listThreads.query({
q: 'label:SENT',
maxResults: 100,
cursor: undefined,
});
const threads = result.threads || [];
// Find first thread with loadingConversation or failedCreatingConversation
const loadingThread = threads.find((thread) => {
const conversationId = (thread.$raw as { conversationId?: string | null } | undefined)
?.conversationId;
return (
conversationId === 'loadingConversation' ||
conversationId === 'failedCreatingConversation'
);
});
if (!loadingThread) {
setLoadingThreadSyncResult({
success: false,
error: 'No threads found with loadingConversation or failedCreatingConversation',
});
setIsSyncingLoadingThread(false);
return;
}
// Sync the found thread with reexecute flag to bypass new message check
const syncResult = await syncThreadMutation.mutateAsync({
threadId: loadingThread.id,
bypassAgentExecutionEnabledCheck: true,
reexecute: true, // Force re-execution even if message already has execution
});
setLoadingThreadSyncResult({
success: syncResult.success,
message: syncResult.message || `Successfully synced thread ${loadingThread.id}`,
error: syncResult.error,
threadId: loadingThread.id,
});
} catch (error) {
console.error('Failed to sync loading thread:', error);
setLoadingThreadSyncResult({
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
});
} finally {
setIsSyncingLoadingThread(false);
}
};
const handleSearchThreads = async () => {
if (!searchQuery.trim()) {
return;
}
setIsSearching(true);
setSearchResults([]);
setExecutionResults([]);
setExecutionProgress(null);
try {
// Call listThreads with search query
const result = await trpcClient.mail.listThreads.query({
q: searchQuery.trim(),
maxResults: 100, // Limit to 100 threads max
cursor: undefined,
});
const threads = result.threads || [];
setSearchResults(
threads.map((t) => ({
id: t.id,
subject: (t.$raw as { latestSubject?: string } | undefined)?.latestSubject || undefined,
})),
);
} catch (error) {
console.error('Search failed:', error);
} finally {
setIsSearching(false);
}
};
const handleExecuteSearchResults = async () => {
if (searchResults.length === 0) {
return;
}
setIsExecutingSearchResults(true);
setExecutionProgress({
current: 0,
total: searchResults.length,
completed: 0,
failed: 0,
});
setExecutionResults([]);
// Execute threads sequentially
for (let i = 0; i < searchResults.length; i++) {
const thread = searchResults[i];
setExecutionProgress({
current: i + 1,
total: searchResults.length,
completed: executionResults.filter((r) => r.success).length,
failed: executionResults.filter((r) => !r.success).length,
});
try {
const result = await syncThreadMutation.mutateAsync({
threadId: thread.id,
bypassAgentExecutionEnabledCheck: true,
});
setExecutionResults((prev) => [
...prev,
{
threadId: thread.id,
success: result.success,
error: result.error,
},
]);
setExecutionProgress((prev) =>
prev
? {
...prev,
current: i + 1,
completed: prev.completed + (result.success ? 1 : 0),
failed: prev.failed + (result.success ? 0 : 1),
}
: null,
);
// Small delay between executions to avoid overwhelming the system
if (i < searchResults.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setExecutionResults((prev) => [
...prev,
{
threadId: thread.id,
success: false,
error: errorMessage,
},
]);
setExecutionProgress((prev) =>
prev
? {
...prev,
current: i + 1,
failed: prev.failed + 1,
}
: null,
);
}
}
setIsExecutingSearchResults(false);
setExecutionProgress((prev) =>
prev
? {
...prev,
current: prev.total,
}
: null,
);
};
// Check if sync is complete
useEffect(() => {
if (progressData?.status === 'completed' || progressData?.status === 'failed') {
setIsSyncing(false);
}
}, [progressData?.status]);
const output = progressData?.output as
| {
totalSynced?: number;
totalPagesProcessed?: number;
totalThreads?: number;
totalSuccessfulSyncs?: number;
totalFailedSyncs?: number;
pageWorkflowResults?: Array<{
pageNumber: number;
workflowId: string;
status: string;
synced: number;
}>;
}
| null
| undefined;
return (
<div className="flex h-full flex-col gap-4 overflow-y-auto p-4">
<div className="flex items-center gap-2">
<TestTube className="h-4 w-4" />
<h3 className="text-sm font-semibold">Sync Testing</h3>
</div>
{isElectron() && (
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4" />
<Label className="text-xs font-medium">Electron Notifications</Label>
</div>
<Button
size="sm"
variant="outline"
className="w-full text-xs"
onClick={() => notify('Cedar Mail', 'Draft ready — Test notification from debugger')}
>
Test notification
</Button>
</div>
)}
<div className="space-y-4">
{/* Search-based Sync Section */}
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
<TestTube className="h-4 w-4" />
<Label className="text-xs font-medium">Search & Sync Threads</Label>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Input
type="text"
placeholder="Enter search query (e.g., 'from:<email>' or 'subject:meeting')"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
disabled={isSearching || isExecutingSearchResults}
className="h-8 flex-1 text-xs"
onKeyDown={(e) => {
if (e.key === 'Enter' && !isSearching && searchQuery.trim()) {
handleSearchThreads();
}
}}
/>
<Button
onClick={handleSearchThreads}
disabled={isSearching || isExecutingSearchResults || !searchQuery.trim()}
size="sm"
variant="outline"
>
{isSearching ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Searching...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Search
</>
)}
</Button>
</div>
{searchResults.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600 dark:text-gray-400">
Found {searchResults.length} thread{searchResults.length !== 1 ? 's' : ''}
</span>
<Button
onClick={handleExecuteSearchResults}
disabled={isExecutingSearchResults}
size="sm"
variant="default"
className="h-7"
>
{isExecutingSearchResults ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Executing...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Execute All Sequentially
</>
)}
</Button>
</div>
{executionProgress && (
<div className="space-y-1 rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Progress:</span>
<span className="font-medium">
{executionProgress.current} / {executionProgress.total}
</span>
</div>
<div className="flex justify-between">
<span className="text-green-600 dark:text-green-400">Completed:</span>
<span className="font-medium">{executionProgress.completed}</span>
</div>
<div className="flex justify-between">
<span className="text-red-600 dark:text-red-400">Failed:</span>
<span className="font-medium">{executionProgress.failed}</span>
</div>
<div className="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{
width: `${(executionProgress.current / executionProgress.total) * 100}%`,
}}
/>
</div>
</div>
)}
{executionResults.length > 0 && (
<div className="max-h-48 space-y-1 overflow-y-auto rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
<div className="font-medium">Execution Results:</div>
{executionResults.map((result, idx) => {
const thread = searchResults.find((t) => t.id === result.threadId);
return (
<div
key=[redacted]
className={`flex items-start justify-between rounded px-2 py-1 ${
result.success
? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
: 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
}`}
>
<div className="flex-1 truncate">
<div className="font-medium">
{idx + 1}. {thread?.subject || result.threadId}
</div>
{result.error && (
<div className="mt-0.5 text-xs opacity-75">{result.error}</div>
)}
</div>
<div className="ml-2 flex-shrink-0">{result.success ? '✓' : '✗'}</div>
</div>
);
})}
</div>
)}
{!isExecutingSearchResults && executionResults.length === 0 && (
<div className="max-h-32 space-y-1 overflow-y-auto rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
{searchResults.slice(0, 10).map((thread, idx) => (
<div key=[redacted] className="truncate">
{idx + 1}. {thread.subject || thread.id}
</div>
))}
{searchResults.length > 10 && (
<div className="text-gray-500">... and {searchResults.length - 10} more</div>
)}
</div>
)}
</div>
)}
</div>
</div>
{/* Sync Loading Thread Section */}
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<Label className="text-xs font-medium">
Sync Thread with Loading/Failed Conversation
</Label>
<div className="space-y-2">
<Button
onClick={handleSyncLoadingThread}
disabled={isSyncingLoadingThread}
size="sm"
variant="outline"
className="w-full"
>
{isSyncingLoadingThread ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Finding and syncing...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Sync First Loading/Failed Thread
</>
)}
</Button>
{loadingThreadSyncResult && (
<div
className={`mt-2 rounded-md p-2 text-xs ${
loadingThreadSyncResult.success
? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
: 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
}`}
>
{loadingThreadSyncResult.success ? (
<div>
<div className="font-medium">{loadingThreadSyncResult.message}</div>
{loadingThreadSyncResult.threadId && (
<div className="mt-1 text-xs opacity-75">
Thread ID: {loadingThreadSyncResult.threadId}
</div>
)}
</div>
) : (
<div>
<div className="font-medium">Error:</div>
<div>{loadingThreadSyncResult.error}</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Sync Single Thread Section */}
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<Label htmlFor="threadId" className="text-xs font-medium">
Sync Specific Thread
</Label>
<div className="flex gap-2">
<Input
id="threadId"
type="text"
placeholder="Paste thread ID here"
value={threadId}
onChange={(e) => setThreadId(e.target.value)}
disabled={isSyncingThread}
className="h-8 flex-1 text-xs"
/>
<Button
onClick={handleSyncThread}
disabled={isSyncingThread || !threadId.trim()}
size="sm"
variant="outline"
>
{isSyncingThread ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Syncing...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Sync Thread
</>
)}
</Button>
</div>
{threadSyncResult && (
<div
className={`mt-2 rounded-md p-2 text-xs ${
threadSyncResult.success
? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
: 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
}`}
>
{threadSyncResult.success ? (
<div className="font-medium">{threadSyncResult.message}</div>
) : (
<div>
<div className="font-medium">Error:</div>
<div>{threadSyncResult.error}</div>
</div>
)}
</div>
)}
</div>
{/* Sync Labels Section */}
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<Label className="text-xs font-medium">Sync Labels</Label>
{labelsQuery.isLoading ? (
<div className="flex items-center gap-2 text-xs text-gray-500">
<Loader2 className="h-3 w-3 animate-spin" />
Loading labels...
</div>
) : labelsQuery.error ? (
<div className="text-xs text-red-600 dark:text-red-400">
Failed to load labels: {labelsQuery.error.message}
</div>
) : userLabels.length === 0 ? (
<div className="text-xs text-gray-500">No user labels available</div>
) : (
<>
<div className="max-h-48 space-y-2 overflow-y-auto">
{userLabels.map((label) => (
<div
key=[redacted]
className="flex items-center gap-2 rounded-md p-2 hover:bg-gray-50 dark:hover:bg-gray-800"
>
<Checkbox
id={`label-${label.id}`}
checked={selectedLabels.has(label.id)}
onCheckedChange={(checked) => handleLabelToggle(label.id, checked === true)}
disabled={isSyncingLabels}
/>
<Label
htmlFor={`label-${label.id}`}
className="flex-1 cursor-pointer text-xs font-normal"
>
{label.name}
</Label>
{label.color && (
<div
className="h-4 w-4 rounded"
style={{ backgroundColor: label.color.backgroundColor }}
/>
)}
</div>
))}
</div>
<div className="flex items-center justify-between gap-2 pt-2">
<div className="text-xs text-gray-500">
{selectedLabels.size} label{selectedLabels.size !== 1 ? 's' : ''} selected
</div>
<Button
onClick={handleSyncSelectedLabels}
disabled={isSyncingLabels || selectedLabels.size === 0}
size="sm"
variant="outline"
className="h-8"
>
{isSyncingLabels ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Syncing...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Sync Selected
</>
)}
</Button>
</div>
{labelSyncWorkflows.size > 0 && (
<div className="mt-2 space-y-1 rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
<div className="font-medium">Active Label Syncs:</div>
{Array.from(labelSyncWorkflows.entries()).map(([labelId, workflowId]) => {
const label = userLabels.find((l) => l.id === labelId);
return (
<div key=[redacted] className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">
{label?.name || labelId}:
</span>
<span className="break-all font-mono text-xs">{workflowId}</span>
</div>
);
})}
</div>
)}
</>
)}
</div>
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
<div className="mb-4 space-y-2">
<Label htmlFor="maxThreads" className="text-xs">
Max Threads (1-1000)
</Label>
<Input
id="maxThreads"
type="number"
min={1}
max={1000}
value={maxThreads}
onChange={(e) => setMaxThreads(parseInt(e.target.value) || 200)}
disabled={isSyncing}
className="h-8 text-xs"
/>
</div>
<Button onClick={handleSyncSentFolder} disabled={isSyncing} size="sm" className="w-full">
{isSyncing ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Syncing...
</>
) : (
<>
<Play className="mr-2 h-3 w-3" />
Start Sync (SENT folder)
</>
)}
</Button>
</div>
{workflowId && (
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="text-xs font-medium">Workflow ID:</div>
<div className="break-all text-xs text-gray-600 dark:text-gray-400">{workflowId}</div>
</div>
)}
{progressData && (
<div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Status:</span>
<span
className={`text-xs font-semibold ${
progressData.status === 'completed'
? 'text-green-600 dark:text-green-400'
: progressData.status === 'failed'
? 'text-red-600 dark:text-red-400'
: 'text-blue-600 dark:text-blue-400'
}`}
>
{progressData.status.toUpperCase()}
</span>
</div>
{output && (
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Total Synced:</span>
<span className="font-medium">{output.totalSynced ?? 0}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Pages Processed:</span>
<span className="font-medium">{output.totalPagesProcessed ?? 0}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Total Threads:</span>
<span className="font-medium">{output.totalThreads ?? 0}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Successful:</span>
<span className="font-medium text-green-600 dark:text-green-400">
{output.totalSuccessfulSyncs ?? 0}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Failed:</span>
<span className="font-medium text-red-600 dark:text-red-400">
{output.totalFailedSyncs ?? 0}
</span>
</div>
{output.pageWorkflowResults && output.pageWorkflowResults.length > 0 && (
<div className="mt-2 space-y-1">
<div className="text-xs font-medium">Page Results:</div>
<div className="max-h-32 space-y-1 overflow-y-auto">
{output.pageWorkflowResults.map((page) => (
<div
key=[redacted]
className="flex justify-between rounded bg-gray-50 px-2 py-1 text-xs dark:bg-gray-800"
>
<span>Page {page.pageNumber}</span>
<span className="font-medium">{page.synced} synced</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{progressData.error && (
<div className="mt-2 rounded bg-red-50 p-2 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
Error: {progressData.error}
</div>
)}
</div>
)}
<div className="mt-4 space-y-2 rounded-md bg-blue-50 p-3 text-xs dark:bg-blue-900/20">
<div className="font-semibold text-blue-900 dark:text-blue-300">Testing Guide:</div>
<ul className="list-inside list-disc space-y-1 text-blue-800 dark:text-blue-200">
<li>Paste a thread ID and click "Sync Thread" to sync a specific thread</li>
<li>
Select one or more labels and click "Sync Selected" to sync threads with
those labels
</li>
<li>Set max threads to limit how many threads to sync in bulk</li>
<li>Click "Start Sync" to begin syncing the SENT folder</li>
<li>Progress updates automatically every 2 seconds for bulk syncs</li>
<li>Monitor the status and results in real-time</li>
</ul>
</div>
</div>
</div>
);
}