Skip to content

fix(extension): local content extraction for Chrome Web Store approval#76

Merged
rohitg00 merged 2 commits intomainfrom
fix/extension-local-content-extraction
Feb 28, 2026
Merged

fix(extension): local content extraction for Chrome Web Store approval#76
rohitg00 merged 2 commits intomainfrom
fix/extension-local-content-extraction

Conversation

@rohitg00
Copy link
Owner

@rohitg00 rohitg00 commented Feb 23, 2026

Summary

  • Chrome Web Store rejected the extension because Save page as Skill called https://skillkit.sh/api/save-skill — unreachable during review
  • Rewired page saving to work entirely locally using chrome.scripting.executeScript to extract content directly in the browser
  • No server dependency remains — all processing (content extraction, tag detection, SKILL.md generation) happens client-side

Changes

  • background.ts: Removed API_URL/callSaveApi(), added extractPageContent() (scripting API), detectTags() (weighted keyword scoring across URL/headings/body), buildPageSkill() (local SKILL.md generation), error logging in catch blocks
  • manifest.json: Added scripting permission, bumped to v1.20.0
  • types.ts: Added tabId to SAVE_PAGE payload for script injection targeting
  • popup.ts: Passes tabId to background, guards against invalid tab ID
  • STORE_LISTING.md: Updated description, features, permissions justification, and data disclosure to reflect fully local processing
  • package.json: Bumped to v1.20.0

Test plan

  • pnpm --filter @skillkit/extension build passes
  • pnpm --filter @skillkit/extension typecheck passes
  • Load unpacked from packages/extension/dist/ in Chrome
  • Click extension on a normal page -> extracts content, downloads SKILL.md
  • Right-click -> "Save page as Skill" -> same result
  • Select text -> right-click -> "Save selection as Skill" -> works (unchanged)
  • Try on chrome://extensions -> shows "Cannot save" error
  • Verify SKILL.md has valid frontmatter, tags, and content

Open with Devin

Summary by CodeRabbit

  • New Features

    • Skills are now generated locally in your browser with no data sent to external servers
    • Improved automatic tag detection based on page content analysis
    • Enhanced description generation from page metadata and content
    • Save is disabled for restricted pages with clearer error/status feedback; selected content saves preserve source info
  • Chores

    • Version bumped to 1.20.0
  • Documentation

    • Store listing updated to emphasize local processing and privacy improvements

Chrome Web Store rejected the extension because the core "Save page as
Skill" feature called https://skillkit.sh/api/save-skill which is
unreachable during review. This rewires page saving to work entirely
locally using chrome.scripting.executeScript to extract page content
(title, meta description, headings, body text) directly in the browser.

- Remove API_URL and callSaveApi() server dependency
- Add extractPageContent() via chrome.scripting.executeScript
- Add detectTags() with weighted keyword scoring (URL/headings/body)
- Add buildPageSkill() to generate SKILL.md with frontmatter locally
- Add scripting permission to manifest
- Pass tabId from popup to background for script injection targeting
- Block chrome-extension:// and chromewebstore.google.com URLs
- Add console.error logging in catch blocks for debuggability
- Guard currentTabId validity in popup click handler
- Update store listing to reflect fully local processing
- Bump version to 1.20.0
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
skillkit Ignored Ignored Preview Feb 28, 2026 6:51am
skillkit-docs Ignored Ignored Preview Feb 28, 2026 6:51am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

The PR moves page/selection skill extraction and markdown generation into the extension (client-side), adding content extraction, tag detection, description derivation, updated messaging, and download/notification flows; it also bumps versions, adds the scripting permission, updates types, and revises store copy and constants for restricted URLs.

Changes

Cohort / File(s) Summary
Version & Manifest
packages/extension/package.json, packages/extension/src/manifest.json
Bumped package and manifest to 1.20.0 and added "scripting" permission.
Background / Extraction Logic
packages/extension/src/background.ts
Added client-side extraction (executeScript), page/selection builders (extractPageContent, buildPageSkill, buildSelectionSkill), tag detection (TECH_KEYWORDS, detectTags), description derivation, YAML escaping/slugify, download and notification helpers, and expanded error handling—replacing server-side API usage.
Popup UI & State
packages/extension/src/popup.ts
Introduced currentTabId, integrated isRestricted checks, added tabId to SAVE_PAGE payload, hardened Save flow and status handling, and standardized string usage.
Types
packages/extension/src/types.ts
Extended ExtensionMessage SAVE_PAGE payload to include tabId: number (discriminated union updated).
Constants
packages/extension/src/constants.ts
New RESTRICTED_PREFIXES and isRestricted(url) helper added to centralize restricted-URL logic.
Store Listing
packages/extension/store-assets/STORE_LISTING.md
Updated marketing/privacy text to state extraction and processing occur locally in the browser (no server-side processing).

Sequence Diagram

sequenceDiagram
    participant User as User / Popup
    participant BG as Background Script
    participant CS as Content Script
    participant Page as Page DOM
    participant DL as Chrome Downloads API

    User->>BG: click Save or send SAVE_PAGE
    BG->>CS: executeScript extractPageContent(tabId)
    CS->>Page: read title, meta, headings, body
    Page-->>CS: page data
    CS-->>BG: PageContent
    BG->>BG: detectTags, makeDescription, buildPageSkill/Selection
    BG->>BG: yamlEscape & slugify metadata
    BG->>DL: download skill markdown
    DL-->>User: file download starts
    BG->>User: notifyTab with badge/result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibbled the page in the browser light,

Collected headings, tags, and a title bright.
No server trips, just a local delight,
Skill saved and downloaded in one quick bite! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: implementing local content extraction to meet Chrome Web Store approval requirements, which is reflected throughout all changed files (background.ts, manifest.json, popup.ts, types.ts, constants.ts, and STORE_LISTING.md).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/extension-local-content-extraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link

@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: 2

🧹 Nitpick comments (3)
packages/extension/src/background.ts (3)

337-346: Download callback ignores chrome.runtime.lastError.

If the download fails (e.g., invalid filename, disabled downloads), the error is silently swallowed. Checking lastError in the callback would let you surface the failure.

Proposed fix
     () => {
+      if (chrome.runtime.lastError) {
+        console.error("[SkillKit] Download failed:", chrome.runtime.lastError.message);
+      }
       URL.revokeObjectURL(blobUrl);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/background.ts` around lines 337 - 346, The download
callback for chrome.downloads.download currently ignores
chrome.runtime.lastError so failures are swallowed; update the anonymous
callback passed to chrome.downloads.download (the one that calls
URL.revokeObjectURL(blobUrl)) to check chrome.runtime.lastError and
surface/log/notify the error (including context like the filename
`skillkit-skills/${name}/SKILL.md`), then always revoke the blob URL
(URL.revokeObjectURL(blobUrl)) in all cases to avoid leaks.

313-323: yamlEscape doesn't quote YAML special bare values.

Strings like "true", "false", "null", "yes", "no", or pure numeric strings pass the character-class test but are interpreted as non-string types by YAML parsers. This mainly matters if yamlEscape is used for fields other than URLs/descriptions (e.g., the name field as noted above), but hardening it now would prevent future surprises.

Example enhancement
 function yamlEscape(value: string): string {
   const singleLine = value.replace(/\r?\n/g, " ").trim();
+  const YAML_SPECIAL = /^(true|false|yes|no|on|off|null|~|\d[\d.eE+-]*)$/i;
   if (
     /[:#{}[\],&*?|>!%@`]/.test(singleLine) ||
     singleLine.startsWith("'") ||
-    singleLine.startsWith('"')
+    singleLine.startsWith('"') ||
+    YAML_SPECIAL.test(singleLine)
   ) {
     return `"${singleLine.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
   }
   return singleLine;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/background.ts` around lines 313 - 323, The yamlEscape
function currently only quotes values based on special characters but misses
bare scalars like true/false/null/yes/no and numeric-only strings which YAML can
coerce to non-string types; update yamlEscape to also detect these cases (e.g.,
match /^(-?\d+(\.\d+)?([eE][+-]?\d+)?)$/ for numbers and a case-insensitive set
for boolean/null tokens like "true","false","yes","no","null","~") and
force-quote them the same way you do when special characters are present; keep
existing newline-to-space trimming and backslash/quote escaping logic and apply
it when quoting inside yamlEscape.

192-198: Substring matching on short keywords can produce false-positive tags.

String.includes(kw) with 2-letter keywords like "ai", "ml", "js", "ts", "ci", "cd" matches inside unrelated words (e.g., "container""ai", "html""ml", "adjust""js"). The binary scoring limits the blast radius, but tech-unrelated pages could still receive spurious tags.

Using word-boundary matching would be more precise:

Suggested improvement
-      if (urlLower.includes(kw)) score += 3;
-      if (headingsLower.includes(kw)) score += 2;
-      if (bodyLower.includes(kw)) score += 1;
+      const re = new RegExp(`\\b${kw}\\b`);
+      if (re.test(urlLower)) score += 3;
+      if (re.test(headingsLower)) score += 2;
+      if (re.test(bodyLower)) score += 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/background.ts` around lines 192 - 198, The current
substring matching in the tag-scoring loop over TECH_KEYWORDS (inside the for
(const [tag, keywords]...) block) produces false positives for short keywords;
change the checks that use urlLower.includes(kw), headingsLower.includes(kw),
and bodyLower.includes(kw) to use word-boundary, case-insensitive regex tests
instead: build/escape a RegExp for each kw using \b... \b and test against
urlLower, headingsLower, and bodyLower so only whole-word matches increment the
score (keep the same +3/+2/+1 scoring logic and ensure kw escaping to avoid
regex metacharacters).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/extension/src/background.ts`:
- Around line 233-237: The YAML frontmatter construction at skillMd currently
emits an unquoted name value which can be parsed as null/boolean for slugs like
"null" or "yes"; update the template so the name field is produced as a properly
quoted/escaped string (e.g., use yamlEscape(skillName) or wrap ${skillName} in
quotes) the same way description uses yamlEscape, ensuring the name line in
skillMd uses yamlEscape(skillName) (or equivalent) to prevent YAML
misinterpretation.

In `@packages/extension/src/popup.ts`:
- Around line 29-33: The popup's restricted-url check is missing the Chrome Web
Store prefix and is out-of-sync with background.ts; extract the prefixes into a
shared constant (e.g., export RESTRICTED_PREFIXES and an isRestricted(url)
helper in a new/shared module like constants.ts or types.ts), include
"https://chromewebstore.google.com" in that list, then import and use the shared
isRestricted in popup.ts (replace the inline currentUrl check) and in
background.ts so both files use the same logic.

---

Nitpick comments:
In `@packages/extension/src/background.ts`:
- Around line 337-346: The download callback for chrome.downloads.download
currently ignores chrome.runtime.lastError so failures are swallowed; update the
anonymous callback passed to chrome.downloads.download (the one that calls
URL.revokeObjectURL(blobUrl)) to check chrome.runtime.lastError and
surface/log/notify the error (including context like the filename
`skillkit-skills/${name}/SKILL.md`), then always revoke the blob URL
(URL.revokeObjectURL(blobUrl)) in all cases to avoid leaks.
- Around line 313-323: The yamlEscape function currently only quotes values
based on special characters but misses bare scalars like true/false/null/yes/no
and numeric-only strings which YAML can coerce to non-string types; update
yamlEscape to also detect these cases (e.g., match
/^(-?\d+(\.\d+)?([eE][+-]?\d+)?)$/ for numbers and a case-insensitive set for
boolean/null tokens like "true","false","yes","no","null","~") and force-quote
them the same way you do when special characters are present; keep existing
newline-to-space trimming and backslash/quote escaping logic and apply it when
quoting inside yamlEscape.
- Around line 192-198: The current substring matching in the tag-scoring loop
over TECH_KEYWORDS (inside the for (const [tag, keywords]...) block) produces
false positives for short keywords; change the checks that use
urlLower.includes(kw), headingsLower.includes(kw), and bodyLower.includes(kw) to
use word-boundary, case-insensitive regex tests instead: build/escape a RegExp
for each kw using \b... \b and test against urlLower, headingsLower, and
bodyLower so only whole-word matches increment the score (keep the same +3/+2/+1
scoring logic and ensure kw escaping to avoid regex metacharacters).
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb76f9 and 2036981.

📒 Files selected for processing (6)
  • packages/extension/package.json
  • packages/extension/src/background.ts
  • packages/extension/src/manifest.json
  • packages/extension/src/popup.ts
  • packages/extension/src/types.ts
  • packages/extension/store-assets/STORE_LISTING.md

Comment on lines +29 to +33
const isRestricted =
!currentUrl ||
currentUrl.startsWith("chrome://") ||
currentUrl.startsWith("chrome-extension://") ||
currentUrl.startsWith("about:");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Restricted-URL check is out of sync with background.ts.

The popup checks chrome://, chrome-extension://, and about: — but background.ts also blocks https://chromewebstore.google.com. This means the Save button stays enabled on Chrome Web Store pages, and the user will see an error only after clicking it.

Consider extracting the restricted-prefix list into a shared constant in types.ts (or a shared module) so both files stay in sync.

Proposed approach

In a shared module (e.g., constants.ts):

export const RESTRICTED_PREFIXES = [
  "chrome://",
  "chrome-extension://",
  "https://chromewebstore.google.com",
  "about:",
];

export function isRestricted(url: string): boolean {
  if (!url) return true;
  return RESTRICTED_PREFIXES.some((p) => url.startsWith(p));
}

Then import and use isRestricted in both popup.ts and background.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/popup.ts` around lines 29 - 33, The popup's
restricted-url check is missing the Chrome Web Store prefix and is out-of-sync
with background.ts; extract the prefixes into a shared constant (e.g., export
RESTRICTED_PREFIXES and an isRestricted(url) helper in a new/shared module like
constants.ts or types.ts), include "https://chromewebstore.google.com" in that
list, then import and use the shared isRestricted in popup.ts (replace the
inline currentUrl check) and in background.ts so both files use the same logic.

- Extract RESTRICTED_PREFIXES and isRestricted() into shared constants.ts
  so popup and background use the same URL blocklist
- Fix yamlEscape to quote YAML bare scalars (true/false/null/yes/no/on/off)
  and numeric strings that parsers would coerce to non-string types
- Apply yamlEscape to skill name fields in both buildPageSkill and
  buildSelectionSkill to prevent YAML misinterpretation
- Switch tag detection from substring matching to word-boundary regex
  to eliminate false positives (e.g. "js" matching "adjustments")
- Check chrome.runtime.lastError in download callback and always
  revoke blob URL to prevent leaks on download failure
Copy link

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/extension/src/background.ts`:
- Around line 330-348: Change downloadSkill(name, skillMd) to return a Promise
that resolves when the chrome.downloads.download callback completes and rejects
if chrome.runtime.lastError exists; ensure URL.revokeObjectURL(blobUrl) still
runs. Update all callers (the four context menu handlers and message handlers
that currently call downloadSkill and then immediately call notifyTab() or
sendResponse()) to await downloadSkill(...) and only call notifyTab/sendResponse
after the Promise resolves, propagating any rejection so callers can
handle/report failures instead of always showing success.

In `@packages/extension/src/popup.ts`:
- Around line 18-23: The code in init and any other checks treats tab IDs as
falsy (rejecting 0); update the checks to use explicit null/undefined
comparisons: in the init function replace the falsy guard on tab?.id with an
explicit null/undefined check (e.g., test for tab?.id == null) and similarly
replace any checks like if (!currentTabId) or if (!tabId) with explicit
null/undefined checks (currentTabId == null) so a valid tab id of 0 is accepted;
apply this change to the guard in init and the other occurrence referencing
currentTabId/tab id.
- Around line 53-57: Wrap the chrome.runtime.sendMessage call in a
try/catch/finally inside the async function that uses response so the UI is
always recovered: call await chrome.runtime.sendMessage(message) inside try,
handle rejected cases in catch (e.g., treat as ErrorResponse or log/propagate),
and move saveBtn.removeAttribute("disabled") into finally so saveBtn is
re-enabled whether sendMessage resolves or throws; reference the response
variable and SaveResponse/ErrorResponse types and the saveBtn DOM element to
locate and update the code.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2036981 and 09ce95e.

📒 Files selected for processing (3)
  • packages/extension/src/background.ts
  • packages/extension/src/constants.ts
  • packages/extension/src/popup.ts

Comment on lines 330 to +348
function downloadSkill(name: string, skillMd: string): void {
const blob = new Blob([skillMd], { type: 'text/markdown' });
const blob = new Blob([skillMd], { type: "text/markdown" });
const blobUrl = URL.createObjectURL(blob);
chrome.downloads.download({
url: blobUrl,
filename: `skillkit-skills/${name}/SKILL.md`,
saveAs: false,
}, () => {
URL.revokeObjectURL(blobUrl);
});
chrome.downloads.download(
{
url: blobUrl,
filename: `skillkit-skills/${name}/SKILL.md`,
saveAs: false,
},
() => {
if (chrome.runtime.lastError) {
console.error(
`[SkillKit] Download failed for skillkit-skills/${name}/SKILL.md:`,
chrome.runtime.lastError.message,
);
}
URL.revokeObjectURL(blobUrl);
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the downloadSkill function and verify it exists
rg "function downloadSkill|const downloadSkill" packages/extension/src/background.ts -A 20

Repository: rohitg00/skillkit

Length of output: 630


🏁 Script executed:

# Find all call sites of downloadSkill
rg "downloadSkill\(" packages/extension/src/background.ts -B 3 -A 3

Repository: rohitg00/skillkit

Length of output: 1307


🏁 Script executed:

# Check the file structure to understand the context better
wc -l packages/extension/src/background.ts

Repository: rohitg00/skillkit

Length of output: 101


🏁 Script executed:

# Get a broader view of the file around the downloadSkill function
sed -n '320,360p' packages/extension/src/background.ts

Repository: rohitg00/skillkit

Length of output: 1244


Propagate download failure instead of only logging it.

downloadSkill currently logs lastError asynchronously but returns void, allowing callers to immediately report success via notifyTab() or sendResponse() without knowing if the download succeeded. This causes the UI to show a success checkmark even when the file write fails. Return a Promise from downloadSkill and have all callers await it before notifying success.

All four call sites (context menu handlers and message handlers) have this issue and must be updated to await the result.

Suggested direction
-function downloadSkill(name: string, skillMd: string): void {
+function downloadSkill(name: string, skillMd: string): Promise<void> {
   const blob = new Blob([skillMd], { type: "text/markdown" });
   const blobUrl = URL.createObjectURL(blob);
-  chrome.downloads.download(
-    {
-      url: blobUrl,
-      filename: `skillkit-skills/${name}/SKILL.md`,
-      saveAs: false,
-    },
-    () => {
-      if (chrome.runtime.lastError) {
-        console.error(
-          `[SkillKit] Download failed for skillkit-skills/${name}/SKILL.md:`,
-          chrome.runtime.lastError.message,
-        );
-      }
-      URL.revokeObjectURL(blobUrl);
-    },
-  );
+  return new Promise((resolve, reject) => {
+    chrome.downloads.download(
+      {
+        url: blobUrl,
+        filename: `skillkit-skills/${name}/SKILL.md`,
+        saveAs: false,
+      },
+      (downloadId) => {
+        try {
+          if (chrome.runtime.lastError || downloadId == null) {
+            reject(
+              new Error(
+                chrome.runtime.lastError?.message ?? "Download did not start",
+              ),
+            );
+            return;
+          }
+          resolve();
+        } finally {
+          URL.revokeObjectURL(blobUrl);
+        }
+      },
+    );
+  });
}

Then await downloadSkill(...) in all callers before notifyTab(...) or sendResponse(...).

📝 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
function downloadSkill(name: string, skillMd: string): void {
const blob = new Blob([skillMd], { type: 'text/markdown' });
const blob = new Blob([skillMd], { type: "text/markdown" });
const blobUrl = URL.createObjectURL(blob);
chrome.downloads.download({
url: blobUrl,
filename: `skillkit-skills/${name}/SKILL.md`,
saveAs: false,
}, () => {
URL.revokeObjectURL(blobUrl);
});
chrome.downloads.download(
{
url: blobUrl,
filename: `skillkit-skills/${name}/SKILL.md`,
saveAs: false,
},
() => {
if (chrome.runtime.lastError) {
console.error(
`[SkillKit] Download failed for skillkit-skills/${name}/SKILL.md:`,
chrome.runtime.lastError.message,
);
}
URL.revokeObjectURL(blobUrl);
},
);
function downloadSkill(name: string, skillMd: string): Promise<void> {
const blob = new Blob([skillMd], { type: "text/markdown" });
const blobUrl = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
chrome.downloads.download(
{
url: blobUrl,
filename: `skillkit-skills/${name}/SKILL.md`,
saveAs: false,
},
(downloadId) => {
try {
if (chrome.runtime.lastError || downloadId == null) {
reject(
new Error(
chrome.runtime.lastError?.message ?? "Download did not start",
),
);
return;
}
resolve();
} finally {
URL.revokeObjectURL(blobUrl);
}
},
);
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/background.ts` around lines 330 - 348, Change
downloadSkill(name, skillMd) to return a Promise that resolves when the
chrome.downloads.download callback completes and rejects if
chrome.runtime.lastError exists; ensure URL.revokeObjectURL(blobUrl) still runs.
Update all callers (the four context menu handlers and message handlers that
currently call downloadSkill and then immediately call notifyTab() or
sendResponse()) to await downloadSkill(...) and only call notifyTab/sendResponse
after the Promise resolves, propagating any rejection so callers can
handle/report failures instead of always showing success.

Comment on lines +18 to 23
let currentTabId = 0;

async function init() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) return;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd packages/extension/src && head -50 popup.ts | cat -n

Repository: rohitg00/skillkit

Length of output: 1994


🌐 Web query:

Chrome tabs API tab ID value range 0 valid identifier

💡 Result:

In the Chrome Extensions Tabs API, a real tab’s ID is a non‑negative integer (≥ 0)—so 0 is a valid tab ID (don’t treat it as “no tab” just because it’s falsy in JS). (chromium.googlesource.com)

The reserved “no tab” sentinel is chrome.tabs.TAB_ID_NONE = -1 (used to represent the absence of a browser tab, e.g., some devtools/app windows). (developer.chrome.com)

There’s no documented maximum value; tab IDs are just unique within the current browser session. (developer.chrome.com)

Citations:


Use explicit null/undefined checks for tab IDs instead of falsy checks.

Tab IDs in Chrome's Tabs API are non-negative integers (≥ 0), so 0 is a valid tab ID. Lines 22 and 38 currently reject this value using falsy checks. Replace with explicit null/undefined checks to allow valid numeric IDs.

Suggested fix
-let currentTabId = 0;
+let currentTabId: number | null = null;

-  if (!tab?.id) return;
+  if (tab?.id == null) return;

-  if (!currentUrl || !currentTabId) return;
+  if (!currentUrl || currentTabId == null) return;

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/popup.ts` around lines 18 - 23, The code in init and
any other checks treats tab IDs as falsy (rejecting 0); update the checks to use
explicit null/undefined comparisons: in the init function replace the falsy
guard on tab?.id with an explicit null/undefined check (e.g., test for tab?.id
== null) and similarly replace any checks like if (!currentTabId) or if (!tabId)
with explicit null/undefined checks (currentTabId == null) so a valid tab id of
0 is accepted; apply this change to the guard in init and the other occurrence
referencing currentTabId/tab id.

Comment on lines 53 to 57
const response: SaveResponse | ErrorResponse | undefined =
await chrome.runtime.sendMessage(message);

saveBtn.removeAttribute('disabled');
saveBtn.removeAttribute("disabled");

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file exists and examine the relevant code
if [ -f "packages/extension/src/popup.ts" ]; then
  echo "=== File found, examining lines 50-70 for context ==="
  sed -n '50,70p' packages/extension/src/popup.ts | cat -n
else
  echo "File not found at packages/extension/src/popup.ts"
  # Try to find it
  find . -name "popup.ts" -type f 2>/dev/null | head -5
fi

Repository: rohitg00/skillkit

Length of output: 733


🏁 Script executed:

# Get more context around the code - read entire function or block
wc -l packages/extension/src/popup.ts

Repository: rohitg00/skillkit

Length of output: 95


🏁 Script executed:

# Read the full file to understand the context better
cat -n packages/extension/src/popup.ts | head -100

Repository: rohitg00/skillkit

Length of output: 3112


Ensure UI recovery when sendMessage fails.

If chrome.runtime.sendMessage rejects, the button remains disabled and the popup appears frozen. Wrap this in try/catch/finally and always restore button state.

Suggested fix
-  const response: SaveResponse | ErrorResponse | undefined =
-    await chrome.runtime.sendMessage(message);
-
-  saveBtn.removeAttribute("disabled");
+  let response: SaveResponse | ErrorResponse | undefined;
+  try {
+    response = await chrome.runtime.sendMessage(message);
+  } catch {
+    setStatus("error", "Failed to communicate with background script");
+    resultEl.style.display = "none";
+    return;
+  } finally {
+    saveBtn.removeAttribute("disabled");
+  }
📝 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
const response: SaveResponse | ErrorResponse | undefined =
await chrome.runtime.sendMessage(message);
saveBtn.removeAttribute('disabled');
saveBtn.removeAttribute("disabled");
let response: SaveResponse | ErrorResponse | undefined;
try {
response = await chrome.runtime.sendMessage(message);
} catch {
setStatus("error", "Failed to communicate with background script");
resultEl.style.display = "none";
return;
} finally {
saveBtn.removeAttribute("disabled");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/extension/src/popup.ts` around lines 53 - 57, Wrap the
chrome.runtime.sendMessage call in a try/catch/finally inside the async function
that uses response so the UI is always recovered: call await
chrome.runtime.sendMessage(message) inside try, handle rejected cases in catch
(e.g., treat as ErrorResponse or log/propagate), and move
saveBtn.removeAttribute("disabled") into finally so saveBtn is re-enabled
whether sendMessage resolves or throws; reference the response variable and
SaveResponse/ErrorResponse types and the saveBtn DOM element to locate and
update the code.

@rohitg00 rohitg00 merged commit a6c0905 into main Feb 28, 2026
10 checks passed
@rohitg00 rohitg00 deleted the fix/extension-local-content-extraction branch February 28, 2026 06:59
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