Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions chrome-extension/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';

const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
const isSafari = process.env['CLI_CEB_SAFARI'] === 'true';

/**
* @prop default_locale
Expand All @@ -17,7 +18,37 @@ const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
* @prop content_scripts
* css: ['content.css'], // public folder
*/
const manifest = {

// Manifest V2 for Safari
const manifestV2 = {
manifest_version: 2,
default_locale: 'en',
name: '__MSG_extensionName__',
version: packageJson.version,
description: '__MSG_extensionDescription__',
permissions: ['storage', 'tabs', '<all_urls>'],
options_page: 'options/index.html',
background: {
scripts: ['background.js'],
persistent: false,
},
browser_action: {
default_icon: 'icon-34.png',
},
icons: {
'128': 'icon-128.png',
},
content_scripts: [
{
matches: ['http://*/*', 'https://*/*', '<all_urls>'],
js: ['content/index.iife.js'],
},
],
web_accessible_resources: ['*.js', '*.css', '*.svg', 'icon-128.png', 'icon-34.png'],
} satisfies chrome.runtime.ManifestV2;

// Manifest V3 for Chrome/Firefox
const manifestV3 = {
manifest_version: 3,
default_locale: 'en',
name: '__MSG_extensionName__',
Expand Down Expand Up @@ -55,4 +86,6 @@ const manifest = {
],
} satisfies chrome.runtime.ManifestV3;

export default manifest;
const manifest = isSafari ? manifestV2 : manifestV3;

export default manifest as chrome.runtime.ManifestV3 | chrome.runtime.ManifestV2;
55 changes: 43 additions & 12 deletions chrome-extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ chrome.runtime.onInstalled.addListener(async () => {
);
});

// Track extension icon pinning action
// Track extension icon pinning action (V3 only)
// This API is not available in V2
if (chrome.action && chrome.action.onUserSettingsChanged) {
chrome.action.onUserSettingsChanged.addListener(async userSettings => {
if (userSettings.isOnToolbar !== undefined) {
Expand All @@ -22,20 +23,50 @@ if (chrome.action && chrome.action.onUserSettingsChanged) {
});
}

self.addEventListener('unhandledrejection', e => {
e.preventDefault();
console.log(e);
});
// Handle unhandled promise rejections for both V2 and V3
if (typeof self !== 'undefined' && self.addEventListener) {
// V3 Service Worker
self.addEventListener('unhandledrejection', e => {
e.preventDefault();
console.log(e);
});
} else if (typeof window !== 'undefined' && window.addEventListener) {
// V2 Background Page
window.addEventListener('unhandledrejection', e => {
e.preventDefault();
console.log(e);
});
}

// Handle action icon click to toggle popup in content script
chrome.action.onClicked.addListener(() => {
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
const tab = tabs[0];
if (tab?.id && tab.url && !isInternalUrl(tab.url)) {
chrome.tabs.sendMessage(tab.id, { action: 'toggle-popup' });
}
// Support both V2 (browserAction) and V3 (action)
const actionAPI =
chrome.action ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(chrome as any).browserAction;
if (actionAPI && actionAPI.onClicked) {
actionAPI.onClicked.addListener(() => {
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
const tab = tabs[0];
if (tab?.id && tab.url && !isInternalUrl(tab.url)) {
const tabId = tab.id;
chrome.tabs.sendMessage(tabId, { action: 'toggle-popup' }, () => {
// Check if there was an error sending the message
const lastError = chrome.runtime.lastError;
if (lastError) {
// If the receiving end does not exist (content script not loaded),
// reload the tab to inject the content script
if (lastError.message?.includes('Receiving end does not exist')) {
chrome.tabs.reload(tabId);
} else {
console.error('Error sending message to content script:', lastError);
}
}
});
}
});
});
});
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'collect') {
Expand Down
4 changes: 2 additions & 2 deletions chrome-extension/utils/plugins/make-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { platform } from 'node:process';
import type { Manifest } from '@extension/dev-utils';
import { colorLog, ManifestParser } from '@extension/dev-utils';
import type { PluginOption } from 'vite';
import { IS_DEV, IS_FIREFOX } from '@extension/env';
import { IS_DEV, IS_FIREFOX, IS_SAFARI } from '@extension/env';

const manifestFile = resolve(import.meta.dirname, '..', '..', 'manifest.js');
const refreshFilePath = resolve(
Expand Down Expand Up @@ -51,7 +51,7 @@ export default (config: { outDir: string }): PluginOption => {
addRefreshContentScript(manifest);
}

writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX));
writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX, IS_SAFARI));

const refreshFileString = readFileSync(refreshFilePath, 'utf-8');

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
"base-build": "pnpm clean:bundle && turbo build",
"build": "pnpm set-global-env && pnpm base-build",
"build:firefox": "pnpm set-global-env CLI_CEB_FIREFOX=true && pnpm base-build",
"build:safari": "pnpm set-global-env CLI_CEB_SAFARI=true && pnpm base-build",
"base-dev": "pnpm clean:bundle && turbo ready && turbo watch dev --concurrency 20",
"dev": "pnpm set-global-env CLI_CEB_DEV=true && pnpm base-dev",
"dev:firefox": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_FIREFOX=true && pnpm base-dev",
"dev:safari": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_SAFARI=true && pnpm base-dev",
"build:eslint": "tsc -b",
"zip": "pnpm build && pnpm -F zipper zip",
"zip:firefox": "pnpm build:firefox && pnpm -F zipper zip",
"zip:safari": "pnpm build:safari && pnpm -F zipper zip",
"lint": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache",
"lint:fix": "turbo lint:fix --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache",
"prettier": "turbo prettier --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
Expand Down
9 changes: 6 additions & 3 deletions packages/dev-utils/lib/manifest-parser/impl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Manifest, ManifestParserInterface } from './types.js';

export const ManifestParserImpl: ManifestParserInterface = {
convertManifestToString: (manifest, isFirefox) => {
if (isFirefox) {
convertManifestToString: (manifest, isFirefox, isSafari = false) => {
// Safari uses Manifest V2 directly from manifest.ts, no conversion needed
// Firefox needs specific conversions for V3
if (isFirefox && !isSafari) {
manifest = convertToFirefoxCompatibleManifest(manifest);
}

Expand All @@ -15,7 +17,8 @@ const convertToFirefoxCompatibleManifest = (manifest: Manifest) => {
...manifest,
} as { [key: string]: unknown };

if (manifest.background?.service_worker) {
// Only convert if it's a V3 manifest with service_worker
if (manifest.background && 'service_worker' in manifest.background && manifest.background.service_worker) {
manifestCopy.background = {
scripts: [manifest.background.service_worker],
type: 'module',
Expand Down
4 changes: 2 additions & 2 deletions packages/dev-utils/lib/manifest-parser/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type Manifest = chrome.runtime.ManifestV3;
export type Manifest = chrome.runtime.ManifestV3 | chrome.runtime.ManifestV2;

export interface ManifestParserInterface {
convertManifestToString: (manifest: Manifest, isFirefox: boolean) => string;
convertManifestToString: (manifest: Manifest, isFirefox: boolean, isSafari?: boolean) => string;
}
1 change: 1 addition & 0 deletions packages/env/lib/const.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const IS_DEV = process.env['CLI_CEB_DEV'] === 'true';
export const IS_PROD = !IS_DEV;
export const IS_FIREFOX = process.env['CLI_CEB_FIREFOX'] === 'true';
export const IS_SAFARI = process.env['CLI_CEB_SAFARI'] === 'true';
export const IS_CI = process.env['CEB_CI'] === 'true';
96 changes: 64 additions & 32 deletions packages/shared/lib/utils/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,49 +46,81 @@ export function axios(
};
}
params.headers['From'] = 'extension';

// Set up timeout using AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);

const options: RequestInit = {
body: params.body,
redirect: 'manual',
headers: params.headers,
method: params.method || 'GET',
credentials: 'include',
signal: controller.signal,
};
return fetch(params.url, options).then(response => {
if (!response.ok) {
if (response.type === 'opaqueredirect') {
const parsedUrl = new URL(params.url);
parsedUrl.hostname = `www.${parsedUrl.hostname}`;
return fetch(parsedUrl.toString(), options).then(innerResponse => {
if (!innerResponse.ok) {
return Promise.reject(new Error(`HTTP error! status: ${innerResponse.status}`));
} else {
return innerResponse.text().then(data => {
if (!data) {
return null;

return fetch(params.url, options)
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
if (response.type === 'opaqueredirect') {
const parsedUrl = new URL(params.url);
parsedUrl.hostname = `www.${parsedUrl.hostname}`;

// Create new controller for retry request
const retryController = new AbortController();
const retryTimeoutId = setTimeout(() => retryController.abort(), 10000);
const retryOptions = { ...options, signal: retryController.signal };

return fetch(parsedUrl.toString(), retryOptions)
.then(innerResponse => {
clearTimeout(retryTimeoutId);
if (!innerResponse.ok) {
return Promise.reject(new Error(`HTTP error! status: ${innerResponse.status}`));
} else {
return innerResponse.text().then(data => {
if (!data) {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
});
}
try {
return JSON.parse(data);
} catch {
return null;
})
.catch(error => {
clearTimeout(retryTimeoutId);
if (error.name === 'AbortError') {
return Promise.reject(new Error('Request timeout after 10 seconds'));
}
throw error;
});
}
if (response.status === 401) {
chrome.storage.sync.remove(['namespaceId', 'resourceId']);
}
return Promise.reject(new Error(`HTTP error! status: ${response.status}`));
} else {
return response.text().then(data => {
if (!data) {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
});
}
if (response.status === 401) {
chrome.storage.sync.remove(['namespaceId', 'resourceId']);
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
return Promise.reject(new Error('Request timeout after 10 seconds'));
}
return Promise.reject(new Error(`HTTP error! status: ${response.status}`));
} else {
return response.text().then(data => {
if (!data) {
return null;
}
try {
return JSON.parse(data);
} catch {
return null;
}
});
}
});
throw error;
});
}
9 changes: 6 additions & 3 deletions pages/content/src/utils/zindex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ export default function zIndex() {
} else {
let value = 0;
document.body.querySelectorAll('*').forEach(item => {
const zindex = Number.parseInt(window.getComputedStyle(item, null).getPropertyValue('z-index'), 10);
if (zindex > value) {
value = zindex;
const rawZindex = window.getComputedStyle(item, null).getPropertyValue('z-index');
if (!['auto', '0'].includes(rawZindex)) {
const zindex = Number.parseInt(rawZindex, 10);
if (zindex > value) {
value = zindex;
}
}
});
if (value <= 0) {
Expand Down
6 changes: 3 additions & 3 deletions pages/content/src/widgets/popup/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ export default function Header(props: IProps) {
const { baseUrl, namespaceId } = props;
const { container } = useApp();
const [target, onTarget] = useState<HTMLElement | null>(null);
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const handleNamespace = () => {
chrome.runtime.sendMessage({
action: 'create-tab',
url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/${namespaceId}/chat`,
url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/${namespaceId}/chat?lang=${i18n.language}`,
});
};
const handleFeedback = () => {
chrome.runtime.sendMessage({
action: 'create-tab',
url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/feedback`,
url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/feedback?lang=${i18n.language}`,
});
};
const handleSetting = () => {
Expand Down
15 changes: 13 additions & 2 deletions pages/content/src/widgets/popup/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Collect from './Collect';
import { useEffect } from 'react';
import { Wrapper } from './Wrapper';
import { Section } from './Section';
import { Loader2 } from 'lucide-react';
import { useUser } from '@src/hooks/useUser';
import type { Response } from '@extension/shared';

Expand All @@ -14,7 +15,7 @@ interface IProps extends Response {

export function Page(props: IProps) {
const { onPopup, data, loading, onChange } = props;
const { user } = useUser({ baseUrl: loading ? '' : data.apiBaseUrl });
const { user, loading: userLoading } = useUser({ baseUrl: loading ? '' : data.apiBaseUrl });

useEffect(() => {
if (loading || !user.id || !data.apiBaseUrl) {
Expand All @@ -25,7 +26,7 @@ export function Page(props: IProps) {
chrome.runtime.sendMessage(
{
action: 'fetch',
url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/api/v1/namespaces/${data.namespaceId}/root`,
url: `${baseUrl}/api/v1/namespaces/${data.namespaceId}/root`,
},
response => {
let match = false;
Expand Down Expand Up @@ -101,6 +102,16 @@ export function Page(props: IProps) {
);
}, [loading, data.apiBaseUrl, data.namespaceId, data.resourceId, user.id, onChange]);

if (userLoading) {
return (
<Wrapper onPopup={onPopup}>
<div className="flex items-center justify-center opacity-60">
<Loader2 className="animate-spin" />
</div>
</Wrapper>
);
}

return (
<Wrapper onPopup={onPopup}>
<Header baseUrl={data.apiBaseUrl} namespaceId={data.namespaceId} />
Expand Down
Loading