From 87adc45518f41801178311e4d6ff785025b28d8e Mon Sep 17 00:00:00 2001 From: Xinyi Li Date: Sat, 17 Feb 2024 00:41:52 -0600 Subject: [PATCH 1/4] tab-specific icon and switch --- src/codemirror.ts | 3 ++ src/contentScript.ts | 26 +++++++++++---- src/monacoCompletionProvider.ts | 2 ++ src/script.ts | 59 ++++++++++++++++++++++++++++++++- src/serviceWorker.ts | 26 ++++++++++++++- src/shared.ts | 23 +++++++++++++ static/manifest.json | 2 +- 7 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/codemirror.ts b/src/codemirror.ts index 707e3c4..d64b0e3 100644 --- a/src/codemirror.ts +++ b/src/codemirror.ts @@ -113,6 +113,9 @@ export class CodeMirrorManager { relativePath: string | undefined, createDisposables: (() => IDisposable[]) | undefined ): Promise { + if (!window.codeium_enabled) { + return; + } const clientSettings = await this.client.clientSettingsPoller.clientSettings; if (clientSettings.apiKey === undefined) { return; diff --git a/src/contentScript.ts b/src/contentScript.ts index 6e4c358..2957241 100644 --- a/src/contentScript.ts +++ b/src/contentScript.ts @@ -1,6 +1,20 @@ -const s = document.createElement('script'); -s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({ id: chrome.runtime.id }); -s.onload = function () { - (this as HTMLScriptElement).remove(); -}; -(document.head || document.documentElement).prepend(s); +// avoid injecting the script into redundant frames +if (window.top === window.self) { + const s = document.createElement('script'); + s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({ id: chrome.runtime.id }); + s.onload = function () { + (this as HTMLScriptElement).remove(); + }; + (document.head || document.documentElement).prepend(s); + + let codeiumEnabled = true; + + chrome.runtime.onMessage.addListener((message) => { + if (message.type === 'codeium_toggle') { + codeiumEnabled = !codeiumEnabled; + window.dispatchEvent( + new CustomEvent('CodeiumEvent', { detail: { enabled: codeiumEnabled } }) + ); + } + }); +} diff --git a/src/monacoCompletionProvider.ts b/src/monacoCompletionProvider.ts index 8f2baaf..2406eed 100644 --- a/src/monacoCompletionProvider.ts +++ b/src/monacoCompletionProvider.ts @@ -406,6 +406,7 @@ export class MonacoCompletionProvider implements monaco.languages.InlineCompleti model: monaco.editor.ITextModel, position: monaco.Position ): Promise { + if (!window.codeium_enabled) return; const clientSettings = await this.client.clientSettingsPoller.clientSettings; if (clientSettings.apiKey === undefined) { return; @@ -454,6 +455,7 @@ export class MonacoCompletionProvider implements monaco.languages.InlineCompleti ) .filter((item): item is monaco.languages.InlineCompletion => item !== undefined); void chrome.runtime.sendMessage(this.extensionId, { type: 'success' }); + console.log(items); return { items }; } diff --git a/src/script.ts b/src/script.ts index ebbd9b5..9b604c0 100644 --- a/src/script.ts +++ b/src/script.ts @@ -27,8 +27,25 @@ async function getAllowlist(extensionId: string): Promise return allowlist; } +// chrome.runtime.onMessage.addListener((message) => { +// console.log('message', message); +// }); + +window.addEventListener('CodeiumEvent', (event) => { + const enabled = (event as CustomEvent).detail.enabled; + window.codeium_enabled = enabled; + console.log('CodeiumEvent', enabled); +}); + // Clear any bad state from another tab. -void chrome.runtime.sendMessage(extensionId, { type: 'success' }); +// void chrome.runtime.sendMessage(extensionId, { type: 'success' }); +function update_icon_status(extensionId: string, status: 'off' | 'idle' | 'active') { + chrome.runtime.sendMessage(extensionId, { type: 'icon_status', status }).catch((e) => { + console.error(e); + }); +} + +update_icon_status(extensionId, 'idle'); const SUPPORTED_MONACO_SITES = new Map([ [/https:\/\/colab.research\.google\.com\/.*/, OMonacoSite.COLAB], @@ -42,9 +59,41 @@ declare global { interface Window { _monaco?: Monaco; _MonacoEnvironment?: monaco.Environment; + _codeium_enabled?: boolean; + _codeium_activated?: boolean; + codeium_enabled?: boolean; + codeium_activated?: boolean; } } +Object.defineProperties(window, { + codeium_enabled: { + get() { + return this._codeium_enabled ?? true; + }, + set(enabled: boolean) { + this._codeium_enabled = enabled; + + if (enabled) { + update_icon_status(extensionId, this.codeium_activated ? 'active' : 'idle'); + } else { + update_icon_status(extensionId, 'off'); + } + }, + }, + codeium_activated: { + get() { + return this._codeium_activated ?? false; + }, + set(activated: boolean) { + this._codeium_activated = activated; + if (this.codeium_enabled) { + update_icon_status(extensionId, activated ? 'active' : 'idle'); + } + }, + }, +}); + // Intercept creation of monaco so we don't have to worry about timing the injection. const addMonacoInject = () => Object.defineProperties(window, { @@ -121,6 +170,7 @@ if (jupyterConfigDataElement !== null) { _jupyterapp.activatePlugin(p.id).then( () => { console.log('Activated Codeium: Jupyter 3.x'); + window.codeium_activated = true; }, (e) => { console.error(e); @@ -146,6 +196,7 @@ if (jupyterConfigDataElement !== null) { _jupyterlab.activatePlugin(p.id).then( () => { console.log('Activated Codeium: Jupyter 2.x'); + window.codeium_activated = true; }, (e) => { console.error(e); @@ -182,6 +233,7 @@ const addCodeMirror5GlobalInject = () => injectCodeMirror = true; const jupyterState = jupyterInject(extensionId, this.Jupyter); addListeners(cm as CodeMirror, jupyterState.codeMirrorManager); + window.codeium_activated = true; console.log('Activated Codeium'); } else { let multiplayer = false; @@ -198,6 +250,7 @@ const addCodeMirror5GlobalInject = () => } if (injectCodeMirror) { new CodeMirrorState(extensionId, cm as CodeMirror, multiplayer); + window.codeium_activated = true; console.log('Activated Codeium'); } } @@ -221,6 +274,7 @@ const addCodeMirror5LocalInject = () => { clearInterval(f); return; } + if (!window.codeium_enabled) return; let notebook = false; for (const pattern of SUPPORTED_CODEMIRROR_NONGLOBAL_SITES) { if (pattern.pattern.test(window.location.href)) { @@ -236,6 +290,9 @@ const addCodeMirror5LocalInject = () => { } const editor = maybeCodeMirror.CodeMirror; hook(editor); + if (!window.codeium_activated) { + window.codeium_activated = true; + } if (notebook) { docsByPosition.set(editor.getDoc(), (el as HTMLElement).getBoundingClientRect().top); } diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 854bf14..484205c 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -7,7 +7,7 @@ import { LanguageServerServiceWorkerClient, LanguageServerWorkerRequest, } from './common'; -import { loggedIn, loggedOut, unhealthy } from './shared'; +import { loggedIn, loggedOut, unhealthy, update_tab_icon } from './shared'; import { defaultAllowlist, getGeneralPortalUrl, @@ -114,6 +114,13 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => // No response needed. return; } + if (message.type == 'icon_status') { + update_tab_icon(sender.tab?.id, message.status).catch((e) => { + console.error(e); + }); + // No response needed. + return; + } if (typeof message.token !== 'string' || typeof message.state !== 'string') { console.log('Unexpected message:', message); return; @@ -129,6 +136,23 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => }); }); +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: 'toggle_feature', + title: 'Enable/Disable Codeium on Current Tab', + contexts: ['all'], + }); +}); + +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (tab === undefined || tab.id === undefined || info.menuItemId !== 'toggle_feature') { + return; + } + chrome.tabs.sendMessage(tab.id, { type: 'codeium_toggle' }).catch((e) => { + console.error(e); + }); +}); + chrome.runtime.onStartup.addListener(async () => { if ((await getStorageItem('user'))?.apiKey === undefined) { await loggedOut(); diff --git a/src/shared.ts b/src/shared.ts index f85b64d..361362a 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -58,3 +58,26 @@ export async function unhealthy(message: string): Promise { setStorageItem('lastError', { message: message }), ]); } + +export async function update_tab_icon(tabId: number | undefined, status: string): Promise { + const iconType = status === 'active' ? 'codeium_square_logo' : 'codeium_square_inactive'; + const text = status === 'active' ? '' : status; + + await Promise.all([ + chrome.action.setBadgeText({ + tabId: tabId, + text, + }), + chrome.action.setIcon({ + tabId: tabId, + path: { + 16: `/icons/16/${iconType}.png`, + 32: `/icons/32/${iconType}.png`, + 48: `/icons/48/${iconType}.png`, + 128: `/icons/128/${iconType}.png`, + }, + }), + chrome.action.setTitle({ title: `Codeium ${status}`, tabId: tabId }), + clearLastError(), + ]); +} diff --git a/static/manifest.json b/static/manifest.json index 94c547c..817035b 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -21,7 +21,7 @@ "matches": [""] } ], - "permissions": ["storage"], + "permissions": ["storage", "tabs", "activeTab", "contextMenus"], "options_ui": { "page": "options.html", "open_in_tab": false, From 375479cb5e4d25708c07661d636ab994417ea197 Mon Sep 17 00:00:00 2001 From: Xinyi Li Date: Thu, 29 Feb 2024 23:48:55 -0600 Subject: [PATCH 2/4] draft popup --- src/component/Popup.tsx | 220 ++++++++++++++++++++++++++++++++++++ src/popup.ts | 89 --------------- src/popup.tsx | 12 ++ static/logged_in_popup.html | 16 ++- static/popup.html | 18 ++- webpack.common.js | 2 +- 6 files changed, 264 insertions(+), 93 deletions(-) create mode 100644 src/component/Popup.tsx delete mode 100644 src/popup.ts create mode 100644 src/popup.tsx diff --git a/src/component/Popup.tsx b/src/component/Popup.tsx new file mode 100644 index 0000000..184fe55 --- /dev/null +++ b/src/component/Popup.tsx @@ -0,0 +1,220 @@ +import React, { useEffect } from 'react'; + +import { + Alert, + Button, + Link, + Snackbar, + TextField, + Typography, + IconButton, + Toolbar, + Box, +} from '@mui/material'; + +import { openAuthTab } from '../auth'; +import { loggedOut } from '../shared'; +import SettingsIcon from '@mui/icons-material/Settings'; +import LoginIcon from '@mui/icons-material/Login'; +import LogoutIcon from '@mui/icons-material/Logout'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import SaveAltIcon from '@mui/icons-material/SaveAlt'; + +import { + computeAllowlist, + defaultAllowlist, + getGeneralProfileUrl, + getStorageItem, + setStorageItem, +} from '../storage'; + +// Function to extract domain and convert it to a regex pattern +function domainToRegex(url: string) { + const { hostname } = new URL(url); + // Extract the top-level domain (TLD) and domain name + const domainParts = hostname.split('.').slice(-2); // This takes the last two parts of the hostname + const domain = domainParts.join('.'); + // Escape special regex characters (just in case) and create a regex that matches any subdomain or path + const regexSafeDomain = domain.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + return new RegExp(`https?://(.*\\.)?${regexSafeDomain}(/.*)?`); +} + +const getCurrentUrl = async () => { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + return tabs[0].url; +}; + +const addToAllowlist = async (input: string) => { + try { + const item = new RegExp(input); + const allowlist = computeAllowlist(await getStorageItem('allowlist')) || []; + for (const regex of allowlist) { + const r = new RegExp(regex); + if (r.source === item.source) { + return 'Already in the whitelist'; + } + } + allowlist.push(input); + + await setStorageItem('allowlist', { defaults: defaultAllowlist, current: allowlist }); + return 'success'; + } catch (e) { + return e?.message || 'Unknown error'; + } +}; + +const getAllowlist = async () => (await getStorageItem('allowlist')) || []; + +export const PopupPage = () => { + const [open, setOpen] = React.useState(false); + const [message, setMessage] = React.useState(''); + const [severity, setSeverity] = React.useState<'success' | 'error'>('success'); + const [user, setUser] = React.useState(undefined); + const [matched, setMatched] = React.useState(false); + const [regexItem, setRegexItem] = React.useState(''); + + const addItem = async () => { + const result = await addToAllowlist(regexItem); + if (result === 'success') { + setSeverity('success'); + setMessage('Added to the whitelist. Please refresh'); + } else { + setSeverity('error'); + setMessage(result); + } + setOpen(true); + }; + + useEffect(() => { + getStorageItem('user').then((u) => { + setUser(u as any); + }); + getCurrentUrl().then((tabURL: string | undefined) => { + getAllowlist().then((allowlist) => { + for (const regex of computeAllowlist(allowlist)) { + console.log(new RegExp(regex).test(tabURL)); + if (new RegExp(regex).test(tabURL)) { + setMatched(true); + setRegexItem(regex); + return; + } + } + setRegexItem(domainToRegex(tabURL ?? '').source); + }); + }); + }, []); + + const logout = async () => { + await setStorageItem('user', {}); + await loggedOut(); + window.close(); + }; + + return ( + + + + Codeium + + + Codeium + + + { + console.log('user', user); + if (user) { + await chrome.tabs.create({ url: 'https://codeium.com/profile' }); + } + }} + sx={{ display: (user as any)?.name ? 'flex' : 'none', float: 'right' }} + > + + + + + + {user && user.name ? ( + + {`Welcome, ${user.name}`} + + ) : ( + + Please login + + )} + + + + {matched ? 'Current URL matches:' : 'Adding current URL to whitelist:'} + + + setRegexItem(e.target.value)} + fullWidth + disabled={matched} + InputProps={{ + readOnly: matched, + endAdornment: !matched && ( + + + + ), + }} + sx={{ marginTop: 2, marginBottom: 2 }} + /> + + + + + + + setOpen(false)} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + > + setOpen(false)}> + {message} + + + + + ); +}; diff --git a/src/popup.ts b/src/popup.ts deleted file mode 100644 index 5624964..0000000 --- a/src/popup.ts +++ /dev/null @@ -1,89 +0,0 @@ -import '../styles/popup.scss'; -import { openAuthTab } from './auth'; -import { loggedOut } from './shared'; -import { getStorageItem, setStorageItem } from './storage'; - -if (CODEIUM_ENTERPRISE) { - const element = document.getElementById('extension-name'); - if (element !== null) { - element.textContent = 'Codeium Enterprise'; - } -} - -document.getElementById('login')?.addEventListener('click', openAuthTab); - -async function maybeShowPortalWarning() { - const portalUrl = await getStorageItem('portalUrl'); - let portalUrlWarningDisplay = 'none'; - let loginButtonDisplay = 'block'; - if (portalUrl === undefined || portalUrl === '') { - portalUrlWarningDisplay = 'block'; - loginButtonDisplay = 'none'; - } - const portalUrlWarning = document.getElementById('portal-url-warning'); - if (portalUrlWarning !== null) { - portalUrlWarning.style.display = portalUrlWarningDisplay; - } - const loginButton = document.getElementById('login'); - if (loginButton !== null) { - loginButton.style.display = loginButtonDisplay; - } -} -if (CODEIUM_ENTERPRISE) { - maybeShowPortalWarning().catch((e) => { - console.error(e); - }); - setInterval(maybeShowPortalWarning, 1000); -} - -document.getElementById('go-to-options')?.addEventListener('click', async () => { - await chrome.tabs.create({ url: 'chrome://extensions/?options=' + chrome.runtime.id }); -}); - -getStorageItem('user') - .then((user) => { - const usernameP = document.getElementById('username'); - if (usernameP !== null && user !== undefined) { - usernameP.textContent = `Welcome, ${user.name}`; - if (user.userPortalUrl !== undefined && user.userPortalUrl !== '') { - const br = document.createElement('br'); - usernameP.appendChild(br); - const a = document.createElement('a'); - const linkText = document.createTextNode('Portal'); - a.appendChild(linkText); - a.title = 'Portal'; - a.href = user.userPortalUrl; - a.addEventListener('click', async () => { - await chrome.tabs.create({ url: user.userPortalUrl }); - }); - usernameP.appendChild(a); - } - } - }) - .catch((error) => { - console.error(error); - }); - -document.getElementById('logout')?.addEventListener('click', async () => { - await setStorageItem('user', {}); - await loggedOut(); - window.close(); -}); - -getStorageItem('lastError').then( - (lastError) => { - const errorP = document.getElementById('error'); - if (errorP == null) { - return; - } - const message = lastError?.message; - if (message === undefined) { - errorP.remove(); - } else { - errorP.textContent = message; - } - }, - (e) => { - console.error(e); - } -); diff --git a/src/popup.tsx b/src/popup.tsx new file mode 100644 index 0000000..6ecf371 --- /dev/null +++ b/src/popup.tsx @@ -0,0 +1,12 @@ +import '../styles/popup.scss'; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { PopupPage } from './component/Popup'; + +const container = document.getElementById('codeium-popup'); + +if (container !== null) { + ReactDOM.render(, container); +} diff --git a/static/logged_in_popup.html b/static/logged_in_popup.html index d9880da..098a579 100644 --- a/static/logged_in_popup.html +++ b/static/logged_in_popup.html @@ -1,4 +1,4 @@ - + + + + + + + + + Codeium options + + +
diff --git a/static/popup.html b/static/popup.html index 9bd60ac..46987b8 100644 --- a/static/popup.html +++ b/static/popup.html @@ -1,4 +1,4 @@ - + + + + + + + + + Codeium options + + +
+ + + \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index 842d3a4..7d7222a 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -11,7 +11,7 @@ module.exports = (env) => ({ entry: { serviceWorker: './src/serviceWorker.ts', contentScript: './src/contentScript.ts', - popup: './src/popup.ts', + popup: './src/popup.tsx', options: './src/options.tsx', // This script is loaded in contentScript.ts. script: './src/script.ts', From a3669956e2f68562ef0efc12a0c00b5ac5080db4 Mon Sep 17 00:00:00 2001 From: Xinyi Li Date: Sun, 3 Mar 2024 02:00:47 -0600 Subject: [PATCH 3/4] unify popup page --- src/component/Popup.tsx | 28 +++++++++++++++------------- src/shared.ts | 4 ++-- static/popup.html | 33 ++------------------------------- 3 files changed, 19 insertions(+), 46 deletions(-) diff --git a/src/component/Popup.tsx b/src/component/Popup.tsx index 184fe55..8e81449 100644 --- a/src/component/Popup.tsx +++ b/src/component/Popup.tsx @@ -47,7 +47,7 @@ const getCurrentUrl = async () => { const addToAllowlist = async (input: string) => { try { const item = new RegExp(input); - const allowlist = computeAllowlist(await getStorageItem('allowlist')) || []; + const allowlist = computeAllowlist(await getStorageItem('allowlist')) || undefined; for (const regex of allowlist) { const r = new RegExp(regex); if (r.source === item.source) { @@ -59,7 +59,7 @@ const addToAllowlist = async (input: string) => { await setStorageItem('allowlist', { defaults: defaultAllowlist, current: allowlist }); return 'success'; } catch (e) { - return e?.message || 'Unknown error'; + return (e as Error).message || 'Unknown error'; } }; @@ -69,7 +69,7 @@ export const PopupPage = () => { const [open, setOpen] = React.useState(false); const [message, setMessage] = React.useState(''); const [severity, setSeverity] = React.useState<'success' | 'error'>('success'); - const [user, setUser] = React.useState(undefined); + const [user, setUser] = React.useState(); const [matched, setMatched] = React.useState(false); const [regexItem, setRegexItem] = React.useState(''); @@ -90,16 +90,18 @@ export const PopupPage = () => { setUser(u as any); }); getCurrentUrl().then((tabURL: string | undefined) => { + const curURL: string = tabURL ?? ''; getAllowlist().then((allowlist) => { - for (const regex of computeAllowlist(allowlist)) { - console.log(new RegExp(regex).test(tabURL)); - if (new RegExp(regex).test(tabURL)) { + for (const regex of computeAllowlist( + allowlist as { defaults: string[]; current: string[] } + )) { + if (new RegExp(regex).test(curURL)) { setMatched(true); setRegexItem(regex); return; } } - setRegexItem(domainToRegex(tabURL ?? '').source); + setRegexItem(domainToRegex(curURL ?? '').source); }); }); }, []); @@ -130,14 +132,14 @@ export const PopupPage = () => { await chrome.tabs.create({ url: 'https://codeium.com/profile' }); } }} - sx={{ display: (user as any)?.name ? 'flex' : 'none', float: 'right' }} + sx={{ display: user?.name ? 'flex' : 'none', float: 'right' }} > - {user && user.name ? ( + {user?.name ? ( {`Welcome, ${user.name}`} @@ -148,7 +150,7 @@ export const PopupPage = () => { )} - + {matched ? 'Current URL matches:' : 'Adding current URL to whitelist:'} @@ -175,11 +177,11 @@ export const PopupPage = () => { >