From d90b350265b89c97a57a0f06c87c0ba99d86deb2 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Sun, 14 Jun 2026 21:33:36 +0800 Subject: [PATCH 01/12] Add tab assistant side panel and model metadata overrides --- background.ts | 129 +++++ components/content/GlobalActionBar.tsx | 46 +- .../options/forms/EnhancedModelSelector.tsx | 7 + .../options/forms/GeneralSettingsForm.tsx | 38 ++ components/options/forms/LLMSettingsForm.tsx | 16 + .../options/forms/ModelSettingsButton.tsx | 25 + components/sidepanel/MessageContent.test.tsx | 48 ++ components/sidepanel/MessageContent.tsx | 73 +++ components/sidepanel/TabAssistantPanel.tsx | 455 ++++++++++++++++++ core/auth/auth-service.ts | 9 +- core/config/llm-config.test.ts | 12 + core/config/llm-config.ts | 16 +- core/content/Selectly.tsx | 95 +++- core/content/content-styles.ts | 3 + core/i18n/locales/de.ts | 41 ++ core/i18n/locales/en.ts | 41 ++ core/i18n/locales/es.ts | 42 ++ core/i18n/locales/fr.ts | 43 ++ core/i18n/locales/ja.ts | 41 ++ core/i18n/locales/pt.ts | 41 ++ core/i18n/locales/zh.ts | 39 ++ core/i18n/types.ts | 41 ++ core/services/llm-service.ts | 7 +- core/services/model-service.ts | 29 +- core/services/tab-context-service.ts | 79 +++ core/storage/collect-db.ts | 4 + core/storage/tab-chat-db.ts | 125 +++++ core/tab-context/budget.test.ts | 13 + core/tab-context/budget.ts | 46 ++ core/tab-context/extractor.ts | 243 ++++++++++ core/tab-context/model-metadata.test.ts | 24 + core/tab-context/model-metadata.ts | 27 ++ core/tab-context/prompt.test.ts | 57 +++ core/tab-context/prompt.ts | 95 ++++ core/tab-context/types.ts | 73 +++ core/tab-context/url.test.ts | 19 + core/tab-context/url.ts | 34 ++ package.json | 4 +- pnpm-lock.yaml | 60 +++ style.css | 62 +++ tabs/tab-assistant.tsx | 9 + tsconfig.tsbuildinfo | 2 +- 42 files changed, 2283 insertions(+), 30 deletions(-) create mode 100644 components/sidepanel/MessageContent.test.tsx create mode 100644 components/sidepanel/MessageContent.tsx create mode 100644 components/sidepanel/TabAssistantPanel.tsx create mode 100644 core/config/llm-config.test.ts create mode 100644 core/services/tab-context-service.ts create mode 100644 core/storage/tab-chat-db.ts create mode 100644 core/tab-context/budget.test.ts create mode 100644 core/tab-context/budget.ts create mode 100644 core/tab-context/extractor.ts create mode 100644 core/tab-context/model-metadata.test.ts create mode 100644 core/tab-context/model-metadata.ts create mode 100644 core/tab-context/prompt.test.ts create mode 100644 core/tab-context/prompt.ts create mode 100644 core/tab-context/types.ts create mode 100644 core/tab-context/url.test.ts create mode 100644 core/tab-context/url.ts create mode 100644 tabs/tab-assistant.tsx diff --git a/background.ts b/background.ts index 52124d4..1e2bb2a 100644 --- a/background.ts +++ b/background.ts @@ -16,6 +16,20 @@ import { secureStorage } from './core/storage/secure-storage'; import { createLogger } from './utils/logger'; const logger = createLogger('Background'); +let lastSidePanelTabId: number | null = null; +const TAB_ASSISTANT_SIDE_PANEL_PATH = 'tabs/tab-assistant.html'; + +const enableTabAssistantSidePanel = (tabId: number) => { + const sidePanel = chrome.sidePanel; + if (!sidePanel?.setOptions) { + return Promise.reject(new Error('Side panel options are not available in this browser')); + } + return sidePanel.setOptions({ + tabId, + path: TAB_ASSISTANT_SIDE_PANEL_PATH, + enabled: true, + }); +}; // Initialize extension on installation chrome.runtime.onInstalled.addListener(async (details) => { @@ -95,6 +109,12 @@ chrome.runtime.onStartup.addListener(async () => { } }); +chrome.tabs.onRemoved.addListener((tabId) => { + if (lastSidePanelTabId === tabId) { + lastSidePanelTabId = null; + } +}); + // Also run migration when extension is enabled/reloaded (async () => { logger.info('Extension loaded'); @@ -416,6 +436,115 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { })(); return true; } + case 'tabContext:openSidePanel': { + const tabId = sender.tab?.id; + if (!tabId) { + sendResponse({ success: false, error: 'Missing tab id' }); + return true; + } + + const sidePanel = chrome.sidePanel; + if (!sidePanel?.open) { + sendResponse({ success: false, error: 'Side panel is not available in this browser' }); + return true; + } + if (!sidePanel.setOptions) { + sendResponse({ success: false, error: 'Side panel options are not available in this browser' }); + return true; + } + + void enableTabAssistantSidePanel(tabId).catch((err: any) => { + logger.warn('Failed to enable tab assistant side panel:', err); + }); + + lastSidePanelTabId = tabId; + try { + const openPromise = sidePanel.open({ tabId }); + Promise.resolve(openPromise) + .then(() => { + sendResponse({ success: true, tabId }); + }) + .catch((err: any) => { + logger.error('Failed to open side panel:', err); + sendResponse({ success: false, error: err?.message || 'Failed to open side panel' }); + }); + } catch (err: any) { + logger.error('Failed to open side panel:', err); + sendResponse({ success: false, error: err?.message || 'Failed to open side panel' }); + } + return true; + } + case 'tabContext:prepareSidePanel': { + (async () => { + try { + const tabId = sender.tab?.id; + if (!tabId) { + sendResponse({ success: false, error: 'Missing tab id' }); + return; + } + + await enableTabAssistantSidePanel(tabId); + sendResponse({ success: true, tabId }); + } catch (err: any) { + logger.warn('Failed to prepare tab assistant side panel:', err); + sendResponse({ success: false, error: err?.message || 'Failed to prepare side panel' }); + } + })(); + return true; + } + case 'tabContext:getActiveTab': { + (async () => { + try { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); + const tab = tabs[0]; + sendResponse({ + success: true, + tab: tab + ? { + id: tab.id, + title: tab.title, + url: tab.url, + windowId: tab.windowId, + } + : lastSidePanelTabId + ? { id: lastSidePanelTabId } + : null, + }); + } catch (err: any) { + logger.warn('Failed to get active tab:', err); + sendResponse({ + success: false, + error: err?.message || 'Failed to get active tab', + tab: lastSidePanelTabId ? { id: lastSidePanelTabId } : null, + }); + } + })(); + return true; + } + case 'tabContext:capture': { + (async () => { + try { + const tabId = request.tabId as number | undefined; + if (!tabId) { + sendResponse({ success: false, error: 'Missing tab id' }); + return; + } + + const res = await chrome.tabs.sendMessage(tabId, { + action: 'tabContext:capturePage', + budget: request.budget, + }); + sendResponse(res); + } catch (err: any) { + logger.warn('Failed to capture tab context:', err); + sendResponse({ + success: false, + error: err?.message || 'This page cannot be read', + }); + } + })(); + return true; + } case 'open': chrome.tabs.create({ url: request.url }); sendResponse({ success: true }); diff --git a/components/content/GlobalActionBar.tsx b/components/content/GlobalActionBar.tsx index d141c94..c0cb447 100644 --- a/components/content/GlobalActionBar.tsx +++ b/components/content/GlobalActionBar.tsx @@ -1,19 +1,29 @@ -import { Bookmark } from 'lucide-react'; +import { Bookmark, MessageCircle } from 'lucide-react'; import { useState } from 'react'; interface GlobalActionBarProps { - onSaveProgress: () => Promise; + onOpenTabAssistant?: () => Promise; + onSaveProgress?: () => Promise; + showTabAssistant?: boolean; + showSaveProgress?: boolean; labels: { + askPage: string; saveProgress: string; progressSaved: string; }; } -export const GlobalActionBar = ({ onSaveProgress, labels }: GlobalActionBarProps) => { +export const GlobalActionBar = ({ + onOpenTabAssistant, + onSaveProgress, + showTabAssistant = true, + showSaveProgress = false, + labels, +}: GlobalActionBarProps) => { const [status, setStatus] = useState<'idle' | 'saving' | 'saved'>('idle'); const handleSave = async () => { - if (status === 'saving') return; + if (status === 'saving' || !onSaveProgress) return; setStatus('saving'); try { await onSaveProgress(); @@ -26,14 +36,26 @@ export const GlobalActionBar = ({ onSaveProgress, labels }: GlobalActionBarProps return (
- + {showTabAssistant && ( + + )} + {showSaveProgress && ( + + )}
); }; diff --git a/components/options/forms/EnhancedModelSelector.tsx b/components/options/forms/EnhancedModelSelector.tsx index 8ed39c1..335f9b9 100644 --- a/components/options/forms/EnhancedModelSelector.tsx +++ b/components/options/forms/EnhancedModelSelector.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import type { LLMProvider, ModelCallSettings } from '../../../core/config/llm-config'; import type { ModelInfo } from '../../../core/services/model-service'; import { modelService } from '../../../core/services/model-service'; +import type { ModelMetadataOverride } from '../../../core/tab-context/types'; import { createLogger } from '../../../utils/logger'; import { ModelSettingsButton } from './ModelSettingsButton'; @@ -20,6 +21,8 @@ interface EnhancedModelSelectorProps { showDefault?: boolean; modelSettings?: ModelCallSettings; onModelSettingsChange?: (settings: ModelCallSettings) => void; + selectedModelMetadata?: ModelMetadataOverride; + onSelectedModelMetadataChange?: (metadata: ModelMetadataOverride) => void; } export const EnhancedModelSelector: React.FC = ({ @@ -33,6 +36,8 @@ export const EnhancedModelSelector: React.FC = ({ showDefault = false, modelSettings, onModelSettingsChange, + selectedModelMetadata, + onSelectedModelMetadataChange, }) => { const [isOpen, setIsOpen] = useState(false); const [selectedProvider, setSelectedProvider] = useState('all'); @@ -188,9 +193,11 @@ export const EnhancedModelSelector: React.FC = ({ {onModelSettingsChange && ( )} diff --git a/components/options/forms/GeneralSettingsForm.tsx b/components/options/forms/GeneralSettingsForm.tsx index 60c1e86..42004ad 100644 --- a/components/options/forms/GeneralSettingsForm.tsx +++ b/components/options/forms/GeneralSettingsForm.tsx @@ -35,6 +35,44 @@ export const GeneralSettingsForm: React.FC = ({ +
+ +
+ +