From 319d19370f11d394ab5e809eb31eef785ff58a11 Mon Sep 17 00:00:00 2001 From: djamiirr Date: Sat, 2 Aug 2025 18:20:14 +0100 Subject: [PATCH 1/5] Attempting to add support for z.ai (GLM) --- README.md | 35 +- chrome-extension/manifest.ts | 6 + .../src/components/mcpPopover/mcpPopover.tsx | 121 +- .../src/components/sidebar/SidebarManager.tsx | 33 +- .../sidebar/base/BaseSidebarManager.tsx | 1 + pages/content/src/plugins/adapters/index.ts | 11 +- .../content/src/plugins/adapters/z.adapter.ts | 1537 +++++++++++++++++ pages/content/src/plugins/plugin-registry.ts | 121 +- pages/content/src/plugins/sidebar.plugin.ts | 29 +- .../src/render_prescript/src/core/config.ts | 9 + .../src/parser/functionParser.ts | 35 +- .../src/renderer/functionBlock.ts | 10 +- 12 files changed, 1786 insertions(+), 162 deletions(-) create mode 100644 pages/content/src/plugins/adapters/z.adapter.ts diff --git a/README.md b/README.md index 1d30d15d..e64e0bc4 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

-Brings MCP to ChatGPT, Perplexity, Grok, Gemini, Google AI Studio, OpenRouter, Kimi, Github Copilot, Mistral and more... +Brings MCP to ChatGPT, Perplexity, Z (GML), Grok, Gemini, Google AI Studio, OpenRouter, Kimi, Github Copilot, Mistral and more...

