Size
14.0 KB
Version
1.0.1
Created
Jan 28, 2026
Updated
6 days ago
1// ==UserScript==
2// @name Synced Tabs Analyzer & Categorizer
3// @description Counts and categorizes synced tabs from Chrome history
4// @version 1.0.1
5// @match chrome://history/*
6// @icon https://robomonkey.io/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Synced Tabs Analyzer & Categorizer loaded');
12
13 // Debounce function to prevent excessive calls
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Function to navigate to synced tabs if not already there
27 function navigateToSyncedTabs() {
28 const currentUrl = window.location.href;
29 if (!currentUrl.includes('syncedTabs')) {
30 console.log('Navigating to synced tabs page...');
31 window.location.href = 'chrome://history/syncedTabs';
32 }
33 }
34
35 // Function to extract tab information from the page
36 function extractTabsInfo() {
37 console.log('Extracting tabs information...');
38
39 // Try multiple selectors to find synced tabs
40 const tabElements = document.querySelectorAll('history-synced-device-card a[href], history-synced-device-card .website-title, history-synced-device-card .item-title, [id*="synced"] a, [class*="synced"] a, [class*="tab"] a[href]');
41
42 console.log(`Found ${tabElements.length} potential tab elements`);
43
44 const tabs = [];
45 const seenUrls = new Set();
46
47 tabElements.forEach((element, index) => {
48 let title = '';
49 let url = '';
50
51 // Extract title
52 if (element.textContent) {
53 title = element.textContent.trim();
54 }
55
56 // Extract URL
57 if (element.href) {
58 url = element.href;
59 } else if (element.getAttribute('href')) {
60 url = element.getAttribute('href');
61 }
62
63 // Only add if we have valid data and haven't seen this URL
64 if ((title || url) && !seenUrls.has(url)) {
65 tabs.push({
66 title: title || 'Untitled',
67 url: url || 'No URL',
68 index: index
69 });
70 if (url) seenUrls.add(url);
71 }
72 });
73
74 console.log(`Extracted ${tabs.length} unique tabs`);
75 return tabs;
76 }
77
78 // Function to categorize tabs using AI
79 async function categorizeTabs(tabs) {
80 console.log('Categorizing tabs with AI...');
81
82 if (tabs.length === 0) {
83 console.log('No tabs to categorize');
84 return { categories: {}, totalTabs: 0 };
85 }
86
87 try {
88 // Prepare tab data for AI
89 const tabsData = tabs.map(tab => ({
90 title: tab.title,
91 url: tab.url
92 }));
93
94 const prompt = `Analyze these browser tabs and categorize them into meaningful categories.
95
96Tabs data:
97${JSON.stringify(tabsData, null, 2)}
98
99Please categorize each tab into appropriate categories like: Social Media, Shopping, News, Development, Entertainment, Productivity, Education, etc.`;
100
101 const categorization = await RM.aiCall(prompt, {
102 type: "json_schema",
103 json_schema: {
104 name: "tab_categorization",
105 schema: {
106 type: "object",
107 properties: {
108 categories: {
109 type: "object",
110 additionalProperties: {
111 type: "object",
112 properties: {
113 count: { type: "number" },
114 tabs: {
115 type: "array",
116 items: {
117 type: "object",
118 properties: {
119 title: { type: "string" },
120 url: { type: "string" }
121 },
122 required: ["title", "url"]
123 }
124 }
125 },
126 required: ["count", "tabs"]
127 }
128 },
129 totalTabs: { type: "number" }
130 },
131 required: ["categories", "totalTabs"]
132 }
133 }
134 });
135
136 console.log('Categorization complete:', categorization);
137 return categorization;
138
139 } catch (error) {
140 console.error('Error categorizing tabs:', error);
141 return { categories: {}, totalTabs: tabs.length, error: error.message };
142 }
143 }
144
145 // Function to display results
146 function displayResults(tabs, categorization) {
147 console.log('Displaying results...');
148
149 // Remove existing results panel if any
150 const existingPanel = document.getElementById('tabs-analyzer-panel');
151 if (existingPanel) {
152 existingPanel.remove();
153 }
154
155 // Create results panel
156 const panel = document.createElement('div');
157 panel.id = 'tabs-analyzer-panel';
158 panel.style.cssText = `
159 position: fixed;
160 top: 20px;
161 right: 20px;
162 width: 400px;
163 max-height: 80vh;
164 overflow-y: auto;
165 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
166 color: white;
167 padding: 20px;
168 border-radius: 12px;
169 box-shadow: 0 10px 40px rgba(0,0,0,0.3);
170 z-index: 999999;
171 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
172 `;
173
174 let html = `
175 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
176 <h2 style="margin: 0; font-size: 20px; font-weight: 600;">📊 Synced Tabs Analysis</h2>
177 <button id="close-analyzer-panel" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 18px;">×</button>
178 </div>
179 <div style="background: rgba(255,255,255,0.15); padding: 15px; border-radius: 8px; margin-bottom: 15px;">
180 <div style="font-size: 32px; font-weight: bold; margin-bottom: 5px;">${categorization.totalTabs || tabs.length}</div>
181 <div style="font-size: 14px; opacity: 0.9;">Total Synced Tabs</div>
182 </div>
183 `;
184
185 if (categorization.error) {
186 html += `
187 <div style="background: rgba(255,255,255,0.15); padding: 15px; border-radius: 8px; margin-bottom: 10px;">
188 <div style="font-size: 14px; color: #ffcccc;">⚠️ Error: ${categorization.error}</div>
189 </div>
190 `;
191 }
192
193 if (categorization.categories && Object.keys(categorization.categories).length > 0) {
194 html += '<h3 style="font-size: 16px; margin: 15px 0 10px 0; font-weight: 600;">Categories:</h3>';
195
196 // Sort categories by count
197 const sortedCategories = Object.entries(categorization.categories)
198 .sort((a, b) => b[1].count - a[1].count);
199
200 sortedCategories.forEach(([categoryName, categoryData]) => {
201 const percentage = ((categoryData.count / categorization.totalTabs) * 100).toFixed(1);
202 html += `
203 <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px; margin-bottom: 8px;">
204 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
205 <div style="font-weight: 600; font-size: 14px;">${categoryName}</div>
206 <div style="background: rgba(255,255,255,0.25); padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">
207 ${categoryData.count} (${percentage}%)
208 </div>
209 </div>
210 <div style="background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px; overflow: hidden;">
211 <div style="background: white; height: 100%; width: ${percentage}%; border-radius: 3px;"></div>
212 </div>
213 <details style="margin-top: 8px;">
214 <summary style="cursor: pointer; font-size: 12px; opacity: 0.8;">Show tabs</summary>
215 <div style="margin-top: 8px; font-size: 12px; opacity: 0.9;">
216 ${categoryData.tabs.map(tab => `
217 <div style="padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
218 <div style="font-weight: 500;">${tab.title}</div>
219 <div style="opacity: 0.7; font-size: 11px; word-break: break-all;">${tab.url}</div>
220 </div>
221 `).join('')}
222 </div>
223 </details>
224 </div>
225 `;
226 });
227 }
228
229 panel.innerHTML = html;
230 document.body.appendChild(panel);
231
232 // Add close button functionality
233 const closeBtn = document.getElementById('close-analyzer-panel');
234 if (closeBtn) {
235 closeBtn.addEventListener('click', () => {
236 panel.remove();
237 });
238 }
239
240 console.log('Results displayed successfully');
241 }
242
243 // Function to show loading indicator
244 function showLoading() {
245 const existingLoader = document.getElementById('tabs-analyzer-loader');
246 if (existingLoader) return;
247
248 const loader = document.createElement('div');
249 loader.id = 'tabs-analyzer-loader';
250 loader.style.cssText = `
251 position: fixed;
252 top: 20px;
253 right: 20px;
254 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
255 color: white;
256 padding: 15px 20px;
257 border-radius: 8px;
258 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
259 z-index: 999999;
260 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
261 font-size: 14px;
262 `;
263 loader.innerHTML = '🔄 Analyzing synced tabs...';
264 document.body.appendChild(loader);
265 }
266
267 function hideLoading() {
268 const loader = document.getElementById('tabs-analyzer-loader');
269 if (loader) {
270 loader.remove();
271 }
272 }
273
274 // Main analysis function
275 async function analyzeSyncedTabs() {
276 console.log('Starting synced tabs analysis...');
277
278 showLoading();
279
280 // Wait a bit for the page to fully load
281 await new Promise(resolve => setTimeout(resolve, 2000));
282
283 const tabs = extractTabsInfo();
284
285 if (tabs.length === 0) {
286 console.log('No tabs found. The page might not be fully loaded or the structure has changed.');
287 hideLoading();
288
289 // Show a message to the user
290 const panel = document.createElement('div');
291 panel.style.cssText = `
292 position: fixed;
293 top: 20px;
294 right: 20px;
295 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
296 color: white;
297 padding: 20px;
298 border-radius: 12px;
299 box-shadow: 0 10px 40px rgba(0,0,0,0.3);
300 z-index: 999999;
301 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
302 `;
303 panel.innerHTML = `
304 <h3 style="margin: 0 0 10px 0;">⚠️ No Synced Tabs Found</h3>
305 <p style="margin: 0; font-size: 14px;">Make sure you're on the Synced Tabs page and have synced devices.</p>
306 `;
307 document.body.appendChild(panel);
308 setTimeout(() => panel.remove(), 5000);
309 return;
310 }
311
312 const categorization = await categorizeTabs(tabs);
313
314 hideLoading();
315 displayResults(tabs, categorization);
316 }
317
318 // Initialize the extension
319 async function init() {
320 console.log('Initializing Synced Tabs Analyzer...');
321
322 // Check if we're on the history page
323 if (!window.location.href.includes('chrome://history')) {
324 console.log('Not on history page, skipping...');
325 return;
326 }
327
328 // Navigate to synced tabs if needed
329 if (!window.location.href.includes('syncedTabs')) {
330 navigateToSyncedTabs();
331 return;
332 }
333
334 // Wait for the page to be ready
335 if (document.readyState === 'loading') {
336 document.addEventListener('DOMContentLoaded', () => {
337 setTimeout(analyzeSyncedTabs, 1000);
338 });
339 } else {
340 setTimeout(analyzeSyncedTabs, 1000);
341 }
342
343 // Re-analyze when the page changes (debounced)
344 const debouncedAnalyze = debounce(analyzeSyncedTabs, 2000);
345 const observer = new MutationObserver(debouncedAnalyze);
346 observer.observe(document.body, {
347 childList: true,
348 subtree: true
349 });
350 }
351
352 // Start the extension
353 init();
354})();