Conversation
📝 WalkthroughWalkthroughThis PR integrates Google's Gemini API into a Chrome extension to intelligently refine project groupings. It adds AI configuration, storage management, UI controls, and a pipeline that processes projects through Gemini to improve organization before archival and filtering logic. Changes
Sequence DiagramsequenceDiagram
participant Popup as Popup UI
participant Storage as Storage
participant Clustering as Clustering
participant AIGroup as AI Grouping
participant Gemini as Gemini API
participant Cache as Local Cache
Popup->>Storage: Load AI settings
Storage-->>Popup: AI enabled state
Popup->>Clustering: Trigger clustering run
Clustering->>AIGroup: Call refineProjectsWithAI()
AIGroup->>Cache: Check cached results (fingerprint)
alt Cache Valid & Matches
Cache-->>AIGroup: Return cached refinements
else Cache Invalid or Data Changed
AIGroup->>AIGroup: Serialize projects
AIGroup->>Gemini: POST with project data & schema
Gemini-->>AIGroup: JSON response with refinements
AIGroup->>Cache: Save results + fingerprint + timestamp
end
AIGroup->>AIGroup: Apply refinements to projects
AIGroup-->>Clustering: Return refined projects
Clustering->>Storage: Save clustered projects
Clustering-->>Popup: Notify update complete
Popup->>Storage: Fetch AI cache timestamp
Popup->>Popup: Update status indicator
Popup->>Popup: Render projects with AI badges
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
popup/popup.js (1)
58-76:⚠️ Potential issue | 🟡 MinorAwait AI status refresh to avoid unhandled rejections.
updateAIStatus()is async; withoutawait, failures bypass the surrounding try/catch.✅ Suggested await
- updateAIStatus(); + await updateAIStatus();- updateAIStatus(); + await updateAIStatus();Also applies to: 636-650
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@popup/popup.js` around lines 58 - 76, In loadData() the call to the async function updateAIStatus() is not awaited, so any rejection escapes the try/catch; update the loadData() implementation to await updateAIStatus() and handle its error within the same try/catch (i.e., change the call in loadData() from updateAIStatus() to await updateAIStatus()); also find the other call site(s) that invoke updateAIStatus() (the one around lines 636-650 in the diff) and ensure those calls are awaited or their Promises are properly handled to prevent unhandled rejections.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In `@background/ai-grouping.js`:
- Around line 225-328: The function applyAIResults currently spreads base (and
reuses its properties) so the old _domains set remains stale after AI changes;
after building the new branches for both the matched-project branch (inside the
if (base) block) and the newly created project (inside the else block) recompute
and assign _domains from the rebuilt branches (e.g., set _domains = new
Set(branches.map(b => b.domain)) or the project's expected _domains
representation) so downstream restoreUserCustomizations overlap matching uses
the updated domain list.
- Around line 121-216: The callGemini function can hang on slow/stalled network
calls; add an AbortController with a timeout (e.g. 15s) and pass
controller.signal into the fetch call, clear the timeout on success, and if the
fetch is aborted throw a clear Error (or propagate) so the clustering pipeline
fails fast; specifically update callGemini to create const controller = new
AbortController(), set a timeout that calls controller.abort(), include signal:
controller.signal in the fetch options, clearTimeout when response is received,
and handle the abort case when throwing errors or parsing the response.
In `@background/clustering.js`:
- Around line 69-91: The algorithm overview comment at the top is stale—insert
the AI refinement step into the header so it matches the actual pipeline:
indicate that refineProjectsWithAI runs before markArchivedProjects and after
whichever step describes initial filtering/dedup (e.g., after withoutDismissed
generation), and update the step numbering and brief description to reference
refineProjectsWithAI, markArchivedProjects, applyBlacklist,
restoreUserCustomizations, sortProjects, capActiveProjects and the final combine
with manualProjects/dismissedProjects; ensure the textual header sequence and
numbering match the implemented order in clustering.js.
In `@config.example.js`:
- Around line 1-10: The template exposes GEMINI_API_KEY in CONFIG on the client;
update the config example to include a clear, prominent warning that keys in
CONFIG (GEMINI_API_KEY) are visible to end users and must be strictly restricted
— instruct users to apply HTTP referrer/extension-ID restrictions, set tight
quotas/rate limits, or prefer issuing short-lived server-side tokens instead of
embedding a long-lived key in the client; add this guidance as a comment next to
the CONFIG block and suggest moving sensitive keys to a backend/service where
possible.
In `@manifest.json`:
- Around line 12-14: The host permission in manifest.json is too broad; update
the "host_permissions" entry (the host_permissions array) to the minimal API
base used by the extension (e.g., replace
"https://generativelanguage.googleapis.com/*" with the exact path prefix the
code calls such as "https://generativelanguage.googleapis.com/v1/*" or the
specific endpoint(s) your code uses) to enforce least privilege while keeping
requests functional.
In `@popup/popup.css`:
- Around line 888-939: The hidden checkbox input inside .toggle-switch is
currently sized 0 so keyboard focus is not visible; update .toggle-switch input
to be positioned over the slider (position: absolute; inset: 0; width: 100%;
height: 100%; opacity: 0;) so it remains focusable, then add a focus-visible
rule targeting .toggle-switch input:focus-visible + .toggle-slider to show a
clear outline/box-shadow (e.g. outline: 2px solid var(--color-focus) and/or
box-shadow) and optionally adjust ::before when focused for better contrast; use
the selectors .toggle-switch input and .toggle-switch input:focus-visible +
.toggle-slider to locate the changes.
In `@popup/popup.html`:
- Around line 111-114: The ai status element (div with class "ai-status" and id
"ai-status") needs an accessible live region so screen readers announce dynamic
updates: add an appropriate ARIA attribute (e.g., aria-live="polite") and/or
role="status" to that container and keep the inner span (class "ai-status-text")
for updated text; ensure updates only modify the text content of ai-status-text
so assistive tech will announce changes correctly.
In `@popup/popup.js`:
- Around line 58-76: In loadData() the call to the async function
updateAIStatus() is not awaited, so any rejection escapes the try/catch; update
the loadData() implementation to await updateAIStatus() and handle its error
within the same try/catch (i.e., change the call in loadData() from
updateAIStatus() to await updateAIStatus()); also find the other call site(s)
that invoke updateAIStatus() (the one around lines 636-650 in the diff) and
ensure those calls are awaited or their Promises are properly handled to prevent
unhandled rejections.
🧹 Nitpick comments (1)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@popup/popup.html`: - Around line 111-114: The ai status element (div with class "ai-status" and id "ai-status") needs an accessible live region so screen readers announce dynamic updates: add an appropriate ARIA attribute (e.g., aria-live="polite") and/or role="status" to that container and keep the inner span (class "ai-status-text") for updated text; ensure updates only modify the text content of ai-status-text so assistive tech will announce changes correctly.popup/popup.html (1)
111-114: Make AI status updates screen-reader friendly.Status text changes dynamically; add a polite live region.
♿ Suggested markup tweak
- <div class="ai-status" id="ai-status"> + <div class="ai-status" id="ai-status" role="status" aria-live="polite"> <span class="ai-status-dot"></span> <span class="ai-status-text">Waiting for first analysis…</span> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@popup/popup.html` around lines 111 - 114, The ai status element (div with class "ai-status" and id "ai-status") needs an accessible live region so screen readers announce dynamic updates: add an appropriate ARIA attribute (e.g., aria-live="polite") and/or role="status" to that container and keep the inner span (class "ai-status-text") for updated text; ensure updates only modify the text content of ai-status-text so assistive tech will announce changes correctly.
| async function callGemini(apiKey, tabData) { | ||
| const url = `${AI.API_BASE}/models/${AI.MODEL}:generateContent?key=${apiKey}`; | ||
|
|
||
| const body = { | ||
| system_instruction: { | ||
| parts: [{ text: SYSTEM_INSTRUCTION }], | ||
| }, | ||
| contents: [ | ||
| { | ||
| role: 'user', | ||
| parts: [ | ||
| { | ||
| text: `Here are the current auto-detected project groups. Please refine them:\n\n${JSON.stringify(tabData, null, 2)}`, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| generationConfig: { | ||
| responseMimeType: 'application/json', | ||
| responseSchema: { | ||
| type: 'OBJECT', | ||
| properties: { | ||
| projects: { | ||
| type: 'ARRAY', | ||
| description: 'Refined list of projects', | ||
| items: { | ||
| type: 'OBJECT', | ||
| properties: { | ||
| originalId: { | ||
| type: 'STRING', | ||
| description: 'The ID of the original project this was derived from (if any)', | ||
| }, | ||
| name: { | ||
| type: 'STRING', | ||
| description: 'Short descriptive project name (2-4 words)', | ||
| }, | ||
| tabs: { | ||
| type: 'ARRAY', | ||
| description: 'URLs that belong in this project', | ||
| items: { | ||
| type: 'OBJECT', | ||
| properties: { | ||
| url: { type: 'STRING' }, | ||
| title: { type: 'STRING' }, | ||
| }, | ||
| required: ['url', 'title'], | ||
| }, | ||
| }, | ||
| reasoning: { | ||
| type: 'STRING', | ||
| description: 'One-sentence explanation of why these tabs are grouped', | ||
| }, | ||
| }, | ||
| required: ['name', 'tabs'], | ||
| }, | ||
| }, | ||
| filtered: { | ||
| type: 'ARRAY', | ||
| description: 'Tabs removed as distractions', | ||
| items: { | ||
| type: 'OBJECT', | ||
| properties: { | ||
| url: { type: 'STRING' }, | ||
| reason: { type: 'STRING' }, | ||
| }, | ||
| required: ['url', 'reason'], | ||
| }, | ||
| }, | ||
| }, | ||
| required: ['projects'], | ||
| }, | ||
| temperature: 0.2, | ||
| maxOutputTokens: 4096, | ||
| }, | ||
| }; | ||
|
|
||
| const response = await fetch(url, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error(`Gemini API ${response.status}: ${errorText.slice(0, 200)}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| const text = data?.candidates?.[0]?.content?.parts?.[0]?.text; | ||
|
|
||
| if (!text) { | ||
| throw new Error('Empty response from Gemini'); | ||
| } | ||
|
|
||
| return JSON.parse(text); | ||
| } |
There was a problem hiding this comment.
Add a timeout/abort for Gemini API calls.
A stalled network call will block the clustering pipeline indefinitely. Add an AbortController with a reasonable timeout to fail fast.
⏱️ Suggested timeout handling
async function callGemini(apiKey, tabData) {
const url = `${AI.API_BASE}/models/${AI.MODEL}:generateContent?key=${apiKey}`;
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 20000);
const body = {
system_instruction: {
parts: [{ text: SYSTEM_INSTRUCTION }],
},
@@
- const response = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
- });
+ let response;
+ try {
+ response = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ signal: controller.signal,
+ });
+ } finally {
+ clearTimeout(timeoutId);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function callGemini(apiKey, tabData) { | |
| const url = `${AI.API_BASE}/models/${AI.MODEL}:generateContent?key=${apiKey}`; | |
| const body = { | |
| system_instruction: { | |
| parts: [{ text: SYSTEM_INSTRUCTION }], | |
| }, | |
| contents: [ | |
| { | |
| role: 'user', | |
| parts: [ | |
| { | |
| text: `Here are the current auto-detected project groups. Please refine them:\n\n${JSON.stringify(tabData, null, 2)}`, | |
| }, | |
| ], | |
| }, | |
| ], | |
| generationConfig: { | |
| responseMimeType: 'application/json', | |
| responseSchema: { | |
| type: 'OBJECT', | |
| properties: { | |
| projects: { | |
| type: 'ARRAY', | |
| description: 'Refined list of projects', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| originalId: { | |
| type: 'STRING', | |
| description: 'The ID of the original project this was derived from (if any)', | |
| }, | |
| name: { | |
| type: 'STRING', | |
| description: 'Short descriptive project name (2-4 words)', | |
| }, | |
| tabs: { | |
| type: 'ARRAY', | |
| description: 'URLs that belong in this project', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| url: { type: 'STRING' }, | |
| title: { type: 'STRING' }, | |
| }, | |
| required: ['url', 'title'], | |
| }, | |
| }, | |
| reasoning: { | |
| type: 'STRING', | |
| description: 'One-sentence explanation of why these tabs are grouped', | |
| }, | |
| }, | |
| required: ['name', 'tabs'], | |
| }, | |
| }, | |
| filtered: { | |
| type: 'ARRAY', | |
| description: 'Tabs removed as distractions', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| url: { type: 'STRING' }, | |
| reason: { type: 'STRING' }, | |
| }, | |
| required: ['url', 'reason'], | |
| }, | |
| }, | |
| }, | |
| required: ['projects'], | |
| }, | |
| temperature: 0.2, | |
| maxOutputTokens: 4096, | |
| }, | |
| }; | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Gemini API ${response.status}: ${errorText.slice(0, 200)}`); | |
| } | |
| const data = await response.json(); | |
| const text = data?.candidates?.[0]?.content?.parts?.[0]?.text; | |
| if (!text) { | |
| throw new Error('Empty response from Gemini'); | |
| } | |
| return JSON.parse(text); | |
| } | |
| async function callGemini(apiKey, tabData) { | |
| const url = `${AI.API_BASE}/models/${AI.MODEL}:generateContent?key=${apiKey}`; | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 20000); | |
| const body = { | |
| system_instruction: { | |
| parts: [{ text: SYSTEM_INSTRUCTION }], | |
| }, | |
| contents: [ | |
| { | |
| role: 'user', | |
| parts: [ | |
| { | |
| text: `Here are the current auto-detected project groups. Please refine them:\n\n${JSON.stringify(tabData, null, 2)}`, | |
| }, | |
| ], | |
| }, | |
| ], | |
| generationConfig: { | |
| responseMimeType: 'application/json', | |
| responseSchema: { | |
| type: 'OBJECT', | |
| properties: { | |
| projects: { | |
| type: 'ARRAY', | |
| description: 'Refined list of projects', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| originalId: { | |
| type: 'STRING', | |
| description: 'The ID of the original project this was derived from (if any)', | |
| }, | |
| name: { | |
| type: 'STRING', | |
| description: 'Short descriptive project name (2-4 words)', | |
| }, | |
| tabs: { | |
| type: 'ARRAY', | |
| description: 'URLs that belong in this project', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| url: { type: 'STRING' }, | |
| title: { type: 'STRING' }, | |
| }, | |
| required: ['url', 'title'], | |
| }, | |
| }, | |
| reasoning: { | |
| type: 'STRING', | |
| description: 'One-sentence explanation of why these tabs are grouped', | |
| }, | |
| }, | |
| required: ['name', 'tabs'], | |
| }, | |
| }, | |
| filtered: { | |
| type: 'ARRAY', | |
| description: 'Tabs removed as distractions', | |
| items: { | |
| type: 'OBJECT', | |
| properties: { | |
| url: { type: 'STRING' }, | |
| reason: { type: 'STRING' }, | |
| }, | |
| required: ['url', 'reason'], | |
| }, | |
| }, | |
| }, | |
| required: ['projects'], | |
| }, | |
| temperature: 0.2, | |
| maxOutputTokens: 4096, | |
| }, | |
| }; | |
| let response; | |
| try { | |
| response = await fetch(url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| signal: controller.signal, | |
| }); | |
| } finally { | |
| clearTimeout(timeoutId); | |
| } | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`Gemini API ${response.status}: ${errorText.slice(0, 200)}`); | |
| } | |
| const data = await response.json(); | |
| const text = data?.candidates?.[0]?.content?.parts?.[0]?.text; | |
| if (!text) { | |
| throw new Error('Empty response from Gemini'); | |
| } | |
| return JSON.parse(text); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/ai-grouping.js` around lines 121 - 216, The callGemini function
can hang on slow/stalled network calls; add an AbortController with a timeout
(e.g. 15s) and pass controller.signal into the fetch call, clear the timeout on
success, and if the fetch is aborted throw a clear Error (or propagate) so the
clustering pipeline fails fast; specifically update callGemini to create const
controller = new AbortController(), set a timeout that calls controller.abort(),
include signal: controller.signal in the fetch options, clearTimeout when
response is received, and handle the abort case when throwing errors or parsing
the response.
| function applyAIResults(originalProjects, refinements) { | ||
| if (!refinements?.projects || refinements.projects.length === 0) { | ||
| return originalProjects; | ||
| } | ||
|
|
||
| const result = []; | ||
|
|
||
| for (const aiProject of refinements.projects) { | ||
| // Try to match to an original project by ID | ||
| let base = null; | ||
| if (aiProject.originalId) { | ||
| base = originalProjects.find((p) => p.id === aiProject.originalId); | ||
| } | ||
|
|
||
| // If no ID match, try to match by tab URL overlap | ||
| if (!base) { | ||
| const aiUrls = new Set(aiProject.tabs.map((t) => t.url)); | ||
| let bestOverlap = 0; | ||
| for (const orig of originalProjects) { | ||
| const origUrls = getAllUrls(orig); | ||
| let overlap = 0; | ||
| for (const u of origUrls) { | ||
| if (aiUrls.has(u)) overlap++; | ||
| } | ||
| if (overlap > bestOverlap) { | ||
| bestOverlap = overlap; | ||
| base = orig; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (base) { | ||
| // Rebuild branches from AI-assigned tabs, grouped by domain | ||
| const domainMap = {}; | ||
| for (const tab of aiProject.tabs) { | ||
| let domain; | ||
| try { | ||
| domain = new URL(tab.url).hostname.replace(/^www\./, ''); | ||
| } catch { | ||
| domain = 'other'; | ||
| } | ||
| if (!domainMap[domain]) domainMap[domain] = []; | ||
| if (!domainMap[domain].some((t) => t.url === tab.url)) { | ||
| domainMap[domain].push({ url: tab.url, title: tab.title }); | ||
| } | ||
| } | ||
|
|
||
| const branches = Object.entries(domainMap).map(([domain, tabs]) => ({ | ||
| id: base.branches?.find((b) => b.domain === domain)?.id || generateShortId(), | ||
| domain, | ||
| tabs, | ||
| })); | ||
|
|
||
| // Sort branches by tab count (most tabs first) | ||
| branches.sort((a, b) => b.tabs.length - a.tabs.length); | ||
|
|
||
| result.push({ | ||
| ...base, | ||
| name: aiProject.name || base.name, | ||
| branches, | ||
| aiRefined: true, | ||
| aiReasoning: aiProject.reasoning || null, | ||
| }); | ||
| } else { | ||
| // AI created a brand new project (merged or restructured) | ||
| const domainMap = {}; | ||
| for (const tab of aiProject.tabs) { | ||
| let domain; | ||
| try { | ||
| domain = new URL(tab.url).hostname.replace(/^www\./, ''); | ||
| } catch { | ||
| domain = 'other'; | ||
| } | ||
| if (!domainMap[domain]) domainMap[domain] = []; | ||
| if (!domainMap[domain].some((t) => t.url === tab.url)) { | ||
| domainMap[domain].push({ url: tab.url, title: tab.title }); | ||
| } | ||
| } | ||
|
|
||
| const branches = Object.entries(domainMap).map(([domain, tabs]) => ({ | ||
| id: generateShortId(), | ||
| domain, | ||
| tabs, | ||
| })); | ||
|
|
||
| branches.sort((a, b) => b.tabs.length - a.tabs.length); | ||
|
|
||
| result.push({ | ||
| id: generateShortId(), | ||
| name: aiProject.name, | ||
| autoDetected: true, | ||
| starred: false, | ||
| archived: false, | ||
| lastAccessed: Date.now(), | ||
| createdAt: Date.now(), | ||
| branches, | ||
| aiRefined: true, | ||
| aiReasoning: aiProject.reasoning || null, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
Recompute _domains after AI rewrites branches.
applyAIResults spreads base, which carries the old _domains set. After AI reassignments, this set can be stale and will skew restoreUserCustomizations overlap matching downstream. Recompute _domains from the new branches for both matched and newly created projects.
🔧 Suggested fix
// Sort branches by tab count (most tabs first)
branches.sort((a, b) => b.tabs.length - a.tabs.length);
+ const domains = new Set(branches.map((b) => b.domain));
result.push({
...base,
name: aiProject.name || base.name,
branches,
+ _domains: domains,
aiRefined: true,
aiReasoning: aiProject.reasoning || null,
});
@@
branches.sort((a, b) => b.tabs.length - a.tabs.length);
+ const domains = new Set(branches.map((b) => b.domain));
result.push({
id: generateShortId(),
name: aiProject.name,
autoDetected: true,
starred: false,
archived: false,
lastAccessed: Date.now(),
createdAt: Date.now(),
branches,
+ _domains: domains,
aiRefined: true,
aiReasoning: aiProject.reasoning || null,
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/ai-grouping.js` around lines 225 - 328, The function
applyAIResults currently spreads base (and reuses its properties) so the old
_domains set remains stale after AI changes; after building the new branches for
both the matched-project branch (inside the if (base) block) and the newly
created project (inside the else block) recompute and assign _domains from the
rebuilt branches (e.g., set _domains = new Set(branches.map(b => b.domain)) or
the project's expected _domains representation) so downstream
restoreUserCustomizations overlap matching uses the updated domain list.
| // Step 6: AI refinement (rename, filter distractions, reassign tabs) | ||
| const aiRefined = await refineProjectsWithAI(withoutDismissed); | ||
|
|
||
| // Step 7: Mark archived projects | ||
| const withArchiveStatus = markArchivedProjects( | ||
| withoutDismissed, | ||
| aiRefined, | ||
| settings.archiveThreshold | ||
| ); | ||
|
|
||
| // Step 7: Apply domain blacklist | ||
| // Step 8: Apply domain blacklist | ||
| const blacklist = await getUserBlacklist(); | ||
| const filtered = applyBlacklist(withArchiveStatus, blacklist); | ||
|
|
||
| // Step 8: Restore user customizations (starred, renamed) | ||
| // Step 9: Restore user customizations (starred, renamed) | ||
| const restored = restoreUserCustomizations(filtered, prevAutoDetected); | ||
|
|
||
| // Step 9: Sort and cap | ||
| // Step 10: Sort and cap | ||
| const sorted = sortProjects(restored); | ||
| const activeCount = sorted.filter((p) => !p.archived).length; | ||
| const capped = capActiveProjects(sorted, settings.maxAutoProjects); | ||
|
|
||
| // Step 10: Combine with manual projects and dismissed (to persist blocking), then save | ||
| // Step 11: Combine with manual projects and dismissed (to persist blocking), then save | ||
| const final = [...manualProjects, ...capped.map(cleanForStorage), ...dismissedProjects]; |
There was a problem hiding this comment.
Update the algorithm overview to include AI refinement.
The header step list (Line 15-22) is now stale because the pipeline includes AI refinement before archiving. Please update the overview to prevent future confusion.
📝 Suggested header update
- * 5. Mark stale projects as archived (not accessed within archive threshold)
- * 6. Apply user's domain blacklist
- * 7. Preserve user-created projects, starred status, and custom names
- * 8. Sort, cap, and save
+ * 5. AI refine projects (rename, filter distractions, reassign tabs)
+ * 6. Mark stale projects as archived (not accessed within archive threshold)
+ * 7. Apply user's domain blacklist
+ * 8. Preserve user-created projects, starred status, and custom names
+ * 9. Sort, cap, and save🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@background/clustering.js` around lines 69 - 91, The algorithm overview
comment at the top is stale—insert the AI refinement step into the header so it
matches the actual pipeline: indicate that refineProjectsWithAI runs before
markArchivedProjects and after whichever step describes initial filtering/dedup
(e.g., after withoutDismissed generation), and update the step numbering and
brief description to reference refineProjectsWithAI, markArchivedProjects,
applyBlacklist, restoreUserCustomizations, sortProjects, capActiveProjects and
the final combine with manualProjects/dismissedProjects; ensure the textual
header sequence and numbering match the implemented order in clustering.js.
| /** | ||
| * Extension configuration template. | ||
| * Copy this file to config.js and fill in your API key. | ||
| * | ||
| * Get a free Gemini API key at: https://aistudio.google.com/apikey | ||
| */ | ||
|
|
||
| export const CONFIG = { | ||
| GEMINI_API_KEY: 'YOUR_GEMINI_API_KEY_HERE', | ||
| }; |
There was a problem hiding this comment.
Client-side API key exposure — add explicit restriction guidance.
This key will be visible to end users. Without strict key restrictions (extension ID/referrer + quota), it can be abused. Recommend adding a clear warning in the template.
🔒 Suggested documentation tweak
/**
* Extension configuration template.
* Copy this file to config.js and fill in your API key.
+ * IMPORTANT: Use a restricted key (limit to your extension ID/referrer) and set quotas.
*
* Get a free Gemini API key at: https://aistudio.google.com/apikey
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Extension configuration template. | |
| * Copy this file to config.js and fill in your API key. | |
| * | |
| * Get a free Gemini API key at: https://aistudio.google.com/apikey | |
| */ | |
| export const CONFIG = { | |
| GEMINI_API_KEY: 'YOUR_GEMINI_API_KEY_HERE', | |
| }; | |
| /** | |
| * Extension configuration template. | |
| * Copy this file to config.js and fill in your API key. | |
| * IMPORTANT: Use a restricted key (limit to your extension ID/referrer) and set quotas. | |
| * | |
| * Get a free Gemini API key at: https://aistudio.google.com/apikey | |
| */ | |
| export const CONFIG = { | |
| GEMINI_API_KEY: 'YOUR_GEMINI_API_KEY_HERE', | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@config.example.js` around lines 1 - 10, The template exposes GEMINI_API_KEY
in CONFIG on the client; update the config example to include a clear, prominent
warning that keys in CONFIG (GEMINI_API_KEY) are visible to end users and must
be strictly restricted — instruct users to apply HTTP referrer/extension-ID
restrictions, set tight quotas/rate limits, or prefer issuing short-lived
server-side tokens instead of embedding a long-lived key in the client; add this
guidance as a comment next to the CONFIG block and suggest moving sensitive keys
to a backend/service where possible.
| "host_permissions": [ | ||
| "https://generativelanguage.googleapis.com/*" | ||
| ], |
There was a problem hiding this comment.
Narrow host permissions to least privilege.
The current host permission is broader than the API base used and increases exposure.
🔐 Suggested scope reduction
"host_permissions": [
- "https://generativelanguage.googleapis.com/*"
+ "https://generativelanguage.googleapis.com/v1beta/*"
],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "host_permissions": [ | |
| "https://generativelanguage.googleapis.com/*" | |
| ], | |
| "host_permissions": [ | |
| "https://generativelanguage.googleapis.com/v1beta/*" | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@manifest.json` around lines 12 - 14, The host permission in manifest.json is
too broad; update the "host_permissions" entry (the host_permissions array) to
the minimal API base used by the extension (e.g., replace
"https://generativelanguage.googleapis.com/*" with the exact path prefix the
code calls such as "https://generativelanguage.googleapis.com/v1/*" or the
specific endpoint(s) your code uses) to enforce least privilege while keeping
requests functional.
| .settings-label--toggle { | ||
| flex-direction: row; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| } | ||
|
|
||
| .toggle-text { | ||
| font-size: 0.775rem; | ||
| font-weight: 600; | ||
| } | ||
|
|
||
| .toggle-switch { | ||
| position: relative; | ||
| width: 36px; | ||
| height: 20px; | ||
| flex-shrink: 0; | ||
| } | ||
|
|
||
| .toggle-switch input { | ||
| opacity: 0; | ||
| width: 0; | ||
| height: 0; | ||
| } | ||
|
|
||
| .toggle-slider { | ||
| position: absolute; | ||
| inset: 0; | ||
| background: var(--color-border); | ||
| border-radius: 10px; | ||
| cursor: pointer; | ||
| transition: background 0.2s ease; | ||
| } | ||
|
|
||
| .toggle-slider::before { | ||
| content: ''; | ||
| position: absolute; | ||
| width: 14px; | ||
| height: 14px; | ||
| left: 3px; | ||
| top: 3px; | ||
| background: var(--color-surface); | ||
| border-radius: 50%; | ||
| transition: transform 0.2s ease; | ||
| } | ||
|
|
||
| .toggle-switch input:checked + .toggle-slider { | ||
| background: var(--color-accent); | ||
| } | ||
|
|
||
| .toggle-switch input:checked + .toggle-slider::before { | ||
| transform: translateX(16px); | ||
| } |
There was a problem hiding this comment.
Add a visible focus state for the AI toggle.
Keyboard users won’t see focus on the hidden checkbox, which is an accessibility blocker.
♿ Proposed focus-visible styling
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(16px);
}
+
+.toggle-switch input:focus-visible + .toggle-slider {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .settings-label--toggle { | |
| flex-direction: row; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .toggle-text { | |
| font-size: 0.775rem; | |
| font-weight: 600; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 36px; | |
| height: 20px; | |
| flex-shrink: 0; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| inset: 0; | |
| background: var(--color-border); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| } | |
| .toggle-slider::before { | |
| content: ''; | |
| position: absolute; | |
| width: 14px; | |
| height: 14px; | |
| left: 3px; | |
| top: 3px; | |
| background: var(--color-surface); | |
| border-radius: 50%; | |
| transition: transform 0.2s ease; | |
| } | |
| .toggle-switch input:checked + .toggle-slider { | |
| background: var(--color-accent); | |
| } | |
| .toggle-switch input:checked + .toggle-slider::before { | |
| transform: translateX(16px); | |
| } | |
| .settings-label--toggle { | |
| flex-direction: row; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .toggle-text { | |
| font-size: 0.775rem; | |
| font-weight: 600; | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 36px; | |
| height: 20px; | |
| flex-shrink: 0; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-slider { | |
| position: absolute; | |
| inset: 0; | |
| background: var(--color-border); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| } | |
| .toggle-slider::before { | |
| content: ''; | |
| position: absolute; | |
| width: 14px; | |
| height: 14px; | |
| left: 3px; | |
| top: 3px; | |
| background: var(--color-surface); | |
| border-radius: 50%; | |
| transition: transform 0.2s ease; | |
| } | |
| .toggle-switch input:checked + .toggle-slider { | |
| background: var(--color-accent); | |
| } | |
| .toggle-switch input:checked + .toggle-slider::before { | |
| transform: translateX(16px); | |
| } | |
| .toggle-switch input:focus-visible + .toggle-slider { | |
| outline: 2px solid var(--color-accent); | |
| outline-offset: 2px; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@popup/popup.css` around lines 888 - 939, The hidden checkbox input inside
.toggle-switch is currently sized 0 so keyboard focus is not visible; update
.toggle-switch input to be positioned over the slider (position: absolute;
inset: 0; width: 100%; height: 100%; opacity: 0;) so it remains focusable, then
add a focus-visible rule targeting .toggle-switch input:focus-visible +
.toggle-slider to show a clear outline/box-shadow (e.g. outline: 2px solid
var(--color-focus) and/or box-shadow) and optionally adjust ::before when
focused for better contrast; use the selectors .toggle-switch input and
.toggle-switch input:focus-visible + .toggle-slider to locate the changes.
Summary by CodeRabbit
New Features
Chores