Guide for adding new AI platform support (e.g., Gemini, Mistral, Anthropic) to the Chrome extension with strategy pattern and testing requirements
This skill guides you through adding support for new AI platforms (like Gemini, Mistral.ai, Meta AI, etc.) to the My Prompt Manager Chrome extension.
Adding a new platform requires a 10-step workflow using the centralized configuration system introduced in v1.3+. The process takes approximately 15-30 minutes for basic support.
Architecture Pattern: Strategy Pattern + Centralized Configuration
PlatformStrategysrc/config/platforms.tsBefore starting, ensure you have:
gemini.google.com)Action: Open browser DevTools on the target platform and identify:
// In browser console, test selectors:
document.querySelectorAll('textarea[placeholder*="message"]')
document.querySelectorAll('div[contenteditable="true"]')
document.querySelectorAll('[role="textbox"]')
What to find:
Common patterns:
textarea[data-testid="chat-input"]div[contenteditable="true"][role="textbox"]textarea[placeholder*="Ask"]File: src/config/platforms.ts
Action: Add a new entry to the SUPPORTED_PLATFORMS object:
export const SUPPORTED_PLATFORMS: Record<string, PlatformDefinition> = {
// ... existing platforms ...
yourplatform: {
id: 'yourplatform', // Unique ID (lowercase, no spaces)
hostname: 'your-ai-platform.com', // Exact hostname
displayName: 'Your AI Platform', // Human-readable name
priority: 70, // 60-79 for new platforms
defaultEnabled: true, // Include in fresh installs
selectors: [ // CSS selectors (most specific first)
'textarea[data-testid="chat-input"]',
'div[contenteditable="true"][role="textbox"]',
'[role="textbox"]' // Generic fallback
],
buttonContainerSelector: '.toolbar-container', // Where to place icon (optional)
strategyClass: 'YourPlatformStrategy', // Strategy class name
hostnamePatterns: ['yourplatform', 'your-ai'], // Additional hostname patterns
brandColors: { // Tailwind classes for badge styling
enabled: 'bg-[#123456] text-white shadow-xs',
disabled: 'bg-gray-300 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
}
}
};
โน๏ธ If you omit
brandColors, the UI falls back to a green gradient badge so the integration still renders correctly.
Priority Guidelines:
Add or update the platform card shown in settings so users can enable/disable the integration.
When to do this:
File: src/components/settings/SiteCard.tsx
Actions:
src/components/icons/SiteIcons.tsx (or reuse an existing one).src/components/settings/SiteCardList.tsx (or wherever cards are listed) to include the new platform with the right icon.getBrandColors(hostname).brandColors if needed.Tip: The label for the platform name comes from displayName in SUPPORTED_PLATFORMS. Keep them in sync.
File: src/content/platforms/yourplatform-strategy.ts
Action: Create a new strategy class:
import { getPlatformById } from '../../config/platforms';
import type { InsertionResult } from '../types/index';
import type { UIElementFactory } from '../ui/element-factory';
import { PlatformStrategy } from './base-strategy';
export class YourPlatformStrategy extends PlatformStrategy {
constructor(hostname?: string) {
super('yourplatform', 70, {
selectors: [
'textarea[placeholder*="Enter your message"]',
'div[contenteditable="true"][role="textbox"]'
],
buttonContainerSelector: '.toolbar-container',
priority: 70
}, hostname);
}
/**
* Determines if this strategy can handle the element
* Handles any element on your-ai-platform.com
*/
canHandle(_element: HTMLElement): boolean {
return this.hostname === getPlatformById('yourplatform')?.hostname;
}
/**
* Inserts content using platform-compatible methods
*/
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
try {
// Focus the element first
element.focus();
element.click();
// Wait for platform initialization
await new Promise(resolve => setTimeout(resolve, 50));
// Method 1: Try for textarea elements
if (element.tagName === 'TEXTAREA') {
(element as HTMLTextAreaElement).value = content;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return { success: true, method: 'yourplatform-textarea' };
}
// Method 2: Try for contenteditable elements
if (element.contentEditable === 'true') {
// Clear existing content
element.textContent = '';
// Insert new content
const inserted = document.execCommand('insertText', false, content);
if (inserted) {
// Trigger platform events
element.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: content
}));
return { success: true, method: 'yourplatform-execCommand' };
}
}
return { success: false, error: 'Unsupported element type' };
} catch (error) {
this._error('Insertion failed', error as Error);
return { success: false, error: (error as Error).message };
}
}
/**
* Gets selectors for finding input elements
*/
getSelectors(): string[] {
return this.config?.selectors || [];
}
/**
* Creates platform-specific icon using the UI factory
*/
createIcon(uiFactory: UIElementFactory): HTMLElement | null {
return uiFactory.createFloatingIcon(); // Use generic icon
// OR: return uiFactory.createYourPlatformIcon(); // Custom icon
}
}
Reference Example: See src/content/platforms/perplexity-strategy.ts for a complete implementation with multiple fallback methods.
File: src/content/platforms/index.ts
Action: Add export for your strategy:
export { YourPlatformStrategy } from './yourplatform-strategy';
File: src/content/platforms/platform-manager.ts
Action: Import and register in the constructor:
// Add import at top
import { YourPlatformStrategy } from './yourplatform-strategy';
// In the constructor, add to the strategies array:
constructor(options: PlatformManagerOptions = {}) {
this.strategies = [
new ClaudeStrategy(),
new ChatGPTStrategy(),
new PerplexityStrategy(),
new MistralStrategy(),
new GeminiStrategy(),
new YourPlatformStrategy(), // Add your strategy here
new DefaultStrategy() // IMPORTANT: Keep DefaultStrategy last
];
// ... rest of constructor
}
File: manifest.json
Action: If the platform hostname is not already covered by <all_urls>, add it to content_scripts matches:
{
"content_scripts": [
{
"matches": [
"https://*.your-ai-platform.com/*"
],
"js": ["src/content/index.ts"],
"css": ["src/content/styles/injected-icon.css"]
}
]
}
Note: The extension uses <all_urls> host_permissions, so this step is usually not needed.
File: src/content/platforms/__tests__/yourplatform-strategy.test.ts
Action: Create comprehensive tests:
import { describe, it, expect, beforeEach } from 'vitest';
import { YourPlatformStrategy } from '../yourplatform-strategy';
describe('YourPlatformStrategy', () => {
let strategy: YourPlatformStrategy;
beforeEach(() => {
strategy = new YourPlatformStrategy('your-ai-platform.com');
});
describe('canHandle', () => {
it('returns true for your-ai-platform.com', () => {
const element = document.createElement('textarea');
expect(strategy.canHandle(element)).toBe(true);
});
it('returns false for other hostnames', () => {
const otherStrategy = new YourPlatformStrategy('other-site.com');
const element = document.createElement('textarea');
expect(otherStrategy.canHandle(element)).toBe(false);
});
});
describe('insert', () => {
it('inserts content into textarea element', async () => {
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
const result = await strategy.insert(textarea, 'Test content');
expect(result.success).toBe(true);
expect(textarea.value).toBe('Test content');
expect(result.method).toBe('yourplatform-textarea');
document.body.removeChild(textarea);
});
it('inserts content into contenteditable element', async () => {
const div = document.createElement('div');
div.contentEditable = 'true';
div.setAttribute('role', 'textbox');
document.body.appendChild(div);
const result = await strategy.insert(div, 'Test content');
expect(result.success).toBe(true);
expect(div.textContent).toContain('Test content');
document.body.removeChild(div);
});
it('returns error for unsupported elements', async () => {
const span = document.createElement('span');
const result = await strategy.insert(span, 'Test content');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe('getSelectors', () => {
it('returns configured selectors', () => {
const selectors = strategy.getSelectors();
expect(selectors).toBeInstanceOf(Array);
expect(selectors.length).toBeGreaterThan(0);
});
});
});
Action: Build and load the extension:
# Build the extension
npm run build
# Load in Chrome:
# 1. Open chrome://extensions/
# 2. Enable Developer mode
# 3. Click "Load unpacked"
# 4. Select the dist/ folder
Manual Testing Checklist:
Debug Mode:
// Enable debug logging in browser console:
localStorage.setItem('prompt-library-debug', 'true');
// Then reload the page
// Check debug interface:
console.log(window.__promptLibraryDebug?.strategies);
console.log(window.__promptLibraryDebug?.activeStrategy);
console.log(window.__promptLibraryDebug?.foundElements);
Action: Ensure all tests pass:
# Run unit tests
npm test
# Run specific test file
npm test -- yourplatform-strategy.test.ts
# Run with coverage
npm run test:coverage
CRITICAL: Both tests and linting MUST pass before proceeding:
# Run linting
npm run lint
# Fix linting issues automatically
npm run lint:fix
When to create a custom icon:
When to use the generic icon:
uiFactory.createFloatingIcon())File: src/content/ui/element-factory.ts
Action: Add a custom icon creation method following the modern pattern:
/**
* Creates Your Platform-specific icon matching native button styling
* Uses shared prompt-library-integrated-icon class for E2E test compatibility
*/
createYourPlatformIcon(): HTMLElement {
const icon = document.createElement('button');
// IMPORTANT: Always include 'prompt-library-integrated-icon' for E2E test compatibility
// Add platform-specific Tailwind classes after the shared class
icon.className = 'prompt-library-integrated-icon flex items-center gap-2 px-3 h-10 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 active:scale-[0.98] transition-all duration-200 text-gray-700 dark:text-gray-300';
// Required attributes
icon.setAttribute('type', 'button');
icon.setAttribute('aria-label', 'Open my prompt manager - Access your saved prompts');
icon.setAttribute('title', 'My Prompt Manager - Access your saved prompts');
icon.setAttribute('data-instance-id', this.instanceId);
icon.setAttribute('tabindex', '0');
icon.setAttribute('data-dashlane-label', 'true');
// Create SVG using secure utility (STANDARD SIZE: 18x18)
const svg = createSVGElement('svg', {
xmlns: 'http://www.w3.org/2000/svg',
width: '18', // Standard size - use 18x18 for consistency
height: '18',
viewBox: '0 0 24 24',
fill: 'currentColor',
'aria-hidden': 'true',
style: 'flex-shrink: 0;'
});
// Standard chat bubble icon with three dots (consistent across platforms)
const path = createSVGElement('path', {
d: 'M5.65 9.42C5.93 9.42 6.17 9.32 6.36 9.13C6.55 8.94 6.65 8.69 6.65 8.4C6.65 8.1 6.55 7.85 6.36 7.66C6.17 7.47 5.93 7.37 5.65 7.37C5.37 7.37 5.13 7.47 4.94 7.66C4.75 7.85 4.65 8.1 4.65 8.4C4.65 8.69 4.75 8.94 4.94 9.13C5.13 9.32 5.37 9.42 5.65 9.42ZM10.08 9.42C10.36 9.42 10.6 9.32 10.79 9.13C10.98 8.94 11.08 8.69 11.08 8.4C11.08 8.1 10.98 7.85 10.79 7.66C10.6 7.47 10.36 7.37 10.08 7.37C9.8 7.37 9.56 7.47 9.37 7.66C9.18 7.85 9.08 8.1 9.08 8.4C9.08 8.69 9.18 8.94 9.37 9.13C9.56 9.32 9.8 9.42 10.08 9.42ZM14.32 9.42C14.6 9.42 14.84 9.32 15.03 9.13C15.22 8.94 15.32 8.69 15.32 8.4C15.32 8.1 15.22 7.85 15.03 7.66C14.84 7.47 14.6 7.37 14.32 7.37C14.04 7.37 13.8 7.47 13.61 7.66C13.42 7.85 13.32 8.1 13.32 8.4C13.32 8.69 13.42 8.94 13.61 9.13C13.8 9.32 14.04 9.42 14.32 9.42ZM0 19.21V1.58C0 1.18 0.15 0.81 0.45 0.49C0.75 0.16 1.1 0 1.5 0H18.5C18.88 0 19.23 0.16 19.54 0.49C19.85 0.81 20 1.18 20 1.58V15.3C20 15.7 19.85 16.07 19.54 16.39C19.23 16.72 18.88 16.88 18.5 16.88H4L1.28 19.76C1.04 20.01 0.77 20.06 0.46 19.93C0.15 19.8 0 19.56 0 19.21ZM1.5 17.28L3.37 15.3H18.5V1.58H1.5V17.28ZM1.5 1.58V15.3V17.28V1.58Z'
});
svg.appendChild(path);
icon.appendChild(svg);
// Add text label (optional - include for button-style integrations)
const textElement = createElement('span', {
class: 'text-sm font-medium whitespace-nowrap'
});
textElement.textContent = 'My Prompts';
icon.appendChild(textElement);
return icon;
}
Key Patterns to Follow:
prompt-library-integrated-icon class - Required for E2E test selectorscreateElement and createSVGElement - Secure DOM construction (never innerHTML)dark: variants for all stylesReference Examples:
createClaudeIcon(), createPerplexityIcon(), createCopilotIcon()createChatGPTIcon(), createGeminiIcon()createFloatingIcon()File: src/components/icons/SiteIcons.tsx
Action: Add a React icon component for the settings UI:
export const YourPlatformIcon: React.FC<{ className?: string; disabled?: boolean }> = ({
className = 'h-6 w-6',
disabled = false
}) => {
return (
<svg className={className} viewBox="0 0 24 24" fill="none">
{/* Your platform's SVG path */}
<path
d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"
fill={disabled ? 'currentColor' : '#YOUR_BRAND_COLOR'}
/>
</svg>
);
};
File: docs/PLATFORM_INTEGRATION.md
Action: Add your platform to the examples section:
### Example: Your Platform
\`\`\`typescript
export class YourPlatformStrategy extends PlatformStrategy {
constructor() {
super('yourplatform', 70, {
selectors: ['textarea[data-testid="input"]'],
priority: 70
});
}
canHandle(_element: HTMLElement): boolean {
return this.hostname === 'your-ai-platform.com';
}
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
// Implementation details...
}
}
\`\`\`
File: README.md
Action: Add to the supported platforms list:
- **Your Platform** - Full support for prompt injection
Symptoms: Icon doesn't show up on the platform
Solutions:
canHandle() returns true for your domainplatform-manager.tsplatforms.ts matches exactlyDebug:
// In browser console:
document.querySelectorAll('your-selector-here')
window.__promptLibraryDebug?.strategies
Symptoms: Icon is visible but poorly positioned
Solutions:
buttonContainerSelector in platforms.tssrc/content/core/injector.tsExample:
buttonContainerSelector: [
'.primary-container', // Try first
'.fallback-container', // Then this
'.generic-container' // Final fallback
].join(', ')
Symptoms: Icon works, but prompt doesn't appear in input
Solutions:
Pattern - Multiple Fallback Methods:
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
// Method 1: execCommand
const execResult = await this._tryExecCommand(element, content);
if (execResult.success) return execResult;
// Method 2: Direct assignment
const directResult = this._tryDirectAssignment(element, content);
if (directResult.success) return directResult;
// Method 3: DOM manipulation
return this._tryDOMManipulation(element, content);
}
Symptoms: Text appears but submit button stays disabled
Solutions:
Comprehensive Event Triggering:
private _triggerEvents(element: HTMLElement, content: string): void {
// Primary input event
element.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: content
}));
// Secondary events
const events = ['change', 'keyup', 'compositionend', 'blur', 'focus'];
events.forEach(eventType => {
element.dispatchEvent(new Event(eventType, { bubbles: true }));
});
}
Symptoms: Manual testing works but automated tests fail
Solutions:
getPlatformById() if neededdocument.body before testingProper Test Setup:
describe('insert', () => {
it('inserts into textarea', async () => {
const textarea = document.createElement('textarea');
document.body.appendChild(textarea); // Add to DOM
const result = await strategy.insert(textarea, 'Test');
expect(result.success).toBe(true);
document.body.removeChild(textarea); // Clean up
});
});
For platforms using React, you may need to trigger React's internal event system:
private _setReactValue(element: HTMLInputElement | HTMLTextAreaElement, value: string): void {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
)?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(element, value);
element.dispatchEvent(new Event('input', { bubbles: true }));
}
}
For platforms using Shadow DOM:
getSelectors(): string[] {
// Check for shadow DOM
const shadowHost = document.querySelector('your-shadow-host');
if (shadowHost?.shadowRoot) {
const shadowInput = shadowHost.shadowRoot.querySelector('textarea');
if (shadowInput) {
return []; // Handle separately
}
}
return this.config?.selectors || [];
}
For platforms that create inputs dynamically:
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
// Wait for element to be fully initialized
await this._waitForReady(element);
// Then proceed with insertion
// ...
}
private async _waitForReady(element: HTMLElement): Promise<void> {
return new Promise((resolve) => {
if (element.isConnected && element.offsetParent !== null) {
resolve();
} else {
const observer = new MutationObserver(() => {
if (element.isConnected && element.offsetParent !== null) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
});
}
Use this checklist when adding a new platform:
src/config/platforms.tssrc/content/platforms/platform-manager.ts and index.tsnpm test)npm run lint)docs/ARCHITECTURE.mddocs/PLATFORM_INTEGRATION.md (comprehensive examples)docs/TESTING.mddocs/COMPONENTS.mdYour platform integration is complete when:
Last Updated: 2025-11-10 Skill Version: 1.1.0