@@ -33,13 +33,14 @@ Brings MCP to ChatGPT, Perplexity, Grok, Gemini, Google AI Studio, OpenRouter, K ## Overview -MCP SuperAssistant is a Chrome extension that integrates the Model Context Protocol (MCP) tools with AI platforms like Perplexity, ChatGPT, Google Gemini, Google AI Studio, Grokand more. It allows users to execute MCP tools directly from these platforms and insert the results back into the conversation, enhancing the capabilities of web-based AI assistants. +MCP SuperAssistant is a Chrome extension that integrates the Model Context Protocol (MCP) tools with AI platforms like Perplexity, Z (GLM), ChatGPT, Google Gemini, Google AI Studio, Grokand more. It allows users to execute MCP tools directly from these platforms and insert the results back into the conversation, enhancing the capabilities of web-based AI assistants. ## Currently Supported Platforms - [ChatGPT](https://chatgpt.com/) - [Google Gemini](https://gemini.google.com/) - [Perplexity](https://perplexity.ai/) +- [Z (GLM)](https://chat.z.ai/) - [Grok](https://grok.com/) - [Google AI Studio](https://aistudio.google.com/) - [OpenRouter Chat](https://openrouter.ai/chat) @@ -64,13 +65,13 @@ The Model Context Protocol (MCP) is an open standard developed by Anthropic that ## Key Features -- **Multiple AI Platform Support**: Works with ChatGPT, Perplexity, Google Gemini, Grok, Google AI Studio, OpenRouter Chat, DeepSeek, Kagi, T3 Chat! +- **Multiple AI Platform Support**: Works with ChatGPT, Perplexity, Z (GLM), Google Gemini, Grok, Google AI Studio, OpenRouter Chat, DeepSeek, Kagi, T3 Chat! - **Plugin Architecture**: Modular plugin system with site-specific adapters for tailored platform integration - **Sidebar UI**: Clean, unobtrusive interface that integrates with the AI platform - **Tool Detection**: Automatically detects MCP tool calls in AI responses - **Tool Execution**: Execute MCP tools with a single click - **Tool Result Integration**: Seamlessly insert tool execution results back into the AI conversation -- **Render Mode**: Renders Function call and Function results. +- **Render Mode**: Renders Function call and Function results. - **Auto-Execute Mode**: Automatically execute detected tools - **Auto-Submit Mode**: Automatically submit chat input after result insertion - **Push Content Mode**: Option to push page content instead of overlaying @@ -118,31 +119,31 @@ To connect the Chrome extension to a local server for proxying connections: ```bash npx @srbhptl39/mcp-superassistant-proxy@latest --config ./mcpconfig.json ``` - - This is useful for: - - Proxying remote MCP servers - - Adding CORS support to remote servers - - Providing health endpoints for monitoring - Use existing MCP config file if available. +This is useful for: +- Proxying remote MCP servers +- Adding CORS support to remote servers +- Providing health endpoints for monitoring + +Use existing MCP config file if available. ``` macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json ``` - **Example mcpconfig.json:** +**Example mcpconfig.json:** ```json { - "mcpServers": { - "desktop-commander": { + "mcpServers": { + "desktop-commander": { "command": "npx", "args": [ - "-y", - "@wonderwhy-er/desktop-commander" + "-y", + "@wonderwhy-er/desktop-commander" ] - } - } + } } +} ``` #### Connection Steps: diff --git a/chrome-extension/manifest.ts b/chrome-extension/manifest.ts index af42ac58..3f183aa9 100644 --- a/chrome-extension/manifest.ts +++ b/chrome-extension/manifest.ts @@ -30,6 +30,7 @@ const manifest = { description: 'MCP SuperAssistant', host_permissions: [ '*://*.perplexity.ai/*', + '*://*.z.ai/*', '*://*.chat.openai.com/*', '*://*.chatgpt.com/*', '*://*.grok.com/*', @@ -77,6 +78,11 @@ const manifest = { js: ['content/index.iife.js'], run_at: 'document_idle', }, + { + matches: ['*://*.z.ai/*'], + js: ['content/index.iife.js'], + run_at: 'document_idle', + }, // Specific content script for ChatGPT tool call parsing { matches: ['*://*.chat.openai.com/*', '*://*.chatgpt.com/*'], diff --git a/pages/content/src/components/mcpPopover/mcpPopover.tsx b/pages/content/src/components/mcpPopover/mcpPopover.tsx index be079417..f6cdf48e 100644 --- a/pages/content/src/components/mcpPopover/mcpPopover.tsx +++ b/pages/content/src/components/mcpPopover/mcpPopover.tsx @@ -625,7 +625,7 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap // Use Zustand hooks for adapter and user preferences const { plugin: activePlugin, insertText, attachFile, isReady: isAdapterActive } = useCurrentAdapter(); const { preferences, updatePreferences } = useUserPreferences(); - + // Use MCP state hook to get persistent MCP toggle state const { mcpEnabled: mcpEnabledFromStore, setMCPEnabled } = useMCPState(); @@ -727,7 +727,7 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap useEffect(() => { // Initial sync setInstructions(instructionsState.instructions || ''); - + // Subscribe to changes in the global instructions state const unsubscribe = instructionsState.subscribe(newInstructions => { setInstructions(newInstructions); @@ -744,7 +744,7 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap // Force initial state sync to ensure popover reflects current persistent MCP state const currentToggleState = toggleStateManager.getState(); console.debug(`[MCPPopover] Initial state sync - toggleManager: ${currentToggleState.mcpEnabled}, store MCP: ${mcpEnabledFromStore}`); - + // Sync automation state from user preferences const syncedState = { ...currentToggleState, @@ -753,9 +753,9 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap autoSubmit: preferences.autoSubmit || false, autoExecute: preferences.autoExecute || false, }; - + setState(syncedState); - + // Also sync the legacy toggle state manager toggleStateManager.setAutoInsert(preferences.autoInsert || false); toggleStateManager.setAutoSubmit(preferences.autoSubmit || false); @@ -765,54 +765,54 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap // Handlers for toggles const handleMCP = (checked: boolean) => { console.debug(`[MCPPopover] MCP toggle changed to: ${checked}`); - + // Update the persistent MCP state in store (this will automatically control sidebar visibility) setMCPEnabled(checked, 'mcp-popover-user-toggle'); - + // Also inform the legacy toggle state manager for compatibility toggleStateManager.setMCPEnabled(checked); - + // State will be updated automatically through the MCP state effect }; const handleAutoInsert = (checked: boolean) => { console.debug(`[MCPPopover] Auto Insert toggle changed to: ${checked}`); - + // Update user preferences store updatePreferences({ autoInsert: checked }); - + // Also update legacy toggle state manager for compatibility toggleStateManager.setAutoInsert(checked); updateState(); - + // Update automation state on window for render_prescript access AutomationService.getInstance().updateAutomationStateOnWindow().catch(console.error); }; const handleAutoSubmit = (checked: boolean) => { console.debug(`[MCPPopover] Auto Submit toggle changed to: ${checked}`); - + // Update user preferences store updatePreferences({ autoSubmit: checked }); - + // Also update legacy toggle state manager for compatibility toggleStateManager.setAutoSubmit(checked); updateState(); - + // Update automation state on window for render_prescript access AutomationService.getInstance().updateAutomationStateOnWindow().catch(console.error); }; const handleAutoExecute = (checked: boolean) => { console.debug(`[MCPPopover] Auto Execute toggle changed to: ${checked}`); - + // Update user preferences store updatePreferences({ autoExecute: checked }); - + // Also update legacy toggle state manager for compatibility toggleStateManager.setAutoExecute(checked); updateState(); - + // Update automation state on window for render_prescript access AutomationService.getInstance().updateAutomationStateOnWindow().catch(console.error); }; @@ -909,40 +909,41 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap if (isAdapterActive && activePlugin && attachFile) { if (!activePlugin.capabilities.includes('file-attachment')) { - setAttachStatus('Not Supported'); - console.warn(`[MCPPopover] File attachment not supported by ${activePlugin.name} adapter`); - return; + setAttachStatus('Not Supported'); + console.warn(`[MCPPopover] File attachment not supported by ${activePlugin.name} adapter`); + return; } const isPerplexity = activePlugin.name === 'Perplexity'; + const isZ = activePlugin.name === 'Z'; const isGemini = activePlugin.name === 'Gemini'; - const fileType = isPerplexity || isGemini ? 'text/plain' : 'text/markdown'; - const fileExtension = isPerplexity || isGemini ? '.txt' : '.md'; + const fileType = isPerplexity || isGemini || isZ ? 'text/plain' : 'text/markdown'; + const fileExtension = isPerplexity || isGemini || isZ ? '.txt' : '.md'; const fileName = `mcp_superassistant_instructions${fileExtension}`; const file = new File([instructions], fileName, { type: fileType }); try { - console.debug(`[MCPPopover] Attempting to attach file using ${activePlugin.name} adapter`); - const success = await attachFile(file); - if (success) { - setAttachStatus('Attached!'); - console.debug(`[MCPPopover] File attached successfully using ${activePlugin.name} adapter`); - } else { - setAttachStatus('Error'); - console.warn(`[MCPPopover] File attachment failed using ${activePlugin.name} adapter`); - } + console.debug(`[MCPPopover] Attempting to attach file using ${activePlugin.name} adapter`); + const success = await attachFile(file); + if (success) { + setAttachStatus('Attached!'); + console.debug(`[MCPPopover] File attached successfully using ${activePlugin.name} adapter`); + } else { + setAttachStatus('Error'); + console.warn(`[MCPPopover] File attachment failed using ${activePlugin.name} adapter`); + } } catch (error) { - console.error(`[MCPPopover] Error attaching file:`, error); - setAttachStatus('Error'); + console.error(`[MCPPopover] Error attaching file:`, error); + setAttachStatus('Error'); } } else { setAttachStatus('No File'); console.warn(`[MCPPopover] Cannot attach file. isAdapterActive: ${isAdapterActive}, activePlugin: ${!!activePlugin}, attachFile: ${!!attachFile}`); if (activePlugin) { - console.warn(`[MCPPopover] Active plugin details:`, { - name: activePlugin.name, - capabilities: activePlugin.capabilities, - hasAttachFileMethod: !!activePlugin.attachFile - }); + console.warn(`[MCPPopover] Active plugin details:`, { + name: activePlugin.name, + capabilities: activePlugin.capabilities, + hasAttachFileMethod: !!activePlugin.attachFile + }); } } setTimeout(() => setAttachStatus('Attach'), 1200); @@ -954,27 +955,27 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap const rect = buttonRef.current.getBoundingClientRect(); const overlayWidth = 130; // fixed width from CSS const overlayHeight = 140; // approximate height for 3 buttons - + // Calculate position above the button let x = rect.right - overlayWidth + 10; // Align to right edge with some offset let y = rect.top - overlayHeight - 10; // Position above with gap - + // Keep within viewport bounds const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - + // Adjust horizontal position if going off screen if (x < 10) { x = 10; } else if (x + overlayWidth > viewportWidth - 10) { x = viewportWidth - overlayWidth - 10; } - + // Adjust vertical position if going off screen if (y < 10) { y = rect.bottom + 10; // Position below if not enough space above } - + setHoverOverlayPosition({ x, y }); } }, []); @@ -1036,14 +1037,14 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap useEffect(() => { if (isHoverOverlayVisible) { updateHoverOverlayPosition(); - + const handleScrollResize = () => { updateHoverOverlayPosition(); }; - + window.addEventListener('scroll', handleScrollResize, true); window.addEventListener('resize', handleScrollResize); - + return () => { window.removeEventListener('scroll', handleScrollResize, true); window.removeEventListener('resize', handleScrollResize); @@ -1073,9 +1074,9 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap const buttonContent = adapterButtonConfig?.contentClassName ? ( - MCP Logo @@ -1083,9 +1084,9 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap ) : ( <> - MCP Logo MCP @@ -1094,7 +1095,7 @@ export const MCPPopover: React.FC = ({ toggleStateManager, adap return (
-
= ({ toggleStateManager, adap {buttonContent}
- + {/* Hover overlay portal */} {isHoverOverlayVisible && createPortal( -
= ({ toggleStateManager, adap boxShadow: theme.innerShadow, }}> {instructions || ( -
- {!instructionsState.instructions - ? 'Loading instructions...' + {!instructionsState.instructions + ? 'Loading instructions...' : 'Generating instructions...' }
diff --git a/pages/content/src/components/sidebar/SidebarManager.tsx b/pages/content/src/components/sidebar/SidebarManager.tsx index 06dab055..f327de25 100644 --- a/pages/content/src/components/sidebar/SidebarManager.tsx +++ b/pages/content/src/components/sidebar/SidebarManager.tsx @@ -16,7 +16,7 @@ const getZustandPreferences = (): UserPreferences => { } catch (error) { logMessage(`[SidebarManager] Error reading Zustand store: ${error}`); } - + // Return default preferences return { autoSubmit: false, @@ -46,6 +46,7 @@ declare global { */ export class SidebarManager extends BaseSidebarManager { private static perplexityInstance: SidebarManager | null = null; + private static zInstance: SidebarManager | null = null; private static chatgptInstance: SidebarManager | null = null; private static grokInstance: SidebarManager | null = null; private static geminiInstance: SidebarManager | null = null; @@ -89,6 +90,11 @@ export class SidebarManager extends BaseSidebarManager { SidebarManager.perplexityInstance = new SidebarManager(siteType); } return SidebarManager.perplexityInstance; + case 'z': + if (!SidebarManager.zInstance) { + SidebarManager.zInstance = new SidebarManager(siteType); + } + return SidebarManager.zInstance; case 'aistudio': if (!SidebarManager.aistudioInstance) { SidebarManager.aistudioInstance = new SidebarManager(siteType); @@ -224,9 +230,9 @@ export class SidebarManager extends BaseSidebarManager { // Check if MCP is enabled from persistent state before showing sidebar const zustandState = JSON.parse(localStorage.getItem('mcp-super-assistant-ui-store') || '{}'); const mcpEnabled = zustandState.state?.mcpEnabled ?? false; - + logMessage(`[SidebarManager] MCP enabled from persisted state: ${mcpEnabled}`); - + if (mcpEnabled) { // MCP is enabled, so show the sidebar logMessage('[SidebarManager] MCP is enabled, showing sidebar'); @@ -334,7 +340,7 @@ export class SidebarManager extends BaseSidebarManager { logMessage('[SidebarManager] Final check: Re-setting window.activeSidebarManager reference before React render'); window.activeSidebarManager = this; } - + logMessage('[SidebarManager] Rendering React component with all initial state ready'); this.render(); @@ -463,8 +469,8 @@ export class SidebarManager extends BaseSidebarManager { const retryHasMargin = document.documentElement.style.marginRight !== ''; const retryHasWidth = document.documentElement.style.width !== ''; const retryComputedStyle = window.getComputedStyle(document.documentElement); - const retryMarginApplied = retryComputedStyle.marginRight === expectedMargin || - retryComputedStyle.transform.includes('translateX'); + const retryMarginApplied = retryComputedStyle.marginRight === expectedMargin || + retryComputedStyle.transform.includes('translateX'); if (retryHasClass && (retryHasMargin || retryMarginApplied)) { logMessage('[SidebarManager] Push mode successfully applied after retry'); @@ -556,7 +562,7 @@ export class SidebarManager extends BaseSidebarManager { logMessage(`[SidebarManager] Synced Zustand visibility state to: ${isVisible}`); } catch (error) { logMessage(`[SidebarManager] Error syncing Zustand visibility state: ${error}`); - + // Fallback to direct localStorage manipulation if store access fails try { const zustandState = JSON.parse(localStorage.getItem('mcp-super-assistant-ui-store') || '{}'); @@ -590,6 +596,11 @@ export class SidebarManager extends BaseSidebarManager { SidebarManager.perplexityInstance = null; } break; + case 'z': + if (SidebarManager.zInstance === this) { + SidebarManager.zInstance = null; + } + break; case 'chatgpt': if (SidebarManager.chatgptInstance === this) { SidebarManager.chatgptInstance = null; @@ -643,8 +654,8 @@ export class SidebarManager extends BaseSidebarManager { */ private isNavigationEvent(): boolean { // If we're on a supported site and the URL is still valid, this is likely navigation - return window.location.hostname === 'gemini.google.com' && - window.location.href.includes('/app'); + return window.location.hostname === 'gemini.google.com' && + window.location.href.includes('/app'); } /** @@ -655,7 +666,7 @@ export class SidebarManager extends BaseSidebarManager { logMessage(`[SidebarManager] Skipping destroy during navigation for ${this.siteType}`); return; } - + logMessage(`[SidebarManager] Performing actual destroy for ${this.siteType}`); this.destroy(); } @@ -666,7 +677,7 @@ export class SidebarManager extends BaseSidebarManager { public async hide(): Promise { // Sync Zustand store with actual visibility state when hiding this.syncZustandVisibilityState(false); - + // Call the parent hide method return super.hide(); } diff --git a/pages/content/src/components/sidebar/base/BaseSidebarManager.tsx b/pages/content/src/components/sidebar/base/BaseSidebarManager.tsx index 2659471a..1521bf4b 100644 --- a/pages/content/src/components/sidebar/base/BaseSidebarManager.tsx +++ b/pages/content/src/components/sidebar/base/BaseSidebarManager.tsx @@ -15,6 +15,7 @@ import '@src/components/sidebar/styles/sidebar.css'; */ export type SiteType = | 'perplexity' + | 'z' | 'chatgpt' | 'grok' | 'gemini' diff --git a/pages/content/src/plugins/adapters/index.ts b/pages/content/src/plugins/adapters/index.ts index 8728026c..fe3d0b65 100644 --- a/pages/content/src/plugins/adapters/index.ts +++ b/pages/content/src/plugins/adapters/index.ts @@ -1,6 +1,6 @@ /** * Adapter Plugins Export Module - * + * * This file exports all available adapter plugins for the MCP-SuperAssistant. */ @@ -16,13 +16,14 @@ export { DeepSeekAdapter } from './deepseek.adapter'; export { T3ChatAdapter } from './t3chat.adapter'; export { MistralAdapter } from './mistral.adapter'; export { GitHubCopilotAdapter } from './ghcopilot.adapter'; +export { ZAdapter } from './z.adapter'; // Export types -export type { - AdapterPlugin, - AdapterConfig, +export type { + AdapterPlugin, + AdapterConfig, PluginRegistration, AdapterCapability, - PluginContext + PluginContext } from '../plugin-types'; diff --git a/pages/content/src/plugins/adapters/z.adapter.ts b/pages/content/src/plugins/adapters/z.adapter.ts new file mode 100644 index 00000000..8dfc7d8c --- /dev/null +++ b/pages/content/src/plugins/adapters/z.adapter.ts @@ -0,0 +1,1537 @@ +import { BaseAdapterPlugin } from './base.adapter'; +import type { AdapterCapability, PluginContext } from '../plugin-types'; + +/** + * Z Adapter for Z AI (z.ai) + * + * This adapter provides specialized functionality for interacting with Z AI's + * chat interface, including text insertion, form submission, and file attachment capabilities. + * + * Migrated from the legacy adapter system to the new plugin architecture. + * Maintains compatibility with existing functionality while integrating with Zustand stores. + */ +export class ZAdapter extends BaseAdapterPlugin { + readonly name = 'ZAdapter'; + readonly version = '1.0.0'; // Incremented for new architecture + readonly hostnames = ['z.ai', 'chat.z.ai']; + readonly capabilities: AdapterCapability[] = [ + 'text-insertion', + 'form-submission', + 'file-attachment', + 'dom-manipulation' + ]; + + // CSS selectors for Z's UI elements + // Updated selectors based on current Z interface + private readonly selectors = { + // Primary chat input selectors + CHAT_INPUT: '#chat-input', + // Submit button selectors (multiple fallbacks) + SUBMIT_BUTTON: '#send-message-button, #send-message-button[type="submit"]', + // File upload related selectors + FILE_UPLOAD_BUTTON: 'button[aria-label*="More"], button[aria-label*="more"]', + FILE_INPUT: 'input[type="file"][multiple][accept*=".pdf,.docx,.doc,.xls,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg,.csv,.py,.txt,.md,.bmp,.gif"], input[type="file"][multiple]', + // Main panel and container selectors + MAIN_PANEL: 'form.w-full.flex.gap-1\.5', + // Drop zones for file attachment + DROP_ZONE: 'input[type="file"][multiple][hidden]', + // File preview elements + FILE_PREVIEW: 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(1)', + // Button insertion points (for MCP popover) - looking for search/research toggle area + BUTTON_INSERTION_CONTAINER: 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1), div.flex.relative.w-full.h-full > div > div.flex.overflow-auto.flex-col.w-full.h-full > div > div:nth-of-type(1) > div.w-full.flex.flex-col.gap-1.justify-center.items-center > div:nth-of-type(3) > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1)', + // Alternative insertion points + FALLBACK_INSERTION: '#chat-input' + }; + + // URL patterns for navigation tracking + private lastUrl: string = ''; + private urlCheckInterval: NodeJS.Timeout | null = null; + + // State management integration + private mcpPopoverContainer: HTMLElement | null = null; + private mutationObserver: MutationObserver | null = null; + private popoverCheckInterval: NodeJS.Timeout | null = null; + + // Setup state tracking + private storeEventListenersSetup: boolean = false; + private domObserversSetup: boolean = false; + private uiIntegrationSetup: boolean = false; + + // Instance tracking for debugging + private static instanceCount = 0; + private instanceId: number; + + // Style injection tracking + private adapterStylesInjected: boolean = false; + + constructor() { + super(); + ZAdapter.instanceCount++; + this.instanceId = ZAdapter.instanceCount; + console.debug(`[ZAdapter] Instance #${this.instanceId} created. Total instances: ${ZAdapter.instanceCount}`); + } + + async initialize(context: PluginContext): Promise { + // Guard against multiple initialization + if (this.currentStatus === 'initializing' || this.currentStatus === 'active') { + this.context?.logger.warn(`Z adapter instance #${this.instanceId} already initialized or active, skipping re-initialization`); + return; + } + + await super.initialize(context); + this.context.logger.debug(`Initializing Z adapter instance #${this.instanceId}...`); + + // Initialize URL tracking + this.lastUrl = window.location.href; + this.setupUrlTracking(); + + // Set up event listeners for the new architecture + this.setupStoreEventListeners(); + } + + async activate(): Promise { + // Guard against multiple activation + if (this.currentStatus === 'active') { + this.context?.logger.warn(`Z adapter instance #${this.instanceId} already active, skipping re-activation`); + return; + } + + await super.activate(); + this.context.logger.debug(`Activating Z adapter instance #${this.instanceId}...`); + + // Inject Z-specific button styles + this.injectZButtonStyles(); + + // Set up DOM observers and UI integration + this.setupDOMObservers(); + this.setupUIIntegration(); + + // Emit activation event for store synchronization + this.context.eventBus.emit('adapter:activated', { + pluginName: this.name, + timestamp: Date.now() + }); + } + + async deactivate(): Promise { + // Guard against double deactivation + if (this.currentStatus === 'inactive' || this.currentStatus === 'disabled') { + this.context?.logger.warn('Z adapter already inactive, skipping deactivation'); + return; + } + + await super.deactivate(); + this.context.logger.debug('Deactivating Z adapter...'); + + // Clean up UI integration + this.cleanupUIIntegration(); + this.cleanupDOMObservers(); + + // Reset setup flags + this.storeEventListenersSetup = false; + this.domObserversSetup = false; + this.uiIntegrationSetup = false; + + // Emit deactivation event + this.context.eventBus.emit('adapter:deactivated', { + pluginName: this.name, + timestamp: Date.now() + }); + } + + async cleanup(): Promise { + await super.cleanup(); + this.context.logger.debug('Cleaning up Z adapter...'); + + // Clear URL tracking interval + if (this.urlCheckInterval) { + clearInterval(this.urlCheckInterval); + this.urlCheckInterval = null; + } + + // Clear popover check interval + if (this.popoverCheckInterval) { + clearInterval(this.popoverCheckInterval); + this.popoverCheckInterval = null; + } + + // Remove injected adapter styles + const styleElement = document.getElementById('mcp-z-button-styles'); + if (styleElement) { + styleElement.remove(); + this.adapterStylesInjected = false; + } + + // Final cleanup + this.cleanupUIIntegration(); + this.cleanupDOMObservers(); + + // Reset all setup flags + this.storeEventListenersSetup = false; + this.domObserversSetup = false; + this.uiIntegrationSetup = false; + } + + /** + * Insert text into the Z chat input field + * Enhanced with better selector handling, event integration, and URL-specific methods + */ + async insertText(text: string, options?: { targetElement?: HTMLElement }): Promise { + this.context.logger.debug(`Attempting to insert text into Z chat input: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`); + + let targetElement: HTMLElement | null = null; + + if (options?.targetElement) { + targetElement = options.targetElement; + } else { + // Try multiple selectors for better compatibility + const selectors = this.selectors.CHAT_INPUT.split(', '); + for (const selector of selectors) { + targetElement = document.querySelector(selector.trim()) as HTMLElement; + if (targetElement) { + this.context.logger.debug(`Found chat input using selector: ${selector.trim()}`); + break; + } + } + } + + if (!targetElement) { + this.context.logger.error('Could not find Z chat input element'); + this.emitExecutionFailed('insertText', 'Chat input element not found'); + return false; + } + + try { + // Check if we're on the homepage and use the special method + const currentUrl = window.location.href; + if (currentUrl === 'https://chat.z.ai/' || currentUrl === 'https://z.ai/' || true) { + // this.context.logger.debug('Homepage detected, using InputEvent method for text insertion'); + this.context.logger.debug('Using InputEvent method for text insertion for all pages'); + return await this.insertTextViaInputEvent(targetElement, text); + } + + // // For other pages, use the existing method + // const isContentEditable = this.isContentEditableElement(targetElement); + // const originalValue = this.getElementContent(targetElement); + + // // Focus the input element + // targetElement.focus(); + + // // Insert the text by updating the value and dispatching appropriate events + // // Append the text to the original value on a new line if there's existing content + // const newContent = originalValue ? originalValue + '\n\n' + text : text; + + // if (isContentEditable) { + // (targetElement as HTMLElement).textContent = newContent; + // } else { + // (targetElement as HTMLInputElement | HTMLTextAreaElement).value = newContent; + // } + + // // Dispatch events to simulate user typing for better compatibility + // targetElement.dispatchEvent(new Event('input', { bubbles: true })); + // targetElement.dispatchEvent(new Event('change', { bubbles: true })); + + // // Emit success event to the new event system + // this.emitExecutionCompleted('insertText', { text }, { + // success: true, + // originalLength: originalValue.length, + // newLength: text.length, + // totalLength: newContent.length, + // method: 'standard' + // }); + + // this.context.logger.debug(`Text inserted successfully. Original: ${originalValue.length}, Added: ${text.length}, Total: ${newContent.length}`); + // return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.context.logger.error(`Error inserting text into Z chat input: ${errorMessage}`); + this.emitExecutionFailed('insertText', errorMessage); + return false; + } + } + + /** + * Special method for inserting text on the homepage using InputEvent + */ + private async insertTextViaInputEvent(element: HTMLElement, text: string): Promise { + try { + const originalValue = this.getElementContent(element); + + // Focus the element + element.focus(); + + // Select all existing content + const range = document.createRange(); + range.selectNodeContents(element); + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + + + // Prepare text to enter with proper line breaks + const textToEnter = originalValue ? originalValue + '\n\n' + text : text; + + // Use InputEvent instead of execCommand + element.value = textToEnter; + element.dispatchEvent(new Event('input', { bubbles: true })); + + /*element.dispatchEvent(new InputEvent('input', { + inputType: 'insertText', + data: textToEnter, + bubbles: true, + cancelable: true + }));*/ + + // Also dispatch change event for compatibility + element.dispatchEvent(new Event('change', { bubbles: true })); + + // Emit success event + this.emitExecutionCompleted('insertText', { text }, { + success: true, + originalLength: originalValue.length, + newLength: text.length, + totalLength: textToEnter.length, + }); + + this.context.logger.debug(`Text inserted successfully. Original: ${originalValue.length}, Added: ${text.length}, Total: ${textToEnter.length}`); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.context.logger.error(`InputEvent method failed: ${errorMessage}`); + this.emitExecutionFailed('insertText', `InputEvent method failed: ${errorMessage}`); + return false; + } + } + + /** + * Check if an element is contenteditable + */ + private isContentEditableElement(element: HTMLElement): boolean { + return element.isContentEditable || + element.getAttribute('contenteditable') === 'true' || + element.hasAttribute('contenteditable'); + } + + /** + * Get content from element (handles both contenteditable and input/textarea) + */ + private getElementContent(element: HTMLElement): string { + if (this.isContentEditableElement(element)) { + return element.textContent || element.innerText || ''; + } else { + return (element as HTMLInputElement | HTMLTextAreaElement).value || ''; + } + } + + /** + * Submit the current text in the Z chat input + * Enhanced with multiple selector fallbacks and better error handling + */ + async submitForm(options?: { formElement?: HTMLFormElement }): Promise { + this.context.logger.debug('Attempting to submit Z chat input'); + + // First try to find submit button + let submitButton: HTMLButtonElement | null = null; + const selectors = this.selectors.SUBMIT_BUTTON.split(', '); + + for (const selector of selectors) { + submitButton = document.querySelector(selector.trim()) as HTMLButtonElement; + if (submitButton) { + this.context.logger.debug(`Found submit button using selector: ${selector.trim()}`); + break; + } + } + + // Also check for generic button near chat input + if (!submitButton) { + const chatInput = document.querySelector(this.selectors.CHAT_INPUT) as HTMLTextAreaElement; + if (chatInput) { + submitButton = chatInput.parentElement?.querySelector('button') as HTMLButtonElement; + if (submitButton) { + this.context.logger.debug('Found submit button near chat input'); + } + } + } + + if (submitButton) { + try { + // Check if the button is disabled + const isDisabled = submitButton.disabled || + submitButton.getAttribute('disabled') !== null || + submitButton.getAttribute('aria-disabled') === 'true' || + submitButton.classList.contains('disabled'); + + if (isDisabled) { + this.context.logger.warn('Z submit button is disabled, waiting for it to be enabled'); + + // Wait for button to be enabled (with timeout) + const maxWaitTime = 5000; // 5 seconds + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime) { + await new Promise(resolve => setTimeout(resolve, 300)); + + // Re-check if button is now enabled + const stillDisabled = submitButton!.disabled || + submitButton!.getAttribute('disabled') !== null || + submitButton!.getAttribute('aria-disabled') === 'true' || + submitButton!.classList.contains('disabled'); + + if (!stillDisabled) { + break; + } + } + + // Final check + const finallyDisabled = submitButton.disabled || + submitButton.getAttribute('disabled') !== null || + submitButton.getAttribute('aria-disabled') === 'true' || + submitButton.classList.contains('disabled'); + + if (finallyDisabled) { + this.context.logger.warn('Submit button remained disabled, falling back to Enter key'); + return this.submitWithEnterKey(); + } + } + + // Check if the button is visible and clickable + const rect = submitButton.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + this.context.logger.warn('Z submit button is not visible, falling back to Enter key'); + return this.submitWithEnterKey(); + } + + // Click the submit button to send the message + submitButton.click(); + + // Emit success event to the new event system + this.emitExecutionCompleted('submitForm', { + formElement: options?.formElement?.tagName || 'unknown' + }, { + success: true, + method: 'submitButton.click', + buttonSelector: selectors.find(s => document.querySelector(s.trim()) === submitButton) + }); + + this.context.logger.debug('Z chat input submitted successfully via button click'); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.context.logger.error(`Error clicking submit button: ${errorMessage}, falling back to Enter key`); + return this.submitWithEnterKey(); + } + } else { + this.context.logger.warn('Could not find Z submit button, falling back to Enter key'); + return this.submitWithEnterKey(); + } + } + + /** + * Fallback method to submit using Enter key + */ + private async submitWithEnterKey(): Promise { + try { + const chatInput = document.querySelector(this.selectors.CHAT_INPUT) as HTMLTextAreaElement; + if (!chatInput) { + this.emitExecutionFailed('submitForm', 'Chat input element not found for Enter key fallback'); + return false; + } + + // Focus the textarea + chatInput.focus(); + + // Simulate Enter key press + const enterEvents = ['keydown', 'keypress', 'keyup']; + for (const eventType of enterEvents) { + chatInput.dispatchEvent(new KeyboardEvent(eventType, { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true + })); + } + + // Try form submission as additional fallback + const form = chatInput.closest('form') as HTMLFormElement; + if (form) { + this.context.logger.debug('Submitting form as additional fallback'); + form.dispatchEvent(new SubmitEvent('submit', { bubbles: true, cancelable: true })); + } + + this.emitExecutionCompleted('submitForm', {}, { + success: true, + method: 'enterKey+formSubmit' + }); + + this.context.logger.debug('Z chat input submitted successfully via Enter key'); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.context.logger.error(`Error submitting with Enter key: ${errorMessage}`); + this.emitExecutionFailed('submitForm', errorMessage); + return false; + } + } + + /** + * Attach a file to the Z chat input + * Enhanced with better error handling and integration with new architecture + */ + async attachFile(file: File, options?: { inputElement?: HTMLInputElement }): Promise { + this.context.logger.debug(`Attempting to attach file: ${file.name} (${file.size} bytes, ${file.type})`); + + try { + // Validate file before attempting attachment + if (!file || file.size === 0) { + this.emitExecutionFailed('attachFile', 'Invalid file: file is empty or null'); + return false; + } + + // Check if file upload is supported on current page + if (!this.supportsFileUpload()) { + this.emitExecutionFailed('attachFile', 'File upload not supported on current page'); + return false; + } + + // Method 1: Try using hidden file input element + const success1 = await this.attachFileViaInput(file); + if (success1) { + this.emitExecutionCompleted('attachFile', { + fileName: file.name, + fileType: file.type, + fileSize: file.size + }, { + success: true, + method: 'file-input' + }); + this.context.logger.debug(`File attached successfully via input: ${file.name}`); + return true; + } + + // Method 2: Fallback to drag and drop simulation + const success2 = await this.attachFileViaDragDrop(file); + if (success2) { + this.emitExecutionCompleted('attachFile', { + fileName: file.name, + fileType: file.type, + fileSize: file.size + }, { + success: true, + method: 'drag-drop' + }); + this.context.logger.debug(`File attached successfully via drag-drop: ${file.name}`); + return true; + } + + // Method 3: Try clipboard as final fallback + const success3 = await this.attachFileViaClipboard(file); + this.emitExecutionCompleted('attachFile', { + fileName: file.name, + fileType: file.type, + fileSize: file.size + }, { + success: success3, + method: 'clipboard' + }); + + if (success3) { + this.context.logger.debug(`File copied to clipboard for manual paste: ${file.name}`); + } else { + this.context.logger.warn(`All file attachment methods failed for: ${file.name}`); + } + + return success3; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.context.logger.error(`Error attaching file to Z: ${errorMessage}`); + this.emitExecutionFailed('attachFile', errorMessage); + return false; + } + } + + /** + * Method 1: Attach file via hidden file input + */ + private async attachFileViaInput(file: File): Promise { + try { + const selectors = this.selectors.FILE_INPUT.split(', '); + let fileInput: HTMLInputElement | null = null; + + for (const selector of selectors) { + fileInput = document.querySelector(selector.trim()) as HTMLInputElement; + if (fileInput) { + this.context.logger.debug(`Found file input using selector: ${selector.trim()}`); + break; + } + } + + if (!fileInput) { + this.context.logger.debug('No file input element found'); + return false; + } + + // Create a DataTransfer object and add the file + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + // Set the files property on the input element + fileInput.files = dataTransfer.files; + + // Trigger the change event to notify the application + const changeEvent = new Event('change', { bubbles: true }); + fileInput.dispatchEvent(changeEvent); + + return true; + } catch (error) { + this.context.logger.debug(`File input method failed: ${error}`); + return false; + } + } + + /** + * Method 2: Attach file via drag and drop simulation + */ + private async attachFileViaDragDrop(file: File): Promise { + try { + const chatInput = document.querySelector(this.selectors.CHAT_INPUT) as HTMLTextAreaElement; + if (!chatInput) { + this.context.logger.debug('No chat input found for drag-drop'); + return false; + } + + // Create a DataTransfer object + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + // Create custom events + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + dataTransfer: dataTransfer, + }); + + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dataTransfer, + }); + + // Prevent default on dragover to enable drop + chatInput.addEventListener('dragover', e => e.preventDefault(), { once: true }); + chatInput.dispatchEvent(dragOverEvent); + + // Simulate the drop event + chatInput.dispatchEvent(dropEvent); + + return true; + } catch (error) { + this.context.logger.debug(`Drag-drop method failed: ${error}`); + return false; + } + } + + /** + * Method 3: Copy file to clipboard as fallback + */ + private async attachFileViaClipboard(file: File): Promise { + try { + await navigator.clipboard.write([ + new ClipboardItem({ + [file.type]: file, + }), + ]); + + // Focus the textarea to make it easier to paste + const chatInput = document.querySelector(this.selectors.CHAT_INPUT) as HTMLTextAreaElement; + if (chatInput) { + chatInput.focus(); + } + + return true; + } catch (error) { + this.context.logger.debug(`Clipboard method failed: ${error}`); + return false; + } + } + + /** + * Check if the current page/URL is supported by this adapter + * Enhanced with better pattern matching and logging + */ + isSupported(): boolean | Promise { + const currentHost = window.location.hostname; + const currentUrl = window.location.href; + + this.context.logger.debug(`Checking if Z adapter supports: ${currentUrl}`); + + // Check hostname first + const isZHost = this.hostnames.some(hostname => { + if (typeof hostname === 'string') { + return currentHost.includes(hostname); + } + // hostname is RegExp if it's not a string + return (hostname as RegExp).test(currentHost); + }); + + if (!isZHost) { + this.context.logger.debug(`Host ${currentHost} not supported by Z adapter`); + return false; + } + + // Check if we're on a supported Z page + const supportedPatterns = [ + /^https:\/\/(?:chat\.)?z\.ai\/search\/.*/, // chat page + ]; + + const isSupported = supportedPatterns.some(pattern => pattern.test(currentUrl)); + + if (isSupported) { + this.context.logger.debug(`Z adapter supports current page: ${currentUrl}`); + } else { + this.context.logger.debug(`URL pattern not supported: ${currentUrl}`); + } + + return isSupported; + } + + /** + * Check if file upload is supported on the current page + * Enhanced with multiple selector checking and better detection + */ + supportsFileUpload(): boolean { + this.context.logger.debug('Checking file upload support for Z'); + + // Check for file input elements + const fileInputSelectors = this.selectors.FILE_INPUT.split(', '); + for (const selector of fileInputSelectors) { + const fileInput = document.querySelector(selector.trim()); + if (fileInput) { + this.context.logger.debug(`Found file input with selector: ${selector.trim()}`); + return true; + } + } + + // Check for file upload buttons + const uploadButtonSelectors = this.selectors.FILE_UPLOAD_BUTTON.split(', '); + for (const selector of uploadButtonSelectors) { + const uploadButton = document.querySelector(selector.trim()); + if (uploadButton) { + this.context.logger.debug(`Found upload button with selector: ${selector.trim()}`); + return true; + } + } + + // Check for drop zones + const dropZoneSelectors = this.selectors.DROP_ZONE.split(', '); + for (const selector of dropZoneSelectors) { + const dropZone = document.querySelector(selector.trim()); + if (dropZone) { + this.context.logger.debug(`Found drop zone with selector: ${selector.trim()}`); + return true; + } + } + + this.context.logger.debug('No file upload support detected'); + return false; + } + + // Private helper methods + + private setupUrlTracking(): void { + if (!this.urlCheckInterval) { + this.urlCheckInterval = setInterval(() => { + const currentUrl = window.location.href; + if (currentUrl !== this.lastUrl) { + this.context.logger.debug(`URL changed from ${this.lastUrl} to ${currentUrl}`); + + // Emit page changed event + if (this.onPageChanged) { + this.onPageChanged(currentUrl, this.lastUrl); + } + + this.lastUrl = currentUrl; + } + }, 1000); // Check every second + } + } + + // New architecture integration methods + + private setupStoreEventListeners(): void { + if (this.storeEventListenersSetup) { + this.context.logger.warn(`Store event listeners already set up for instance #${this.instanceId}, skipping`); + return; + } + + this.context.logger.debug(`Setting up store event listeners for Z adapter instance #${this.instanceId}`); + + // Listen for tool execution events from the store + this.context.eventBus.on('tool:execution-completed', (data) => { + this.context.logger.debug('Tool execution completed:', data); + // Handle auto-actions based on store state + this.handleToolExecutionCompleted(data); + }); + + // Listen for UI state changes + this.context.eventBus.on('ui:sidebar-toggle', (data) => { + this.context.logger.debug('Sidebar toggled:', data); + }); + + this.storeEventListenersSetup = true; + } + + private setupDOMObservers(): void { + if (this.domObserversSetup) { + this.context.logger.warn(`DOM observers already set up for instance #${this.instanceId}, skipping`); + return; + } + + this.context.logger.debug(`Setting up DOM observers for Z adapter instance #${this.instanceId}`); + + // Set up mutation observer to detect page changes and re-inject UI if needed + this.mutationObserver = new MutationObserver((mutations) => { + let shouldReinject = false; + + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + // Check if our MCP popover was removed + if (!document.getElementById('mcp-popover-container')) { + shouldReinject = true; + } + } + }); + + if (shouldReinject) { + // Only attempt re-injection if we can find an insertion point + const insertionPoint = this.findButtonInsertionPoint(); + if (insertionPoint) { + this.context.logger.debug('MCP popover removed, attempting to re-inject'); + this.setupUIIntegration(); + } + } + }); + + // Start observing + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + this.domObserversSetup = true; + } + + private setupUIIntegration(): void { + // Allow multiple calls for UI integration (for re-injection after page changes) + // but log it for debugging + if (this.uiIntegrationSetup) { + this.context.logger.debug(`UI integration already set up for instance #${this.instanceId}, re-injecting for page changes`); + } else { + this.context.logger.debug(`Setting up UI integration for Z adapter instance #${this.instanceId}`); + this.uiIntegrationSetup = true; + } + + // Wait for page to be ready, then inject MCP popover + this.waitForPageReady().then(() => { + this.injectMCPPopoverWithRetry(); + }).catch((error) => { + this.context.logger.warn('Failed to wait for page ready:', error); + // Don't retry if we can't find insertion point + }); + + // Set up periodic check to ensure popover stays injected + // this.setupPeriodicPopoverCheck(); + } + + private async waitForPageReady(): Promise { + return new Promise((resolve, reject) => { + let attempts = 0; + const maxAttempts = 5; // Maximum 10 seconds (20 * 500ms) + + const checkReady = () => { + attempts++; + const insertionPoint = this.findButtonInsertionPoint(); + if (insertionPoint) { + this.context.logger.debug('Page ready for MCP popover injection'); + resolve(); + } else if (attempts >= maxAttempts) { + this.context.logger.warn('Page ready check timed out - no insertion point found'); + reject(new Error('No insertion point found after maximum attempts')); + } else { + setTimeout(checkReady, 500); + } + }; + setTimeout(checkReady, 100); + }); + } + + private injectMCPPopoverWithRetry(maxRetries: number = 5): void { + const attemptInjection = (attempt: number) => { + this.context.logger.debug(`Attempting MCP popover injection (attempt ${attempt}/${maxRetries})`); + + // Check if popover already exists + if (document.getElementById('mcp-popover-container')) { + this.context.logger.debug('MCP popover already exists'); + return; + } + + // Find insertion point + const insertionPoint = this.findButtonInsertionPoint(); + if (insertionPoint) { + this.injectMCPPopover(insertionPoint); + } else if (attempt < maxRetries) { + // Retry after delay + this.context.logger.debug(`Insertion point not found, retrying in 1 second (attempt ${attempt}/${maxRetries})`); + setTimeout(() => attemptInjection(attempt + 1), 1000); + } else { + this.context.logger.warn('Failed to inject MCP popover after maximum retries'); + } + }; + + attemptInjection(1); + } + + private setupPeriodicPopoverCheck(): void { + // Check every 5 seconds if the popover is still there + if (!this.popoverCheckInterval) { + this.popoverCheckInterval = setInterval(() => { + if (!document.getElementById('mcp-popover-container')) { + // Only attempt re-injection if we can find an insertion point + const insertionPoint = this.findButtonInsertionPoint(); + if (insertionPoint) { + this.context.logger.debug('MCP popover missing, attempting to re-inject'); + this.injectMCPPopoverWithRetry(3); // Fewer retries for periodic checks + } + } + }, 5000); + } + } + + private cleanupDOMObservers(): void { + this.context.logger.debug('Cleaning up DOM observers for Z adapter'); + + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + } + + private cleanupUIIntegration(): void { + this.context.logger.debug('Cleaning up UI integration for Z adapter'); + + // Remove MCP popover if it exists + const popoverContainer = document.getElementById('mcp-popover-container'); + if (popoverContainer) { + popoverContainer.remove(); + } + + this.mcpPopoverContainer = null; + } + + private handleToolExecutionCompleted(data: any): void { + this.context.logger.debug('Handling tool execution completion in Z adapter:', data); + + // Use the base class method to check if we should handle events + if (!this.shouldHandleEvents()) { + this.context.logger.debug('Z adapter should not handle events, ignoring tool execution event'); + return; + } + + // Get current UI state from stores to determine auto-actions + const uiState = this.context.stores.ui; + if (uiState && data.execution) { + // Handle auto-insert, auto-submit based on store state + // This integrates with the new architecture's state management + this.context.logger.debug('Tool execution handled with new architecture integration'); + } + } + + private findButtonInsertionPoint(): { container: Element; insertAfter: Element | null } | null { + this.context.logger.debug('Finding button insertion point for MCP popover'); + + // Try to find the search/research toggle area first (primary insertion point) + const radioGroup = document.querySelector(this.selectors.BUTTON_INSERTION_CONTAINER); + if (radioGroup) { + const container = radioGroup.closest('div.flex'); + if (container) { + this.context.logger.debug('Found Tools container, placing MCP button next to it'); + const wrapperDiv = radioGroup.parentElement; + return { container, insertAfter: wrapperDiv }; + } + } + + // Fallback: Look for the main input area's action buttons container + const actionsContainer = document.querySelector('div.flex.items-end.gap-sm'); + if (actionsContainer) { + this.context.logger.debug('Found actions container (fallback)'); + const fileUploadButton = actionsContainer.querySelector('button[aria-label*="Attach"]'); + return { container: actionsContainer, insertAfter: fileUploadButton }; + } + + // Try fallback selectors + const fallbackSelectors = [ + '.input-area .actions', + '.chat-input-actions', + '.conversation-input .actions' + ]; + + for (const selector of fallbackSelectors) { + const container = document.querySelector(selector); + if (container) { + this.context.logger.debug(`Found fallback insertion point: ${selector}`); + return { container, insertAfter: null }; + } + } + + this.context.logger.debug('Could not find suitable insertion point for MCP popover'); + return null; + } + + private injectMCPPopover(insertionPoint: { container: Element; insertAfter: Element | null }): void { + this.context.logger.debug('Injecting MCP popover into Z interface'); + + try { + // Check if popover already exists + if (document.getElementById('mcp-popover-container')) { + this.context.logger.debug('MCP popover already exists, skipping injection'); + return; + } + + // Create container for the popover + const reactContainer = document.createElement('div'); + reactContainer.id = 'mcp-popover-container'; + reactContainer.style.display = 'inline-block'; + reactContainer.style.margin = '0 4px'; + + // Insert at appropriate location + const { container, insertAfter } = insertionPoint; + if (insertAfter && insertAfter.parentNode === container) { + container.insertBefore(reactContainer, insertAfter.nextSibling); + this.context.logger.debug('Inserted popover container after specified element'); + } else { + container.appendChild(reactContainer); + this.context.logger.debug('Appended popover container to container element'); + } + + // Store reference + this.mcpPopoverContainer = reactContainer; + + // Render the React MCP Popover using the new architecture + this.renderMCPPopover(reactContainer); + + this.context.logger.debug('MCP popover injected and rendered successfully'); + } catch (error) { + this.context.logger.error('Failed to inject MCP popover:', error); + } + } + + private renderMCPPopover(container: HTMLElement): void { + this.context.logger.debug('Rendering MCP popover with new architecture integration'); + + try { + // Import React and ReactDOM dynamically to avoid bundling issues + import('react').then(React => { + import('react-dom/client').then(ReactDOM => { + import('../../components/mcpPopover/mcpPopover').then(({ MCPPopover }) => { + // Create toggle state manager that integrates with new stores + const toggleStateManager = this.createToggleStateManager(); + + // Create adapter button configuration + const adapterButtonConfig = { + className: 'mcp-z-button-base', + contentClassName: 'mcp-z-button-content', + textClassName: 'mcp-z-button-text', + activeClassName: 'mcp-button-active' + }; + + // Create React root and render + const root = ReactDOM.createRoot(container); + root.render( + React.createElement(MCPPopover, { + toggleStateManager: toggleStateManager, + adapterButtonConfig: adapterButtonConfig, + adapterName: this.name + }) + ); + + this.context.logger.debug('MCP popover rendered successfully with new architecture'); + }).catch(error => { + this.context.logger.error('Failed to import MCPPopover component:', error); + }); + }).catch(error => { + this.context.logger.error('Failed to import ReactDOM:', error); + }); + }).catch(error => { + this.context.logger.error('Failed to import React:', error); + }); + } catch (error) { + this.context.logger.error('Failed to render MCP popover:', error); + } + } + + private createToggleStateManager() { + const context = this.context; + const adapterName = this.name; + + // Create the state manager object + const stateManager = { + getState: () => { + try { + // Get state from UI store - MCP enabled state should be the persistent MCP toggle state + const uiState = context.stores.ui; + + // Get the persistent MCP enabled state and other preferences + const mcpEnabled = uiState?.mcpEnabled ?? false; + const autoSubmitEnabled = uiState?.preferences?.autoSubmit ?? false; + + context.logger.debug(`Getting MCP toggle state: mcpEnabled=${mcpEnabled}, autoSubmit=${autoSubmitEnabled}`); + + return { + mcpEnabled: mcpEnabled, // Use the persistent MCP state + autoInsert: autoSubmitEnabled, + autoSubmit: autoSubmitEnabled, + autoExecute: false // Default for now, can be extended + }; + } catch (error) { + context.logger.error('Error getting toggle state:', error); + // Return safe defaults in case of error + return { + mcpEnabled: false, + autoInsert: false, + autoSubmit: false, + autoExecute: false + }; + } + }, + + setMCPEnabled: (enabled: boolean) => { + context.logger.debug(`Setting MCP ${enabled ? 'enabled' : 'disabled'} - controlling sidebar visibility via MCP state`); + + try { + // Primary method: Control MCP state through UI store (which will automatically control sidebar) + if (context.stores.ui?.setMCPEnabled) { + context.stores.ui.setMCPEnabled(enabled, 'mcp-popover-toggle'); + context.logger.debug(`MCP state set to: ${enabled} via UI store`); + } else { + context.logger.warn('UI store setMCPEnabled method not available'); + + // Fallback: Control sidebar visibility directly if MCP state setter not available + if (context.stores.ui?.setSidebarVisibility) { + context.stores.ui.setSidebarVisibility(enabled, 'mcp-popover-toggle-fallback'); + context.logger.debug(`Sidebar visibility set to: ${enabled} via UI store fallback`); + } + } + + // Secondary method: Control through global sidebar manager as additional safeguard + const sidebarManager = (window as any).activeSidebarManager; + if (sidebarManager) { + if (enabled) { + context.logger.debug('Showing sidebar via activeSidebarManager'); + sidebarManager.show().catch((error: any) => { + context.logger.error('Error showing sidebar:', error); + }); + } else { + context.logger.debug('Hiding sidebar via activeSidebarManager'); + sidebarManager.hide().catch((error: any) => { + context.logger.error('Error hiding sidebar:', error); + }); + } + } else { + context.logger.warn('activeSidebarManager not available on window - will rely on UI store only'); + } + + context.logger.debug(`MCP toggle completed: MCP ${enabled ? 'enabled' : 'disabled'}, sidebar ${enabled ? 'shown' : 'hidden'}`); + } catch (error) { + context.logger.error('Error in setMCPEnabled:', error); + } + + stateManager.updateUI(); + }, + + setAutoInsert: (enabled: boolean) => { + context.logger.debug(`Setting Auto Insert ${enabled ? 'enabled' : 'disabled'}`); + + // Update preferences through store + if (context.stores.ui?.updatePreferences) { + context.stores.ui.updatePreferences({ autoSubmit: enabled }); + } + + stateManager.updateUI(); + }, + + setAutoSubmit: (enabled: boolean) => { + context.logger.debug(`Setting Auto Submit ${enabled ? 'enabled' : 'disabled'}`); + + // Update preferences through store + if (context.stores.ui?.updatePreferences) { + context.stores.ui.updatePreferences({ autoSubmit: enabled }); + } + + stateManager.updateUI(); + }, + + setAutoExecute: (enabled: boolean) => { + context.logger.debug(`Setting Auto Execute ${enabled ? 'enabled' : 'disabled'}`); + // Can be extended to handle auto execute functionality + stateManager.updateUI(); + }, + + updateUI: () => { + context.logger.debug('Updating MCP popover UI'); + + // Dispatch custom event to update the popover + const popoverContainer = document.getElementById('mcp-popover-container'); + if (popoverContainer) { + const currentState = stateManager.getState(); + const event = new CustomEvent('mcp:update-toggle-state', { + detail: { toggleState: currentState } + }); + popoverContainer.dispatchEvent(event); + } + } + }; + + return stateManager; + } + + /** + * Public method to manually inject MCP popover (for debugging or external calls) + */ + public injectMCPPopoverManually(): void { + this.context.logger.debug('Manual MCP popover injection requested'); + this.injectMCPPopoverWithRetry(); + } + + /** + * Check if MCP popover is currently injected + */ + public isMCPPopoverInjected(): boolean { + return !!document.getElementById('mcp-popover-container'); + } + + private emitExecutionCompleted(toolName: string, parameters: any, result: any): void { + this.context.eventBus.emit('tool:execution-completed', { + execution: { + id: this.generateCallId(), + toolName, + parameters, + result, + timestamp: Date.now(), + status: 'success' + } + }); + } + + private emitExecutionFailed(toolName: string, error: string): void { + this.context.eventBus.emit('tool:execution-failed', { + toolName, + error, + callId: this.generateCallId() + }); + } + + private generateCallId(): string { + return `z-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + } + + /** + * Check if the sidebar is properly available after navigation + */ + private checkAndRestoreSidebar(): void { + this.context.logger.debug('Checking sidebar state after page navigation'); + + try { + // Check if there's an active sidebar manager + const activeSidebarManager = (window as any).activeSidebarManager; + + if (!activeSidebarManager) { + this.context.logger.warn('No active sidebar manager found after navigation'); + return; + } + + // Sidebar manager exists, just ensure MCP popover connection is working + this.ensureMCPPopoverConnection(); + + } catch (error) { + this.context.logger.error('Error checking sidebar state after navigation:', error); + } + } + + /** + * Ensure MCP popover is properly connected to the sidebar after navigation + */ + private ensureMCPPopoverConnection(): void { + this.context.logger.debug('Ensuring MCP popover connection after navigation'); + + try { + // Check if MCP popover is still injected + if (!this.isMCPPopoverInjected()) { + this.context.logger.debug('MCP popover missing after navigation, re-injecting'); + this.injectMCPPopoverWithRetry(3); + } else { + this.context.logger.debug('MCP popover is still present after navigation'); + } + } catch (error) { + this.context.logger.error('Error ensuring MCP popover connection:', error); + } + } + + // Event handlers - Enhanced for new architecture integration + onPageChanged?(url: string, oldUrl?: string): void { + this.context.logger.debug(`Z page changed: from ${oldUrl || 'N/A'} to ${url}`); + + // Update URL tracking + this.lastUrl = url; + + // Re-check support and re-inject UI if needed + const stillSupported = this.isSupported(); + if (stillSupported) { + // Re-inject styles on page navigation + this.adapterStylesInjected = false; + this.injectZButtonStyles(); + + // Re-setup UI integration after page change + setTimeout(() => { + this.setupUIIntegration(); + }, 1000); // Give page time to load + + // Check if sidebar exists and restore it if needed + setTimeout(() => { + this.checkAndRestoreSidebar(); + }, 1500); // Additional delay to ensure page is fully loaded + } else { + this.context.logger.warn('Page no longer supported after navigation'); + } + + // Emit page change event to stores + this.context.eventBus.emit('app:site-changed', { + site: url, + hostname: window.location.hostname + }); + } + + onHostChanged?(newHost: string, oldHost?: string): void { + this.context.logger.debug(`Z host changed: from ${oldHost || 'N/A'} to ${newHost}`); + + // Re-check if the adapter is still supported + const stillSupported = this.isSupported(); + if (!stillSupported) { + this.context.logger.warn('Z adapter no longer supported on this host/page'); + // Emit deactivation event using available event type + this.context.eventBus.emit('adapter:deactivated', { + pluginName: this.name, + timestamp: Date.now() + }); + } else { + // Re-setup for new host + this.setupUIIntegration(); + } + } + + onToolDetected?(tools: any[]): void { + this.context.logger.debug(`Tools detected in Z adapter:`, tools); + + // Forward to tool store + tools.forEach(tool => { + this.context.stores.tool?.addDetectedTool?.(tool); + }); + } + + // Z-specific button styling methods + + /** + * Get Z-specific button styles that match the platform's segmented control design system + */ + private getZButtonStyles(): string { + return ` + .mcp-z-button-base { + /* Base button styling matching Z's segmented-control design */ + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + outline: none; + cursor: pointer; + white-space: nowrap; + user-select: none; + border-radius: 8px; + height: 32px; + min-width: 36px; + padding: 0 0px; + gap: 6px; + font-size: 14px; + font-weight: 500; + border: none; + background: transparent; + transition: all 300ms ease-out; + + /* Default colors - using Z's actual theme colors */ + color: oklch(var(--text-color-200, 50.2% 0.008 106.677)); /* Inactive text */ + + /* Focus states */ + &:focus { + outline: none; + } + + /* Hover states */ + &:hover { + color: oklch(var(--text-color-100, 30.4% 0.04 213.681)); /* Active text on hover */ + } + + /* Active/selected state - matches the checked segmented control */ + &.mcp-button-active { + color: oklch(var(--text-super-color-100, 55.3% 0.086 208.538)); /* Super color for active state */ + } + + /* Active button overlay styling (matches data-state="checked" div) */ + &.mcp-button-active::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: 8px; + border: 1px solid oklch(var(--text-super-color-100, 55.3% 0.086 208.538)); + background-color: oklch(0.963 0.007 106.523); /* Light background */ + box-shadow: 0 1px 3px 0 oklch(var(--text-super-color-100, 55.3% 0.086 208.538) / 0.3); + transition: all 300ms ease-out; + opacity: 1; + } + } + + /* Dark mode support */ + @media (prefers-color-scheme: dark) { + .mcp-z-button-base { + color: oklch(var(--dark-text-color-200, 65.3% 0.005 197.042)); /* Dark mode inactive text */ + + &:hover { + color: oklch(var(--dark-text-color-100, 93% 0.003 106.451)); /* Dark mode hover text */ + } + + &.mcp-button-active { + color: oklch(var(--text-super-color-100, 55.3% 0.086 208.538)); /* Keep super color in dark mode */ + } + + &.mcp-button-active::before { + background-color: oklch(var(--lt-color-text-dark, 0.113 0.005 247.858)); /* Dark background equivalent */ + border-color: oklch(var(--text-super-color-100, 55.3% 0.086 208.538)); + box-shadow: 0 1px 3px 0 oklch(var(--text-super-color-100, 55.3% 0.086 208.538) / 0.2); + } + } + } + + .mcp-z-button-content { + /* Content container styling - matches the inner div structure */ + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + min-width: 0; + font-weight: 500; + position: relative; + z-index: 10; /* matches relative z-10 */ + height: 32px; + min-width: 36px; + padding: 4px 10px; /* matches py-xs px-2.5 equivalent */ + } + + .mcp-z-button-text { + font-size: 14px; + font-weight: 500; + line-height: 1.2; + color: inherit; /* Inherit color from parent */ + } + + /* Icon styling within button */ + .mcp-z-button-base svg, + .mcp-z-button-base img { + width: 16px; + height: 16px; + transition: all 300ms ease-out; + flex-shrink: 0; + } + + .mcp-z-button-base img { + border-radius: 50%; + margin-right: 1px; + } + + /* Integration with Z's button group layout */ + .gap-xs .mcp-z-button-base, + .gap-sm .mcp-z-button-base, + .flex.items-center .mcp-z-button-base { + margin: 0 2px; + } + + /* Special styling for group context (matches p-two flex items-center structure) */ + .p-two .mcp-z-button-base, + [class*="p-"] .mcp-z-button-base { + margin: 0 1px; + } + + /* Focus-visible styling for accessibility */ + .mcp-z-button-base:focus-visible { + outline: 2px solid oklch(var(--text-super-color-100, 55.3% 0.086 208.538)); + outline-offset: 2px; + outline-style: dashed; + } + + .mcp-z-button-base:focus-visible::before { + border-style: dashed !important; + } + + /* Responsive adjustments */ + @media (max-width: 640px) { + .mcp-z-button-base { + height: 28px; + min-width: 32px; + padding: 0 8px; + font-size: 13px; + } + + .mcp-z-button-content { + height: 28px; + min-width: 32px; + padding: 2px 8px; + } + + .mcp-z-button-base svg, + .mcp-z-button-base img { + width: 14px; + height: 14px; + } + + /* Adjust ring size for mobile */ + .mcp-z-button-base.mcp-button-active::before { + border-width: 1px; /* Keep consistent border width on mobile */ + } + } + + `; + } + + /** + * Inject Z-specific button styles into the page + */ + private injectZButtonStyles(): void { + if (this.adapterStylesInjected) return; + + try { + const styleId = 'mcp-z-button-styles'; + const existingStyles = document.getElementById(styleId); + if (existingStyles) existingStyles.remove(); + + const styleElement = document.createElement('style'); + styleElement.id = styleId; + styleElement.textContent = this.getZButtonStyles(); + document.head.appendChild(styleElement); + + this.adapterStylesInjected = true; + this.context.logger.debug('Z button styles injected successfully'); + } catch (error) { + this.context.logger.error('Failed to inject Z button styles:', error); + } + } +} \ No newline at end of file diff --git a/pages/content/src/plugins/plugin-registry.ts b/pages/content/src/plugins/plugin-registry.ts index 4aa50a08..72487b2e 100644 --- a/pages/content/src/plugins/plugin-registry.ts +++ b/pages/content/src/plugins/plugin-registry.ts @@ -1,6 +1,6 @@ // plugins/plugin-registry.ts import { eventBus } from '../events/event-bus'; -import type { EventMap } from '../events'; +import type { EventMap } from '../events'; import performanceMonitor from '../core/performance'; import globalErrorHandler from '../core/error-handler'; import { useAdapterStore } from '../stores/adapter.store'; @@ -17,6 +17,7 @@ import { GitHubCopilotAdapter } from './adapters/ghcopilot.adapter'; import { DeepSeekAdapter } from './adapters/deepseek.adapter'; import { GrokAdapter } from './adapters/grok.adapter'; import { PerplexityAdapter } from './adapters/perplexity.adapter'; +import { ZAdapter } from './adapters/z.adapter'; import { AIStudioAdapter } from './adapters/aistudio.adapter'; import { OpenRouterAdapter } from './adapters/openrouter.adapter'; import { T3ChatAdapter } from './adapters/t3chat.adapter'; @@ -101,13 +102,13 @@ class PluginRegistry { // Listen for site changes to auto-activate plugins const unsubscribeSiteChange = eventBus.on('app:site-changed', async ({ hostname }: EventMap['app:site-changed']) => { console.debug(`[PluginRegistry] Site change event received for ${hostname}, initial activation flag: ${this.isPerformingInitialActivation}`); - + // Skip if we're in the middle of initial activation to prevent race conditions if (this.isPerformingInitialActivation) { console.debug(`[PluginRegistry] Skipping site-change activation for ${hostname} (initial activation in progress)`); return; } - + console.debug(`[PluginRegistry] Site changed to ${hostname}, attempting plugin activation`); await this.activatePluginForHostname(hostname); }); @@ -135,7 +136,7 @@ class PluginRegistry { } const pluginName = plugin.name; - + if (this.plugins.has(pluginName)) { console.warn(`[PluginRegistry] Plugin ${pluginName} is already registered`); return; @@ -186,11 +187,11 @@ class PluginRegistry { const errorMessage = error instanceof Error ? error.message : 'Unknown registration error'; globalErrorHandler.handleError( error as Error, - { + { component: 'plugin-registry', - operation: 'registration', + operation: 'registration', source: '[PluginRegistry]', - details: { pluginName, error: errorMessage } + details: { pluginName, error: errorMessage } } ); eventBus.emit('adapter:error', { name: pluginName, error: errorMessage }); @@ -208,7 +209,7 @@ class PluginRegistry { } const factoryName = factory.name; - + if (this.adapterFactories.has(factoryName)) { console.warn(`[PluginRegistry] Adapter factory ${factoryName} is already registered`); return; @@ -249,18 +250,18 @@ class PluginRegistry { try { console.debug(`[PluginRegistry] Lazily initializing adapter: ${factoryName}`); - + const adapter = factoryRegistration.factory.create(); - + // Register the initialized adapter await this.register(adapter, factoryRegistration.factory.config); - + // Remove from factory registry since it's now initialized this.adapterFactories.delete(factoryName); - + console.debug(`[PluginRegistry] Successfully initialized adapter: ${factoryName}`); return adapter; - + } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown initialization error'; console.error(`[PluginRegistry] Failed to initialize adapter ${factoryName}:`, errorMessage); @@ -304,11 +305,11 @@ class PluginRegistry { const errorMessage = error instanceof Error ? error.message : 'Unknown unregistration error'; globalErrorHandler.handleError( error as Error, - { + { component: 'plugin-registry', - operation: 'unregistration', + operation: 'unregistration', source: '[PluginRegistry]', - details: { pluginName, error: errorMessage } + details: { pluginName, error: errorMessage } } ); throw error; @@ -319,12 +320,12 @@ class PluginRegistry { if (isInitialActivation) { this.isPerformingInitialActivation = true; } - + try { console.debug(`[PluginRegistry] activatePluginForHostname called for: ${hostname}${isInitialActivation ? ' (initial)' : ''}`); - + const plugin = await this.findOrInitializePluginForHostname(hostname); - + if (!plugin) { console.debug(`[PluginRegistry] No plugin found for hostname: ${hostname}`); if (this.activePlugin) { @@ -368,7 +369,7 @@ class PluginRegistry { // Activate new plugin const pluginInstance = registration.plugin; - + await performanceMonitor.time(`plugin-activation-${pluginName}`, async () => { await pluginInstance.activate(); }); @@ -397,14 +398,14 @@ class PluginRegistry { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown activation error'; - + globalErrorHandler.handleError( error as Error, - { + { component: 'plugin-registry', - operation: 'activation', + operation: 'activation', source: '[PluginRegistry]', - details: { pluginName, error: errorMessage } + details: { pluginName, error: errorMessage } } ); @@ -444,16 +445,16 @@ class PluginRegistry { } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown deactivation error'; - - globalErrorHandler.handleError(error as Error, { + + globalErrorHandler.handleError(error as Error, { component: 'plugin-registry', - operation: 'deactivation', + operation: 'deactivation', source: '[PluginRegistry]', - details: { pluginName, error: errorMessage } + details: { pluginName, error: errorMessage } }); // Force deactivation even on error this.activePlugin = null; - + eventBus.emit('adapter:error', { name: pluginName, error: errorMessage }); } } @@ -466,18 +467,18 @@ class PluginRegistry { // Only look for website-adapter type plugins, not sidebar or core-ui plugins for (const [, registration] of this.plugins) { const { plugin, config } = registration; - + if (!config.enabled) { console.debug(`[PluginRegistry] Skipping disabled plugin: ${plugin.name}`); continue; } - + // Special handling for SidebarPlugin - exclude it from hostname matching if (plugin.name === 'sidebar-plugin') { console.debug(`[PluginRegistry] Skipping sidebar plugin: ${plugin.name}`); continue; } - + // Default to 'website-adapter' if type is not specified, filter out 'sidebar' and 'core-ui' const pluginType = plugin.type || 'website-adapter'; console.debug(`[PluginRegistry] Checking plugin: ${plugin.name} (type: ${pluginType})`); @@ -529,7 +530,7 @@ class PluginRegistry { for (const [, factoryRegistration] of this.adapterFactories) { const { factory } = factoryRegistration; console.debug(`[PluginRegistry] Checking factory: ${factory.name} (type: ${factory.type}) with hostnames:`, factory.hostnames); - + if (factory.type !== 'website-adapter') { console.debug(`[PluginRegistry] Skipping factory ${factory.name} - not a website-adapter (type: ${factory.type})`); continue; // Filter by plugin type @@ -572,7 +573,7 @@ class PluginRegistry { private async findOrInitializePluginForHostname(hostname: string): Promise { console.debug(`[PluginRegistry] findOrInitializePluginForHostname called with: ${hostname}`); - + // First check for already initialized plugins let plugin = this.findPluginForHostname(hostname); if (plugin) { @@ -597,7 +598,7 @@ class PluginRegistry { private validatePlugin(plugin: AdapterPlugin): void { const required = ['name', 'version', 'hostnames', 'capabilities', 'initialize', 'activate', 'deactivate', 'cleanup']; - + for (const prop of required) { if (!(prop in plugin)) { throw new Error(`Plugin missing required property: ${prop}`); @@ -617,14 +618,14 @@ class PluginRegistry { throw new Error(`Plugin has invalid type: ${plugin.type}`); } - // Optional core methods like insertText and submitForm are checked by their usage + // Optional core methods like insertText and submitForm are checked by their usage // or specific capability declarations rather than a blanket requirement here, // as they are optional in the AdapterPlugin interface. } private validateAdapterFactory(factory: AdapterFactory): void { const required = ['name', 'version', 'type', 'hostnames', 'capabilities', 'create']; - + for (const prop of required) { if (!(prop in factory)) { throw new Error(`Adapter factory missing required property: ${prop}`); @@ -670,7 +671,7 @@ class PluginRegistry { private async registerBuiltInAdapters(): Promise { try { - // Register Remote Config Plugin (core extension functionality) - EAGERLY INITIALIZED + // Register Remote Config Plugin (core extension functionality) - EAGERLY INITIALIZED const remoteConfigPlugin = new RemoteConfigPlugin(); await this.register(remoteConfigPlugin, { id: 'remote-config-plugin', @@ -837,6 +838,28 @@ class PluginRegistry { }, }); + // Register ZAdapter factory for Perplexity AI + this.registerAdapterFactory({ + name: 'z-adapter', + version: '2.0.0', + type: 'website-adapter', + hostnames: ['z.ai'], + capabilities: ['text-insertion', 'form-submission', 'file-attachment'], + create: () => new ZAdapter(), + config: { + id: 'z-adapter', + name: 'Z Adapter', + description: 'Specialized adapter for Z (GLM) AI with chat input, form submission, and file attachment support', + version: '1.0.0', + enabled: true, + priority: 5, + settings: { + logLevel: 'info', + urlCheckInterval: 1000, + }, + }, + }); + // Register AIStudioAdapter factory for Google AI Studio this.registerAdapterFactory({ name: 'aistudio-adapter', @@ -947,7 +970,7 @@ class PluginRegistry { }, }, }); - + console.debug(`[PluginRegistry] Successfully registered SidebarPlugin (initialized) and ${this.adapterFactories.size} adapter factories (lazy)`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown registration error'; @@ -997,7 +1020,7 @@ class PluginRegistry { const updatedConfig = { ...registration.config, ...config }; registration.config = updatedConfig; - + eventBus.emit('plugin:config-updated', { name: pluginName, config: updatedConfig, timestamp: Date.now() }); } @@ -1024,7 +1047,7 @@ class PluginRegistry { try { // Deactivate current plugin await this.deactivateCurrentPlugin(); - + // Cleanup all registered plugins for (const [name, registration] of this.plugins.entries()) { try { @@ -1035,11 +1058,11 @@ class PluginRegistry { console.error(`[PluginRegistry] Failed to cleanup plugin ${name}:`, errorMessage); } } - + // Clear plugins map and factories map this.plugins.clear(); this.adapterFactories.clear(); - + // Run context cleanup functions if (this.context?.cleanupFunctions) { for (const cleanup of this.context.cleanupFunctions) { @@ -1051,12 +1074,12 @@ class PluginRegistry { } this.context.cleanupFunctions = []; } - + this.isInitialized = false; this.activePlugin = null; this.context = null; this.initializationPromise = null; - + console.debug('[PluginRegistry] Cleanup completed'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown cleanup error'; @@ -1103,7 +1126,7 @@ export async function initializePluginRegistry(): Promise { resolve(element); return; } - + const observer = new MutationObserver(() => { const element = root.querySelector(selector) as HTMLElement; if (element) { @@ -1111,9 +1134,9 @@ export async function initializePluginRegistry(): Promise { resolve(element); } }); - + observer.observe(root, { childList: true, subtree: true }); - + setTimeout(() => { observer.disconnect(); resolve(null); @@ -1167,7 +1190,7 @@ export async function initializePluginRegistry(): Promise { }, cleanupFunctions: [], }; - + await pluginRegistry.initialize(context); } diff --git a/pages/content/src/plugins/sidebar.plugin.ts b/pages/content/src/plugins/sidebar.plugin.ts index 90d78d38..96612dc9 100644 --- a/pages/content/src/plugins/sidebar.plugin.ts +++ b/pages/content/src/plugins/sidebar.plugin.ts @@ -5,7 +5,7 @@ import { useUIStore } from '../stores/ui.store'; /** * SidebarPlugin - Manages the sidebar as a plugin in the new architecture - * + * * This plugin: * - Automatically shows the sidebar when the page loads * - Integrates with Zustand stores and event system @@ -26,12 +26,12 @@ export class SidebarPlugin implements AdapterPlugin { async initialize(context: PluginContext): Promise { this.context = context; - + context.logger.debug('[SidebarPlugin] Initializing sidebar plugin...'); - + // Set up event listeners for sidebar management this.setupEventListeners(); - + context.logger.debug('[SidebarPlugin] Sidebar plugin initialized successfully'); } @@ -43,7 +43,7 @@ export class SidebarPlugin implements AdapterPlugin { const hostname = window.location.hostname; const pathname = window.location.pathname; - + // Handle all GitHub domains if (hostname === 'github.com' || hostname.endsWith('.github.com')) { // Only support GitHub Copilot pages @@ -58,19 +58,19 @@ export class SidebarPlugin implements AdapterPlugin { try { // Initialize sidebar manager for current site await this.initializeSidebarManager(); - + // Show sidebar automatically on activation await this.showSidebar(); - + this.isActive = true; this.context?.logger.debug('[SidebarPlugin] Sidebar plugin activated successfully'); - + // Emit activation event this.context?.eventBus.emit('plugin:activated', { pluginName: this.name, timestamp: Date.now() }); - + } catch (error) { this.context?.logger.error('[SidebarPlugin] Failed to activate:', error); throw error; @@ -237,19 +237,19 @@ export class SidebarPlugin implements AdapterPlugin { try { this.context?.logger.debug('[SidebarPlugin] Initializing sidebar manager...'); - + // Determine site type from current hostname const hostname = window.location.hostname; const siteType = this.determineSiteType(hostname); - + // Create sidebar manager instance this.sidebarManager = SidebarManager.getInstance(siteType); - + // Expose sidebar manager globally for backward compatibility (window as any).activeSidebarManager = this.sidebarManager; - + this.context?.logger.debug(`[SidebarPlugin] Sidebar manager initialized for site type: ${siteType}`); - + } catch (error) { this.context?.logger.error('[SidebarPlugin] Failed to initialize sidebar manager:', error); throw error; @@ -259,6 +259,7 @@ export class SidebarPlugin implements AdapterPlugin { private determineSiteType(hostname: string): SiteType { // Map hostnames to site types (same logic as legacy adapters) if (hostname.includes('perplexity.ai')) return 'perplexity'; + if (hostname.includes('z.ai')) return 'z'; if (hostname.includes('chatgpt.com') || hostname.includes('chat.openai.com')) return 'chatgpt'; if (hostname.includes('x.ai') || hostname.includes('grok')) return 'grok'; if (hostname.includes('gemini.google.com')) return 'gemini'; diff --git a/pages/content/src/render_prescript/src/core/config.ts b/pages/content/src/render_prescript/src/core/config.ts index 721bc972..f97bf6d4 100644 --- a/pages/content/src/render_prescript/src/core/config.ts +++ b/pages/content/src/render_prescript/src/core/config.ts @@ -76,6 +76,15 @@ export const WEBSITE_CONFIGS: Array<{ function_result_selector: ['div.group\\/query', '.group\\/query', 'div[class*="group/query"]'], }, }, + { + urlPattern: 'z', + config: { + targetSelectors: ['.cm-editor'], + streamingContainerSelectors: ['.cm-content'], + function_result_selector: ['div.chat-user'], + updateThrottle: 900, + }, + }, { urlPattern: 'gemini', config: { diff --git a/pages/content/src/render_prescript/src/parser/functionParser.ts b/pages/content/src/render_prescript/src/parser/functionParser.ts index aebc9d61..7e20da14 100644 --- a/pages/content/src/render_prescript/src/parser/functionParser.ts +++ b/pages/content/src/render_prescript/src/parser/functionParser.ts @@ -1,5 +1,33 @@ -import type { FunctionInfo } from '../core/types'; import { extractLanguageTag } from './languageParser'; +import { CONFIG, FunctionInfo } from '../core'; + + +export function startsWithFunctionCalls(str: string | null): boolean { + if (str == null) { + return false; + } + if (CONFIG.targetSelectors.includes('.cm-editor')) { + const regex = /^.*?(?:›⌄⌄\s*)?\n?\s*') || str.includes(' { // Check for any signs of function call content if ( + !startsWithFunctionCalls(content) && !content.includes('<') && - !content.includes('') && - !content.includes('') && !content.includes(' { const contentToExamine = langTagResult.content || content; // Check for Claude Opus style function calls - if (contentToExamine.includes('') || contentToExamine.includes(' { - const setupAutoExecution = () => { + const setupAutoExecution = () => { const attempts = executionTracker.incrementAttempts(blockId); if (attempts > MAX_AUTO_EXECUTE_ATTEMPTS) { @@ -982,6 +982,12 @@ export const renderFunctionCall = (block: HTMLPreElement, isProcessingRef: { cur const functionInfo = containsFunctionCalls(block); + if (functionInfo.hasFunctionCalls && CONFIG.targetSelectors.includes('.cm-editor')) { + if (block && block.querySelector('.cm-scroller')) { + block.querySelector('.cm-scroller')?.style.setProperty('display', 'none', 'important'); + } + } + // Early exit checks if (!functionInfo.hasFunctionCalls || block.closest('.function-block')) { return false; From e833cce2dbada3574952d17b9d3c3d1252757c50 Mon Sep 17 00:00:00 2001 From: djamiirr Date: Sun, 10 Aug 2025 19:36:04 +0100 Subject: [PATCH 2/5] Added long text generation support --- .../content/src/plugins/adapters/z.adapter.ts | 296 ++++++++++-------- .../content/src/render_prescript/prescript.js | 5 +- .../src/render_prescript/src/core/config.ts | 4 +- .../src/observer/functionResultObserver.ts | 7 +- .../src/observer/mutationObserver.ts | 13 +- .../src/observer/streamObserver.ts | 23 +- .../src/parser/functionParser.ts | 42 +-- .../src/renderer/functionBlock.ts | 19 +- pages/content/src/utils/helpers.ts | 44 +++ 9 files changed, 265 insertions(+), 188 deletions(-) diff --git a/pages/content/src/plugins/adapters/z.adapter.ts b/pages/content/src/plugins/adapters/z.adapter.ts index 8dfc7d8c..108c9545 100644 --- a/pages/content/src/plugins/adapters/z.adapter.ts +++ b/pages/content/src/plugins/adapters/z.adapter.ts @@ -30,17 +30,20 @@ export class ZAdapter extends BaseAdapterPlugin { SUBMIT_BUTTON: '#send-message-button, #send-message-button[type="submit"]', // File upload related selectors FILE_UPLOAD_BUTTON: 'button[aria-label*="More"], button[aria-label*="more"]', - FILE_INPUT: 'input[type="file"][multiple][accept*=".pdf,.docx,.doc,.xls,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg,.csv,.py,.txt,.md,.bmp,.gif"], input[type="file"][multiple]', + FILE_INPUT: + 'input[type="file"][multiple][accept*=".pdf,.docx,.doc,.xls,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg,.csv,.py,.txt,.md,.bmp,.gif"], input[type="file"][multiple]', // Main panel and container selectors MAIN_PANEL: 'form.w-full.flex.gap-1\.5', // Drop zones for file attachment DROP_ZONE: 'input[type="file"][multiple][hidden]', // File preview elements - FILE_PREVIEW: 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(1)', + FILE_PREVIEW: + 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(1)', // Button insertion points (for MCP popover) - looking for search/research toggle area - BUTTON_INSERTION_CONTAINER: 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1), div.flex.relative.w-full.h-full > div > div.flex.overflow-auto.flex-col.w-full.h-full > div > div:nth-of-type(1) > div.w-full.flex.flex-col.gap-1.justify-center.items-center > div:nth-of-type(3) > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1)', + BUTTON_INSERTION_CONTAINER: + 'div.flex.relative.w-full.h-full > div > div.px-3.pb-3 > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1), div.flex.relative.w-full.h-full > div > div.flex.overflow-auto.flex-col.w-full.h-full > div > div:nth-of-type(1) > div.w-full.flex.flex-col.gap-1.justify-center.items-center > div:nth-of-type(3) > div.w-full.font-primary > div.transparent > div > div > form > div > div:nth-of-type(2) > div:nth-of-type(1)', // Alternative insertion points - FALLBACK_INSERTION: '#chat-input' + FALLBACK_INSERTION: '#chat-input', }; // URL patterns for navigation tracking @@ -74,7 +77,9 @@ export class ZAdapter extends BaseAdapterPlugin { async initialize(context: PluginContext): Promise { // Guard against multiple initialization if (this.currentStatus === 'initializing' || this.currentStatus === 'active') { - this.context?.logger.warn(`Z adapter instance #${this.instanceId} already initialized or active, skipping re-initialization`); + this.context?.logger.warn( + `Z adapter instance #${this.instanceId} already initialized or active, skipping re-initialization`, + ); return; } @@ -177,7 +182,9 @@ export class ZAdapter extends BaseAdapterPlugin { * Enhanced with better selector handling, event integration, and URL-specific methods */ async insertText(text: string, options?: { targetElement?: HTMLElement }): Promise { - this.context.logger.debug(`Attempting to insert text into Z chat input: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`); + this.context.logger.debug( + `Attempting to insert text into Z chat input: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`, + ); let targetElement: HTMLElement | null = null; @@ -269,7 +276,6 @@ export class ZAdapter extends BaseAdapterPlugin { selection.addRange(range); } - // Prepare text to enter with proper line breaks const textToEnter = originalValue ? originalValue + '\n\n' + text : text; @@ -288,14 +294,20 @@ export class ZAdapter extends BaseAdapterPlugin { element.dispatchEvent(new Event('change', { bubbles: true })); // Emit success event - this.emitExecutionCompleted('insertText', { text }, { - success: true, - originalLength: originalValue.length, - newLength: text.length, - totalLength: textToEnter.length, - }); - - this.context.logger.debug(`Text inserted successfully. Original: ${originalValue.length}, Added: ${text.length}, Total: ${textToEnter.length}`); + this.emitExecutionCompleted( + 'insertText', + { text }, + { + success: true, + originalLength: originalValue.length, + newLength: text.length, + totalLength: textToEnter.length, + }, + ); + + this.context.logger.debug( + `Text inserted successfully. Original: ${originalValue.length}, Added: ${text.length}, Total: ${textToEnter.length}`, + ); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -309,9 +321,11 @@ export class ZAdapter extends BaseAdapterPlugin { * Check if an element is contenteditable */ private isContentEditableElement(element: HTMLElement): boolean { - return element.isContentEditable || + return ( + element.isContentEditable || element.getAttribute('contenteditable') === 'true' || - element.hasAttribute('contenteditable'); + element.hasAttribute('contenteditable') + ); } /** @@ -358,7 +372,8 @@ export class ZAdapter extends BaseAdapterPlugin { if (submitButton) { try { // Check if the button is disabled - const isDisabled = submitButton.disabled || + const isDisabled = + submitButton.disabled || submitButton.getAttribute('disabled') !== null || submitButton.getAttribute('aria-disabled') === 'true' || submitButton.classList.contains('disabled'); @@ -374,7 +389,8 @@ export class ZAdapter extends BaseAdapterPlugin { await new Promise(resolve => setTimeout(resolve, 300)); // Re-check if button is now enabled - const stillDisabled = submitButton!.disabled || + const stillDisabled = + submitButton!.disabled || submitButton!.getAttribute('disabled') !== null || submitButton!.getAttribute('aria-disabled') === 'true' || submitButton!.classList.contains('disabled'); @@ -385,7 +401,8 @@ export class ZAdapter extends BaseAdapterPlugin { } // Final check - const finallyDisabled = submitButton.disabled || + const finallyDisabled = + submitButton.disabled || submitButton.getAttribute('disabled') !== null || submitButton.getAttribute('aria-disabled') === 'true' || submitButton.classList.contains('disabled'); @@ -407,13 +424,17 @@ export class ZAdapter extends BaseAdapterPlugin { submitButton.click(); // Emit success event to the new event system - this.emitExecutionCompleted('submitForm', { - formElement: options?.formElement?.tagName || 'unknown' - }, { - success: true, - method: 'submitButton.click', - buttonSelector: selectors.find(s => document.querySelector(s.trim()) === submitButton) - }); + this.emitExecutionCompleted( + 'submitForm', + { + formElement: options?.formElement?.tagName || 'unknown', + }, + { + success: true, + method: 'submitButton.click', + buttonSelector: selectors.find(s => document.querySelector(s.trim()) === submitButton), + }, + ); this.context.logger.debug('Z chat input submitted successfully via button click'); return true; @@ -445,14 +466,16 @@ export class ZAdapter extends BaseAdapterPlugin { // Simulate Enter key press const enterEvents = ['keydown', 'keypress', 'keyup']; for (const eventType of enterEvents) { - chatInput.dispatchEvent(new KeyboardEvent(eventType, { - key: 'Enter', - code: 'Enter', - keyCode: 13, - which: 13, - bubbles: true, - cancelable: true - })); + chatInput.dispatchEvent( + new KeyboardEvent(eventType, { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + }), + ); } // Try form submission as additional fallback @@ -462,10 +485,14 @@ export class ZAdapter extends BaseAdapterPlugin { form.dispatchEvent(new SubmitEvent('submit', { bubbles: true, cancelable: true })); } - this.emitExecutionCompleted('submitForm', {}, { - success: true, - method: 'enterKey+formSubmit' - }); + this.emitExecutionCompleted( + 'submitForm', + {}, + { + success: true, + method: 'enterKey+formSubmit', + }, + ); this.context.logger.debug('Z chat input submitted successfully via Enter key'); return true; @@ -500,14 +527,18 @@ export class ZAdapter extends BaseAdapterPlugin { // Method 1: Try using hidden file input element const success1 = await this.attachFileViaInput(file); if (success1) { - this.emitExecutionCompleted('attachFile', { - fileName: file.name, - fileType: file.type, - fileSize: file.size - }, { - success: true, - method: 'file-input' - }); + this.emitExecutionCompleted( + 'attachFile', + { + fileName: file.name, + fileType: file.type, + fileSize: file.size, + }, + { + success: true, + method: 'file-input', + }, + ); this.context.logger.debug(`File attached successfully via input: ${file.name}`); return true; } @@ -515,28 +546,36 @@ export class ZAdapter extends BaseAdapterPlugin { // Method 2: Fallback to drag and drop simulation const success2 = await this.attachFileViaDragDrop(file); if (success2) { - this.emitExecutionCompleted('attachFile', { - fileName: file.name, - fileType: file.type, - fileSize: file.size - }, { - success: true, - method: 'drag-drop' - }); + this.emitExecutionCompleted( + 'attachFile', + { + fileName: file.name, + fileType: file.type, + fileSize: file.size, + }, + { + success: true, + method: 'drag-drop', + }, + ); this.context.logger.debug(`File attached successfully via drag-drop: ${file.name}`); return true; } // Method 3: Try clipboard as final fallback const success3 = await this.attachFileViaClipboard(file); - this.emitExecutionCompleted('attachFile', { - fileName: file.name, - fileType: file.type, - fileSize: file.size - }, { - success: success3, - method: 'clipboard' - }); + this.emitExecutionCompleted( + 'attachFile', + { + fileName: file.name, + fileType: file.type, + fileSize: file.size, + }, + { + success: success3, + method: 'clipboard', + }, + ); if (success3) { this.context.logger.debug(`File copied to clipboard for manual paste: ${file.name}`); @@ -684,7 +723,7 @@ export class ZAdapter extends BaseAdapterPlugin { // Check if we're on a supported Z page const supportedPatterns = [ - /^https:\/\/(?:chat\.)?z\.ai\/search\/.*/, // chat page + /^https:\/\/(?:chat\.)?z\.ai\/search\/.*/, // chat page ]; const isSupported = supportedPatterns.some(pattern => pattern.test(currentUrl)); @@ -770,14 +809,14 @@ export class ZAdapter extends BaseAdapterPlugin { this.context.logger.debug(`Setting up store event listeners for Z adapter instance #${this.instanceId}`); // Listen for tool execution events from the store - this.context.eventBus.on('tool:execution-completed', (data) => { + this.context.eventBus.on('tool:execution-completed', data => { this.context.logger.debug('Tool execution completed:', data); // Handle auto-actions based on store state this.handleToolExecutionCompleted(data); }); // Listen for UI state changes - this.context.eventBus.on('ui:sidebar-toggle', (data) => { + this.context.eventBus.on('ui:sidebar-toggle', data => { this.context.logger.debug('Sidebar toggled:', data); }); @@ -793,10 +832,10 @@ export class ZAdapter extends BaseAdapterPlugin { this.context.logger.debug(`Setting up DOM observers for Z adapter instance #${this.instanceId}`); // Set up mutation observer to detect page changes and re-inject UI if needed - this.mutationObserver = new MutationObserver((mutations) => { + this.mutationObserver = new MutationObserver(mutations => { let shouldReinject = false; - mutations.forEach((mutation) => { + mutations.forEach(mutation => { if (mutation.type === 'childList') { // Check if our MCP popover was removed if (!document.getElementById('mcp-popover-container')) { @@ -828,19 +867,23 @@ export class ZAdapter extends BaseAdapterPlugin { // Allow multiple calls for UI integration (for re-injection after page changes) // but log it for debugging if (this.uiIntegrationSetup) { - this.context.logger.debug(`UI integration already set up for instance #${this.instanceId}, re-injecting for page changes`); + this.context.logger.debug( + `UI integration already set up for instance #${this.instanceId}, re-injecting for page changes`, + ); } else { this.context.logger.debug(`Setting up UI integration for Z adapter instance #${this.instanceId}`); this.uiIntegrationSetup = true; } // Wait for page to be ready, then inject MCP popover - this.waitForPageReady().then(() => { - this.injectMCPPopoverWithRetry(); - }).catch((error) => { - this.context.logger.warn('Failed to wait for page ready:', error); - // Don't retry if we can't find insertion point - }); + this.waitForPageReady() + .then(() => { + this.injectMCPPopoverWithRetry(); + }) + .catch(error => { + this.context.logger.warn('Failed to wait for page ready:', error); + // Don't retry if we can't find insertion point + }); // Set up periodic check to ensure popover stays injected // this.setupPeriodicPopoverCheck(); @@ -972,11 +1015,7 @@ export class ZAdapter extends BaseAdapterPlugin { } // Try fallback selectors - const fallbackSelectors = [ - '.input-area .actions', - '.chat-input-actions', - '.conversation-input .actions' - ]; + const fallbackSelectors = ['.input-area .actions', '.chat-input-actions', '.conversation-input .actions']; for (const selector of fallbackSelectors) { const container = document.querySelector(selector); @@ -1033,40 +1072,46 @@ export class ZAdapter extends BaseAdapterPlugin { try { // Import React and ReactDOM dynamically to avoid bundling issues - import('react').then(React => { - import('react-dom/client').then(ReactDOM => { - import('../../components/mcpPopover/mcpPopover').then(({ MCPPopover }) => { - // Create toggle state manager that integrates with new stores - const toggleStateManager = this.createToggleStateManager(); - - // Create adapter button configuration - const adapterButtonConfig = { - className: 'mcp-z-button-base', - contentClassName: 'mcp-z-button-content', - textClassName: 'mcp-z-button-text', - activeClassName: 'mcp-button-active' - }; - - // Create React root and render - const root = ReactDOM.createRoot(container); - root.render( - React.createElement(MCPPopover, { - toggleStateManager: toggleStateManager, - adapterButtonConfig: adapterButtonConfig, - adapterName: this.name - }) - ); - - this.context.logger.debug('MCP popover rendered successfully with new architecture'); - }).catch(error => { - this.context.logger.error('Failed to import MCPPopover component:', error); - }); - }).catch(error => { - this.context.logger.error('Failed to import ReactDOM:', error); + import('react') + .then(React => { + import('react-dom/client') + .then(ReactDOM => { + import('../../components/mcpPopover/mcpPopover') + .then(({ MCPPopover }) => { + // Create toggle state manager that integrates with new stores + const toggleStateManager = this.createToggleStateManager(); + + // Create adapter button configuration + const adapterButtonConfig = { + className: 'mcp-z-button-base', + contentClassName: 'mcp-z-button-content', + textClassName: 'mcp-z-button-text', + activeClassName: 'mcp-button-active', + }; + + // Create React root and render + const root = ReactDOM.createRoot(container); + root.render( + React.createElement(MCPPopover, { + toggleStateManager: toggleStateManager, + adapterButtonConfig: adapterButtonConfig, + adapterName: this.name, + }), + ); + + this.context.logger.debug('MCP popover rendered successfully with new architecture'); + }) + .catch(error => { + this.context.logger.error('Failed to import MCPPopover component:', error); + }); + }) + .catch(error => { + this.context.logger.error('Failed to import ReactDOM:', error); + }); + }) + .catch(error => { + this.context.logger.error('Failed to import React:', error); }); - }).catch(error => { - this.context.logger.error('Failed to import React:', error); - }); } catch (error) { this.context.logger.error('Failed to render MCP popover:', error); } @@ -1093,7 +1138,7 @@ export class ZAdapter extends BaseAdapterPlugin { mcpEnabled: mcpEnabled, // Use the persistent MCP state autoInsert: autoSubmitEnabled, autoSubmit: autoSubmitEnabled, - autoExecute: false // Default for now, can be extended + autoExecute: false, // Default for now, can be extended }; } catch (error) { context.logger.error('Error getting toggle state:', error); @@ -1102,13 +1147,15 @@ export class ZAdapter extends BaseAdapterPlugin { mcpEnabled: false, autoInsert: false, autoSubmit: false, - autoExecute: false + autoExecute: false, }; } }, setMCPEnabled: (enabled: boolean) => { - context.logger.debug(`Setting MCP ${enabled ? 'enabled' : 'disabled'} - controlling sidebar visibility via MCP state`); + context.logger.debug( + `Setting MCP ${enabled ? 'enabled' : 'disabled'} - controlling sidebar visibility via MCP state`, + ); try { // Primary method: Control MCP state through UI store (which will automatically control sidebar) @@ -1143,7 +1190,9 @@ export class ZAdapter extends BaseAdapterPlugin { context.logger.warn('activeSidebarManager not available on window - will rely on UI store only'); } - context.logger.debug(`MCP toggle completed: MCP ${enabled ? 'enabled' : 'disabled'}, sidebar ${enabled ? 'shown' : 'hidden'}`); + context.logger.debug( + `MCP toggle completed: MCP ${enabled ? 'enabled' : 'disabled'}, sidebar ${enabled ? 'shown' : 'hidden'}`, + ); } catch (error) { context.logger.error('Error in setMCPEnabled:', error); } @@ -1187,11 +1236,11 @@ export class ZAdapter extends BaseAdapterPlugin { if (popoverContainer) { const currentState = stateManager.getState(); const event = new CustomEvent('mcp:update-toggle-state', { - detail: { toggleState: currentState } + detail: { toggleState: currentState }, }); popoverContainer.dispatchEvent(event); } - } + }, }; return stateManager; @@ -1229,7 +1278,7 @@ export class ZAdapter extends BaseAdapterPlugin { this.context.eventBus.emit('tool:execution-failed', { toolName, error, - callId: this.generateCallId() + callId: this.generateCallId(), }); } @@ -1254,7 +1303,6 @@ export class ZAdapter extends BaseAdapterPlugin { // Sidebar manager exists, just ensure MCP popover connection is working this.ensureMCPPopoverConnection(); - } catch (error) { this.context.logger.error('Error checking sidebar state after navigation:', error); } @@ -1309,7 +1357,7 @@ export class ZAdapter extends BaseAdapterPlugin { // Emit page change event to stores this.context.eventBus.emit('app:site-changed', { site: url, - hostname: window.location.hostname + hostname: window.location.hostname, }); } @@ -1323,7 +1371,7 @@ export class ZAdapter extends BaseAdapterPlugin { // Emit deactivation event using available event type this.context.eventBus.emit('adapter:deactivated', { pluginName: this.name, - timestamp: Date.now() + timestamp: Date.now(), }); } else { // Re-setup for new host @@ -1534,4 +1582,4 @@ export class ZAdapter extends BaseAdapterPlugin { this.context.logger.error('Failed to inject Z button styles:', error); } } -} \ No newline at end of file +} diff --git a/pages/content/src/render_prescript/prescript.js b/pages/content/src/render_prescript/prescript.js index d232803c..c505f10e 100644 --- a/pages/content/src/render_prescript/prescript.js +++ b/pages/content/src/render_prescript/prescript.js @@ -1,3 +1,5 @@ +import { getCMContent } from '@src/utils/helpers'; + (() => { // Configurable options const CONFIG = { @@ -662,7 +664,8 @@ // Check if a block contains function calls, handling language tags // Returns an object with more detailed information about the function call state const containsFunctionCalls = block => { - const content = block.textContent.trim(); + var content = getCMContent(block)?.trim() || block.textContent.trim(); + const result = { hasFunctionCalls: false, isComplete: false, diff --git a/pages/content/src/render_prescript/src/core/config.ts b/pages/content/src/render_prescript/src/core/config.ts index f97bf6d4..6b6382cf 100644 --- a/pages/content/src/render_prescript/src/core/config.ts +++ b/pages/content/src/render_prescript/src/core/config.ts @@ -80,9 +80,9 @@ export const WEBSITE_CONFIGS: Array<{ urlPattern: 'z', config: { targetSelectors: ['.cm-editor'], - streamingContainerSelectors: ['.cm-content'], + streamingContainerSelectors: ['.cm-editor', '.cm-gutters'], function_result_selector: ['div.chat-user'], - updateThrottle: 900, + updateThrottle: 500, }, }, { diff --git a/pages/content/src/render_prescript/src/observer/functionResultObserver.ts b/pages/content/src/render_prescript/src/observer/functionResultObserver.ts index c13629d2..57f7c209 100644 --- a/pages/content/src/render_prescript/src/observer/functionResultObserver.ts +++ b/pages/content/src/render_prescript/src/observer/functionResultObserver.ts @@ -1,5 +1,6 @@ import { CONFIG } from '../core/config'; import { renderFunctionResult, processedResultElements } from '../renderer/functionResult'; +import { getCMContent } from '../../../utils/helpers'; // State for processing and observers const isProcessing = false; @@ -238,10 +239,8 @@ export const startFunctionResultMonitoring = (): void => { }); // Also check if the content of any text nodes might contain function result patterns - if ( - element.textContent && - (element.textContent.includes('')) - ) { + const content = getCMContent(element) || element.textContent; + if (content && (content.includes(''))) { potentialFunctionResult = true; } diff --git a/pages/content/src/render_prescript/src/observer/mutationObserver.ts b/pages/content/src/render_prescript/src/observer/mutationObserver.ts index d1c91533..f044b7f9 100644 --- a/pages/content/src/render_prescript/src/observer/mutationObserver.ts +++ b/pages/content/src/render_prescript/src/observer/mutationObserver.ts @@ -16,6 +16,7 @@ import { startStalledStreamDetection, stopStalledStreamDetection, } from './stalledStreamHandler'; +import { getCMContent } from '../../../utils/helpers'; // State for processing and observers let isProcessing = false; @@ -225,12 +226,14 @@ export const startDirectMonitoring = (): void => { element.querySelectorAll(CONFIG.streamingContainerSelectors.join(',')).length > 0; // Also check if the content of any text nodes might contain function call patterns + const textContent = getCMContent(element) || element.textContent; + if ( - element.textContent && - (element.textContent.includes('') || - element.textContent.includes('') || - element.textContent.includes('') || + textContent.includes('') || + textContent.includes(' { if (CONFIG.debug) console.debug(`Setting up direct monitoring for block: ${blockId}`); + let currentContent = getCMContent(node) || node.textContent || ''; + let currentLength = currentContent.length; + // Initialize the last updated timestamp streamingLastUpdated.set(blockId, Date.now()); @@ -374,7 +379,7 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { // Track consecutive inactive periods (no content changes) let inactivePeriods = 0; - let lastContentLength = node.textContent?.length || 0; + let lastContentLength = currentContent.length || 0; let detectedIncompleteTags = false; // Setup a periodic checker for this node that can detect abrupt endings @@ -384,9 +389,9 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { clearInterval(periodicChecker); return; } - - const currentContent = node.textContent || ''; - const currentLength = currentContent.length; + // Refreshing current content + currentContent = getCMContent(node) || node.textContent || currentContent; + currentLength = currentContent.length; // Check if content has incomplete tags const hasOpenFunctionCallsTag = @@ -455,7 +460,7 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { // Get the content once for analysis const targetNode = mutation.target; - const textContent = targetNode.textContent || ''; + const textContent = getCMContent(targetNode) || targetNode.textContent || ''; // Use fast pattern matching instead of string includes if (!functionCallPattern) { @@ -497,7 +502,7 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { if (contentChanged) { // Reset the inactive periods counter since we've seen new content inactivePeriods = 0; - lastContentLength = node.textContent?.length || 0; + lastContentLength = (getCMContent(node) || node.textContent)?.length || 0; // Update the last updated timestamp when content changes streamingLastUpdated.set(blockId, Date.now()); @@ -509,7 +514,11 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { // Find the nearest element that contains our monitored node let target = node; - while (target && !CONFIG.targetSelectors.includes(target.tagName.toLowerCase())) { + while ( + target && + !CONFIG.targetSelectors.includes(target.tagName.toLowerCase()) && + !CONFIG.targetSelectors.includes('.cm-editor') + ) { target = target.parentElement as HTMLElement; if (!target) break; } diff --git a/pages/content/src/render_prescript/src/parser/functionParser.ts b/pages/content/src/render_prescript/src/parser/functionParser.ts index 7e20da14..aab672a6 100644 --- a/pages/content/src/render_prescript/src/parser/functionParser.ts +++ b/pages/content/src/render_prescript/src/parser/functionParser.ts @@ -1,33 +1,6 @@ import { extractLanguageTag } from './languageParser'; -import { CONFIG, FunctionInfo } from '../core'; - - -export function startsWithFunctionCalls(str: string | null): boolean { - if (str == null) { - return false; - } - if (CONFIG.targetSelectors.includes('.cm-editor')) { - const regex = /^.*?(?:›⌄⌄\s*)?\n?\s*') || str.includes(' { - const content = block.textContent?.trim() || ''; + const content = getCMContent(block)?.trim() || block.textContent?.trim() || ''; const result: FunctionInfo = { hasFunctionCalls: false, isComplete: false, @@ -50,12 +23,7 @@ export const containsFunctionCalls = (block: HTMLElement): FunctionInfo => { }; // Check for any signs of function call content - if ( - !startsWithFunctionCalls(content) && - !content.includes('<') && - !content.includes('') && - !content.includes('') && !content.includes(' { const contentToExamine = langTagResult.content || content; // Check for Claude Opus style function calls - if (startsWithFunctionCalls(contentToExamine)) { + if (contentToExamine.includes('') || contentToExamine.includes(' { } }, 5000); }; + + +export function getCMContent(node: Element | Node): string | null { + // Checking if streaming container is CM Editor + if ( + !( + (CONFIG.streamingContainerSelectors.includes('.cm-editor') || + CONFIG.streamingContainerSelectors.includes('.cm-gutters')) && + node.parentElement?.querySelector('.cm-content') + ) + ) { + return null; + } + + const element = node.parentElement?.querySelector('.cm-content'); + if (element == null) return null; + const uniqueId = 'cm-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); + element.setAttribute('data-cm-id', uniqueId); + const script = document.createElement('script'); + script.textContent = ` + (function() { + const el = document.querySelector('[data-cm-id="${uniqueId}"]'); + if (el && el.cmView) { + const doc = el.cmView.view?.viewState?.state?.doc; + let content = ''; + if (doc?.text) content = doc.text.join(''); + else if (doc?.children) content = doc.children.join(''); + el.setAttribute('data-cm-text', content); + } + })(); + `; + document.documentElement.appendChild(script); + script.remove(); + + // Now read it back + const target = document.querySelector('[data-cm-id="' + uniqueId + '"]'); + const content = target?.getAttribute('data-cm-text') || ''; + if (target) { + target.removeAttribute('data-cm-text'); + } + return content; +} From eae50778eb22ad49008e32a11e67b75bebcf27bb Mon Sep 17 00:00:00 2001 From: djamiirr Date: Sun, 10 Aug 2025 20:01:28 +0100 Subject: [PATCH 3/5] Shrinking throttle for z.ai --- pages/content/src/render_prescript/src/core/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/content/src/render_prescript/src/core/config.ts b/pages/content/src/render_prescript/src/core/config.ts index 6b6382cf..b5ea9d11 100644 --- a/pages/content/src/render_prescript/src/core/config.ts +++ b/pages/content/src/render_prescript/src/core/config.ts @@ -82,7 +82,7 @@ export const WEBSITE_CONFIGS: Array<{ targetSelectors: ['.cm-editor'], streamingContainerSelectors: ['.cm-editor', '.cm-gutters'], function_result_selector: ['div.chat-user'], - updateThrottle: 500, + updateThrottle: 300, }, }, { From 7af7f618a0d1e4c7cb41a4021304892b89cfd534 Mon Sep 17 00:00:00 2001 From: djamiirr Date: Sun, 10 Aug 2025 20:03:23 +0100 Subject: [PATCH 4/5] Shrinking throttle for z.ai --- pages/content/src/render_prescript/src/core/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/content/src/render_prescript/src/core/config.ts b/pages/content/src/render_prescript/src/core/config.ts index b5ea9d11..e05b9360 100644 --- a/pages/content/src/render_prescript/src/core/config.ts +++ b/pages/content/src/render_prescript/src/core/config.ts @@ -82,7 +82,6 @@ export const WEBSITE_CONFIGS: Array<{ targetSelectors: ['.cm-editor'], streamingContainerSelectors: ['.cm-editor', '.cm-gutters'], function_result_selector: ['div.chat-user'], - updateThrottle: 300, }, }, { From 49d14d487969346749d58f210da8c3cce32d72aa Mon Sep 17 00:00:00 2001 From: djamiirr Date: Mon, 11 Aug 2025 20:22:42 +0100 Subject: [PATCH 5/5] - Fixed retrieved text format - removed unecessary usage of getCMContent from rescript.js --- .../content/src/render_prescript/prescript.js | 2 -- .../src/observer/streamObserver.ts | 5 +++ pages/content/src/utils/helpers.ts | 33 +++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pages/content/src/render_prescript/prescript.js b/pages/content/src/render_prescript/prescript.js index c505f10e..cfa236c8 100644 --- a/pages/content/src/render_prescript/prescript.js +++ b/pages/content/src/render_prescript/prescript.js @@ -1,5 +1,3 @@ -import { getCMContent } from '@src/utils/helpers'; - (() => { // Configurable options const CONFIG = { diff --git a/pages/content/src/render_prescript/src/observer/streamObserver.ts b/pages/content/src/render_prescript/src/observer/streamObserver.ts index 88589aa4..e826b35b 100644 --- a/pages/content/src/render_prescript/src/observer/streamObserver.ts +++ b/pages/content/src/render_prescript/src/observer/streamObserver.ts @@ -393,6 +393,11 @@ export const monitorNode = (node: HTMLElement, blockId: string): void => { currentContent = getCMContent(node) || node.textContent || currentContent; currentLength = currentContent.length; + // preventing keep checking in case of cm editor. + if (currentContent.includes('')) { + clearInterval(periodicChecker); + } + // Check if content has incomplete tags const hasOpenFunctionCallsTag = currentContent.includes('') && !currentContent.includes(''); diff --git a/pages/content/src/utils/helpers.ts b/pages/content/src/utils/helpers.ts index 1b622dbd..bc40517c 100644 --- a/pages/content/src/utils/helpers.ts +++ b/pages/content/src/utils/helpers.ts @@ -109,32 +109,44 @@ export const debugShadowDomStyles = (shadowRoot: ShadowRoot): void => { }, 5000); }; - -export function getCMContent(node: Element | Node): string | null { - // Checking if streaming container is CM Editor +export function getCMContent(el: Element | Node): string | null { + // Verify element is inside a recognized CodeMirror container and has .cm-content if ( !( (CONFIG.streamingContainerSelectors.includes('.cm-editor') || CONFIG.streamingContainerSelectors.includes('.cm-gutters')) && - node.parentElement?.querySelector('.cm-content') + el.parentElement?.querySelector('.cm-content') ) ) { return null; } - const element = node.parentElement?.querySelector('.cm-content'); + let node = el as HTMLElement; + + // Climb DOM until reaching .cm-editor or + while (!node.matches('.cm-editor') && node !== document.body) { + node = node.parentElement!; + } + + if (node === document.body) { + return null; + } + + const element = node.querySelector('.cm-content'); if (element == null) return null; + + // Unique ID for targeting in injected script const uniqueId = 'cm-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); element.setAttribute('data-cm-id', uniqueId); + + // Inject script in page context to access cmView (CodeMirror internal API) + // NOTE: Using doc.toString() here may alter formatting (line breaks/spacing). const script = document.createElement('script'); script.textContent = ` (function() { const el = document.querySelector('[data-cm-id="${uniqueId}"]'); if (el && el.cmView) { - const doc = el.cmView.view?.viewState?.state?.doc; - let content = ''; - if (doc?.text) content = doc.text.join(''); - else if (doc?.children) content = doc.children.join(''); + const content = el.cmView.view?.viewState?.state?.doc?.toString(); el.setAttribute('data-cm-text', content); } })(); @@ -142,11 +154,12 @@ export function getCMContent(node: Element | Node): string | null { document.documentElement.appendChild(script); script.remove(); - // Now read it back + // Read extracted content const target = document.querySelector('[data-cm-id="' + uniqueId + '"]'); const content = target?.getAttribute('data-cm-text') || ''; if (target) { target.removeAttribute('data-cm-text'); } + return content; }