Skip to content

ai#1

Open
e-yang6 wants to merge 1 commit intomainfrom
ai
Open

ai#1
e-yang6 wants to merge 1 commit intomainfrom
ai

Conversation

@e-yang6
Copy link
Copy Markdown
Collaborator

@e-yang6 e-yang6 commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Added AI-powered project refinement using Gemini integration to intelligently refine and organize project groupings
    • Added AI refinement toggle in Settings to control the feature
    • Added visual AI status indicator showing whether refinement is active, waiting, or disabled
    • Added AI refinement badges on projects that have been enhanced
  • Chores

    • Updated extension manifest with required API permissions
    • Added configuration file template for API key setup

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Configuration & Setup
.gitignore, config.example.js, manifest.json, shared/constants.js
Added Gemini API configuration, example config file with API key placeholder, host permission for Google Generative Language API, and new AI constants (model, base URL, rate limit interval, max tabs per request).
AI Core Logic
background/ai-grouping.js
New module implementing refineProjectsWithAI() with Gemini integration, including project serialization, fingerprint-based caching, rate limiting, API error handling, and result application with domain-based grouping and fallback logic.
Integration & Storage
background/clustering.js, background/storage.js
Integrated AI refinement into clustering pipeline as a new preprocessing step; added storage accessors (getAICache, saveAICache, getAISettings, saveAISettings) with default settings management.
UI Components
popup/popup.html, popup/popup.css, popup/popup.js
Added AI Refinement settings section with enable/disable toggle, status indicator, custom toggle and status styling, and integration functions to manage settings, update UI status, display AI badges on refined projects, and trigger background reclustering on setting changes.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Hoppity hop, the tabs align! ✨
Gemini whispers secrets so divine,
Projects refined with AI's clever sight,
Groups dance together, organized just right,
Smart sorting magic, now pure delight! 🐰

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'ai' is extremely vague and generic, providing no meaningful information about the changeset despite substantial additions of AI-powered refinement functionality. Rename the title to clearly describe the main change, such as 'Add AI-powered project refinement using Gemini API' or 'Integrate Gemini AI for intelligent project grouping'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ai

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Await AI status refresh to avoid unhandled rejections.

updateAIStatus() is async; without await, 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.

Comment on lines +121 to +216
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +225 to +328
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +69 to 91
// 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];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +1 to +10
/**
* 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',
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
/**
* 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.

Comment on lines +12 to +14
"host_permissions": [
"https://generativelanguage.googleapis.com/*"
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
"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.

Comment on lines +888 to +939
.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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant