Conversation
The share page now imports shortcuts directly into the extension via chrome.runtime.sendMessage (externally_connectable API). Flow: 1. User clicks 'Import N shortcuts' on the share page 2. Share page sends message to extension with shortcut data 3. Background script merges shortcuts into existing storage 4. Button turns green: '✓ Imported N shortcuts!' Fallbacks: - Extension not installed → shows 'Install Shortkeys first' link - Not Chrome → shows 'Copy JSON to clipboard' button Config: - externally_connectable matches shortkeys.app and localhost - onMessageExternal listener in background.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the import button was hidden by a try/catch when the extension ID didn't match (dev vs store). Now always shows both 'Import' and 'Copy JSON' buttons. If import fails, auto-copies JSON and shows a temporary error state before resetting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rror Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reviewer's GuideAdds a direct import flow from shortkeys.app share pages into the extension by replacing the static Web Store link with an import button that talks to the extension via external messages, implements the corresponding background listener to merge imported shortcuts into stored keys, and updates the extension manifest config to allow external connections from the share domain and localhost. Sequence diagram for importing shared shortcuts into the extensionsequenceDiagram
actor User
participant SharePage as Share_page_shortkeys.app
participant ChromeRuntime as Chrome_runtime
participant Background as Extension_background
participant Storage as Extension_storage
User->>SharePage: Click import button
SharePage->>SharePage: importToExtension()
alt chrome.runtime.sendMessage available
SharePage->>ChromeRuntime: sendMessage(STORE_ID, importShortcuts, shortcutsData)
ChromeRuntime->>Background: onMessageExternal(message)
Background->>Background: validate action and shortcuts
Background->>Storage: loadKeys()
Storage-->>Background: existingShortcuts
Background->>Background: add ids and merge with existing
Background->>Storage: saveKeys(mergedShortcuts)
Storage-->>Background: save complete
Background-->>ChromeRuntime: sendResponse({ success, count })
ChromeRuntime-->>SharePage: response
SharePage->>SharePage: Update button to Imported state
else runtime missing or error/unsuccessful response
SharePage->>SharePage: showFallback(btn)
SharePage->>SharePage: copyJson()
SharePage->>User: Show Copied confirmation on button
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Summary of ChangesHello @ib-bsb-br, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the user experience for sharing and importing shortcuts by introducing a direct import mechanism. Instead of manually copying JSON, users can now import shortcuts directly from the Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The
chrome.runtime.onMessageExternalhandler trusts any message that matches the action shape; consider validating the sender (e.g.,sender.origin/sender.urlagainst an allowlist) in addition toexternally_connectable.matchesbefore importing shortcuts. - When importing shortcuts you
concatexisting and new items and reuse incomingids when present; ifids collide or the same shortcut is imported multiple times this can lead to duplicates—consider normalizing IDs and/or deduplicating based on a stable key.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `chrome.runtime.onMessageExternal` handler trusts any message that matches the action shape; consider validating the sender (e.g., `sender.origin`/`sender.url` against an allowlist) in addition to `externally_connectable.matches` before importing shortcuts.
- When importing shortcuts you `concat` existing and new items and reuse incoming `id`s when present; if `id`s collide or the same shortcut is imported multiple times this can lead to duplicates—consider normalizing IDs and/or deduplicating based on a stable key.
## Individual Comments
### Comment 1
<location> `src/entrypoints/background.ts:182-183` </location>
<code_context>
+ if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
+ ;(async () => {
+ const raw = await loadKeys()
+ const existing = JSON.parse(raw || '[]')
+ const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
+ const merged = existing.concat(newShortcuts)
+ await saveKeys(merged)
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider validating and normalizing imported shortcuts before merging them into storage.
Since `message.shortcuts` comes from an external origin, it’s currently written to storage with arbitrary shape. It would be safer to whitelist/normalize the expected fields and enforce a reasonable limit on the number of shortcuts before saving, so external pages can’t inject unexpected data into extension storage.
Suggested implementation:
```typescript
const MAX_IMPORTED_SHORTCUTS = 100
const MAX_TOTAL_SHORTCUTS = 1000
const normalizeImportedShortcut = (s: any) => {
if (!s || typeof s !== 'object') return null
const normalized: any = {}
if (typeof s.key === 'string') normalized.key = s.key
if (typeof s.action === 'string') normalized.action = s.action
if (typeof s.label === 'string') normalized.label = s.label
if (Array.isArray(s.sites)) {
normalized.sites = s.sites
.filter((site: any) => typeof site === 'string')
.slice(0, 50)
}
if (typeof s.blacklist === 'boolean') normalized.blacklist = s.blacklist
if (typeof s.active === 'boolean') normalized.active = s.active
if (typeof s.activeInInputs === 'boolean') {
normalized.activeInInputs = s.activeInInputs
}
// Require at least a key and action for a valid shortcut
if (!normalized.key || !normalized.action) return null
normalized.id = typeof s.id === 'string' ? s.id : uuid()
return normalized
}
// Handle imports from shortkeys.app share links
```
```typescript
chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
;(async () => {
const raw = await loadKeys()
const existing = JSON.parse(raw || '[]')
// Limit number of imported shortcuts and normalize their shape
const limitedIncoming = message.shortcuts.slice(0, MAX_IMPORTED_SHORTCUTS)
const newShortcuts = limitedIncoming
.map(normalizeImportedShortcut)
.filter((s): s is any => s !== null)
const merged = existing.concat(newShortcuts).slice(0, MAX_TOTAL_SHORTCUTS)
await saveKeys(merged)
sendResponse({ success: true, count: newShortcuts.length })
})()
return true
}
})
})
```
</issue_to_address>
### Comment 2
<location> `site/share.html:126-133` </location>
<code_context>
document.getElementById('copy-json').style.display = 'block'
+
+ function importToExtension() {
+ const btn = document.getElementById('import-btn')
+ btn.disabled = true
+ btn.innerHTML = 'Importing...'
+
+ // Try sending to the extension
+ if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
+ chrome.runtime.sendMessage(STORE_ID, {
+ action: 'importShortcuts',
+ shortcuts: shortcutsData
</code_context>
<issue_to_address>
**suggestion:** Add a timeout or fallback for cases where sendMessage never invokes the callback.
If `chrome.runtime.sendMessage` never calls its callback (e.g., background script not reachable or crashes), the button will stay disabled with `Importing...`. Consider adding a timeout that checks if the button is still in the loading state after a few seconds and then calls `showFallback(btn)` so the UI can recover in this scenario.
</issue_to_address>
### Comment 3
<location> `wxt.config.ts:142-144` </location>
<code_context>
'userScripts',
],
host_permissions: ['*://*/*'],
+ externally_connectable: {
+ matches: ['https://shortkeys.app/*', 'http://localhost/*'],
+ },
content_security_policy: {
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider scoping the localhost externally_connectable entry to development only.
This configuration lets any localhost origin send external messages to the extension in all builds. If this is only for local development/testing, consider narrowing it to a specific port/path or including the localhost entry only in dev builds to avoid overly broad permissions in production.
```suggestion
externally_connectable: {
matches: [
'https://shortkeys.app/*',
...(process.env.NODE_ENV === 'development' ? ['http://localhost/*'] : []),
],
},
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const existing = JSON.parse(raw || '[]') | ||
| const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() })) |
There was a problem hiding this comment.
🚨 suggestion (security): Consider validating and normalizing imported shortcuts before merging them into storage.
Since message.shortcuts comes from an external origin, it’s currently written to storage with arbitrary shape. It would be safer to whitelist/normalize the expected fields and enforce a reasonable limit on the number of shortcuts before saving, so external pages can’t inject unexpected data into extension storage.
Suggested implementation:
const MAX_IMPORTED_SHORTCUTS = 100
const MAX_TOTAL_SHORTCUTS = 1000
const normalizeImportedShortcut = (s: any) => {
if (!s || typeof s !== 'object') return null
const normalized: any = {}
if (typeof s.key === 'string') normalized.key = s.key
if (typeof s.action === 'string') normalized.action = s.action
if (typeof s.label === 'string') normalized.label = s.label
if (Array.isArray(s.sites)) {
normalized.sites = s.sites
.filter((site: any) => typeof site === 'string')
.slice(0, 50)
}
if (typeof s.blacklist === 'boolean') normalized.blacklist = s.blacklist
if (typeof s.active === 'boolean') normalized.active = s.active
if (typeof s.activeInInputs === 'boolean') {
normalized.activeInInputs = s.activeInInputs
}
// Require at least a key and action for a valid shortcut
if (!normalized.key || !normalized.action) return null
normalized.id = typeof s.id === 'string' ? s.id : uuid()
return normalized
}
// Handle imports from shortkeys.app share links chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
;(async () => {
const raw = await loadKeys()
const existing = JSON.parse(raw || '[]')
// Limit number of imported shortcuts and normalize their shape
const limitedIncoming = message.shortcuts.slice(0, MAX_IMPORTED_SHORTCUTS)
const newShortcuts = limitedIncoming
.map(normalizeImportedShortcut)
.filter((s): s is any => s !== null)
const merged = existing.concat(newShortcuts).slice(0, MAX_TOTAL_SHORTCUTS)
await saveKeys(merged)
sendResponse({ success: true, count: newShortcuts.length })
})()
return true
}
})
})| function importToExtension() { | ||
| const btn = document.getElementById('import-btn') | ||
| btn.disabled = true | ||
| btn.innerHTML = 'Importing...' | ||
|
|
||
| // Try sending to the extension | ||
| if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) { | ||
| chrome.runtime.sendMessage(STORE_ID, { |
There was a problem hiding this comment.
suggestion: Add a timeout or fallback for cases where sendMessage never invokes the callback.
If chrome.runtime.sendMessage never calls its callback (e.g., background script not reachable or crashes), the button will stay disabled with Importing.... Consider adding a timeout that checks if the button is still in the loading state after a few seconds and then calls showFallback(btn) so the UI can recover in this scenario.
| externally_connectable: { | ||
| matches: ['https://shortkeys.app/*', 'http://localhost/*'], | ||
| }, |
There was a problem hiding this comment.
🚨 suggestion (security): Consider scoping the localhost externally_connectable entry to development only.
This configuration lets any localhost origin send external messages to the extension in all builds. If this is only for local development/testing, consider narrowing it to a specific port/path or including the localhost entry only in dev builds to avoid overly broad permissions in production.
| externally_connectable: { | |
| matches: ['https://shortkeys.app/*', 'http://localhost/*'], | |
| }, | |
| externally_connectable: { | |
| matches: [ | |
| 'https://shortkeys.app/*', | |
| ...(process.env.NODE_ENV === 'development' ? ['http://localhost/*'] : []), | |
| ], | |
| }, |
There was a problem hiding this comment.
3 issues found across 3 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/entrypoints/background.ts">
<violation number="1" location="src/entrypoints/background.ts:178">
P1: Security: The `_sender` origin is not validated. Even though `externally_connectable` restricts which origins can send messages, explicitly checking `_sender.origin` is a critical defense-in-depth measure — especially since `http://localhost/*` is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including `javascript` action shortcuts with arbitrary code).</violation>
<violation number="2" location="src/entrypoints/background.ts:183">
P1: Security: Imported shortcuts are spread (`...s`) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with `action: 'javascript'` (via `registerUserScript`), an external sender can inject arbitrary JS code that runs on all pages by including `{ action: 'javascript', code: '...' }` in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping `javascript` action types from external imports.</violation>
</file>
<file name="site/share.html">
<violation number="1" location="site/share.html:151">
P2: The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| }) | ||
|
|
||
| // Handle imports from shortkeys.app share links | ||
| chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => { |
There was a problem hiding this comment.
P1: Security: The _sender origin is not validated. Even though externally_connectable restricts which origins can send messages, explicitly checking _sender.origin is a critical defense-in-depth measure — especially since http://localhost/* is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including javascript action shortcuts with arbitrary code).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 178:
<comment>Security: The `_sender` origin is not validated. Even though `externally_connectable` restricts which origins can send messages, explicitly checking `_sender.origin` is a critical defense-in-depth measure — especially since `http://localhost/*` is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including `javascript` action shortcuts with arbitrary code).</comment>
<file context>
@@ -173,4 +173,19 @@ export default defineBackground(() => {
})
+
+ // Handle imports from shortkeys.app share links
+ chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
+ if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
+ ;(async () => {
</file context>
| ;(async () => { | ||
| const raw = await loadKeys() | ||
| const existing = JSON.parse(raw || '[]') | ||
| const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() })) |
There was a problem hiding this comment.
P1: Security: Imported shortcuts are spread (...s) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with action: 'javascript' (via registerUserScript), an external sender can inject arbitrary JS code that runs on all pages by including { action: 'javascript', code: '...' } in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping javascript action types from external imports.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 183:
<comment>Security: Imported shortcuts are spread (`...s`) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with `action: 'javascript'` (via `registerUserScript`), an external sender can inject arbitrary JS code that runs on all pages by including `{ action: 'javascript', code: '...' }` in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping `javascript` action types from external imports.</comment>
<file context>
@@ -173,4 +173,19 @@ export default defineBackground(() => {
+ ;(async () => {
+ const raw = await loadKeys()
+ const existing = JSON.parse(raw || '[]')
+ const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
+ const merged = existing.concat(newShortcuts)
+ await saveKeys(merged)
</file context>
|
|
||
| function showFallback(btn) { | ||
| // Silently copy JSON and show success | ||
| copyJson() |
There was a problem hiding this comment.
P2: The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At site/share.html, line 151:
<comment>The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.</comment>
<file context>
@@ -111,13 +111,52 @@ <h1>Shared Shortcuts</h1>
+
+ function showFallback(btn) {
+ // Silently copy JSON and show success
+ copyJson()
+ btn.innerHTML = '✓ Copied!'
+ btn.style.background = '#059669'
</file context>
There was a problem hiding this comment.
Code Review
The pull request introduces functionality to directly import shared shortcuts from shortkeys.app into the extension. This includes adding an import button on the website, allowing the extension to accept external messages for importing shortcuts, and configuring the extension's externally_connectable manifest property. The changes are well-structured and address the stated objectives. I've identified a few areas for improvement regarding error handling and code clarity.
| if (chrome.runtime.lastError || !response?.success) { | ||
| showFallback(btn) | ||
| return |
There was a problem hiding this comment.
The chrome.runtime.lastError can contain valuable information about why the message failed. It would be beneficial to log this error to the console for debugging purposes, especially during development. This helps in understanding potential issues with external message communication.
| if (chrome.runtime.lastError || !response?.success) { | |
| showFallback(btn) | |
| return | |
| if (chrome.runtime.lastError || !response?.success) { | |
| console.error("Error importing shortcuts:", chrome.runtime.lastError); | |
| showFallback(btn); | |
| return; | |
| } |
| ;(async () => { | ||
| const raw = await loadKeys() | ||
| const existing = JSON.parse(raw || '[]') | ||
| const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() })) |
There was a problem hiding this comment.
The s: any type assertion is a bit broad. While it works, defining a more specific interface for Shortcut would improve type safety and code readability. This would help catch potential issues at compile time and make the code easier to understand for future maintainers.
| const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() })) | |
| const newShortcuts = message.shortcuts.map((s: Shortcut) => ({ ...s, id: s.id || uuid() })) |
When you select 'Run JavaScript' as the behavior, a 'Snippets' button appears in the code editor header. Click it to browse a searchable library of 26 curated scripts across 5 categories: Page Tools (6): Dark mode, reader mode, hide sticky banners, increase text size, highlight links, make page editable Productivity (6): Copy as markdown link, copy all page links, word count, extract emails, export table to CSV, show QR code Content (4): Remove images, show image sizes, page outline, reading progress bar Developer (5): Show layout grid, clear site storage, performance metrics, log all events, pretty print JSON Media (5): Picture-in-picture, set video speed, list image URLs, show audio volume Click any snippet to insert its code into the editor. Also sets the shortcut label to the snippet name if not already set. 8 tests validate: required fields, unique IDs, valid categories, valid JS syntax (via new Function()), searchability. 370 tests total. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Page Scripts (new behavior category): All 26 JS snippets are now selectable directly from the Behavior dropdown under 'Page Scripts' — no need to choose 'Run JavaScript' and paste code manually. Includes: - Dark mode, reader mode, hide sticky banners, bigger text - Copy markdown link, copy all links, word count, extract emails - Table→CSV, QR code, picture-in-picture, video speed - Layout grid, clear storage, perf metrics, pretty-print JSON - And 10 more Greasyfork/OpenUserJS Import: When using 'Run JavaScript', a new 'Import from URL' field appears below the code editor. Paste a Greasyfork or OpenUserJS URL and click Fetch — the extension downloads the userscript, strips the metadata header, extracts the name, and populates the editor. Supports: - Greasyfork page URLs → auto-converts to raw code URL - OpenUserJS page URLs → auto-converts to install URL - Direct .user.js URLs Background script handles the fetch (bypasses CORS via host_permissions). 395 tests passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
3 issues found across 6 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/utils/fetch-userscript.ts">
<violation number="1" location="src/utils/fetch-userscript.ts:6">
P2: Greasyfork locale codes can include hyphens (e.g., zh-CN), but the regex only matches \w+ so those URLs won’t resolve to the raw code URL. Allow hyphenated locale segments (and optional locale) so users on non-2-letter locales are handled.</violation>
</file>
<file name="src/entrypoints/options/App.vue">
<violation number="1" location="src/entrypoints/options/App.vue:443">
P2: importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.</violation>
</file>
<file name="src/entrypoints/background.ts">
<violation number="1" location="src/entrypoints/background.ts:118">
P1: Security: `fetchUrl` handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's `*://*/*` host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through `resolveUserscriptUrl()`, the background handler should enforce an allowlist of permitted domains (e.g., `greasyfork.org`, `openuserjs.org`) as a defense-in-depth measure.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const action = request.action | ||
|
|
||
| if (action === 'fetchUrl') { | ||
| fetch(request.url) |
There was a problem hiding this comment.
P1: Security: fetchUrl handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's *://*/* host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through resolveUserscriptUrl(), the background handler should enforce an allowlist of permitted domains (e.g., greasyfork.org, openuserjs.org) as a defense-in-depth measure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 118:
<comment>Security: `fetchUrl` handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's `*://*/*` host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through `resolveUserscriptUrl()`, the background handler should enforce an allowlist of permitted domains (e.g., `greasyfork.org`, `openuserjs.org`) as a defense-in-depth measure.</comment>
<file context>
@@ -114,6 +114,14 @@ export default defineBackground(() => {
const action = request.action
+ if (action === 'fetchUrl') {
+ fetch(request.url)
+ .then(r => r.text())
+ .then(text => sendResponse({ text }))
</file context>
| return | ||
| } | ||
| const { code, name } = parseUserscript(resp.text) | ||
| keys.value[index].code = code |
There was a problem hiding this comment.
P2: importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/options/App.vue, line 443:
<comment>importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.</comment>
<file context>
@@ -423,6 +427,29 @@ async function testJavascript(row: KeySetting) {
+ return
+ }
+ const { code, name } = parseUserscript(resp.text)
+ keys.value[index].code = code
+ userscriptMessage.value = '✓ Imported: ' + name
+ userscriptUrl.value = ''
</file context>
…itor UI - Removed Snippets button, modal, and all related code/CSS since all 26 snippets are now first-class actions in the Behavior dropdown - Moved userscript import bar outside the dark code-editor-wrap so it sits cleanly below with light theme styling and a link icon - Fixed Greasyfork URL regex to handle zh-CN locales and URLs without language prefix - 11 new tests for userscript URL resolution and parsing - 406 tests total Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…visibility
- Removed duplicate 'Copy as markdown link' from Page Scripts
(already exists as copytitleurlmarkdown in Location)
- All action labels converted to sentence case ('Log all events'
not 'Log All Events')
- Userscript URL import bar now only shows when action is
'javascript' (was persisting after switching behaviors)
- Fixed shortcut-header padding so toggle doesn't clip edge
405 tests passing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…matches The shortcut-group is now a single rounded container with overflow: hidden. Cards inside have no border-radius — they sit flush within the group. The group header, shortcut cards, and add-shortcut button all share the group's rounded corners. No more sharp-meets- round corner artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary by Sourcery
Enable direct importing of shared shortcuts from shortkeys.app into the extension and allow the website to communicate with the extension for this purpose.
New Features:
Enhancements:
Summary by cubic
Enable one-click import of shared shortcuts from shortkeys.app, add 26 built-in Page Scripts as first-class actions, and support importing userscripts from Greasyfork/OpenUserJS. Also polishes the groups UI to fix corner mismatches.
New Features
Bug Fixes
Written for commit 957d9a2. Summary will update on new commits.