| name | Platform Integration Workflow |
| description | Guide for adding new AI platform support (e.g., Gemini, Mistral, Anthropic) to the Chrome extension with strategy pattern and testing requirements |
Platform Integration Workflow
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.
When to Use This Skill
- User asks to add support for a new AI platform or website
- User mentions "add platform", "support [platform name]", "integrate with [AI]"
- User wants the prompt library icon to appear on a new AI chat interface
- User asks about platform compatibility or extending platform support
Overview
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
- Each platform has a strategy class extending
PlatformStrategy
- All platforms are registered in
src/config/platforms.ts
- Platform detection, injection, and insertion are handled automatically
Prerequisites
Before starting, ensure you have:
- The target platform URL (e.g.,
gemini.google.com)
- Browser DevTools open on the target platform
- Understanding of the input element type (textarea vs contenteditable)
- Preferred Tailwind classes for the platform badge styling (optional but recommended)
10-Step Integration Workflow
Step 1: Inspect the Target Platform
Action: Open browser DevTools on the target platform and identify:
document.querySelectorAll('textarea[placeholder*="message"]')
document.querySelectorAll('div[contenteditable="true"]')
document.querySelectorAll('[role="textbox"]')
What to find:
- CSS selector for the text input element
- Container where the icon should be placed
- Input element type (textarea, contenteditable, or custom)
Common patterns:
- ChatGPT:
textarea[data-testid="chat-input"]
- Claude:
div[contenteditable="true"][role="textbox"]
- Perplexity:
textarea[placeholder*="Ask"]
Step 2: Add Platform Configuration
File: src/config/platforms.ts
Action: Add a new entry to the SUPPORTED_PLATFORMS object:
export const SUPPORTED_PLATFORMS: Record<string, PlatformDefinition> = {
yourplatform: {
id: 'yourplatform',
hostname: 'your-ai-platform.com',
displayName: 'Your AI Platform',
priority: 70,
defaultEnabled: true,
selectors: [
'textarea[data-testid="chat-input"]',
'div[contenteditable="true"][role="textbox"]',
'[role="textbox"]'
],
buttonContainerSelector: '.toolbar-container',
strategyClass: 'YourPlatformStrategy',
hostnamePatterns: ['yourplatform', 'your-ai'],
brandColors: {
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:
- 100: Premium platforms (Claude, ChatGPT)
- 80-90: Major platforms (Perplexity, Mistral, Gemini)
- 60-79: Secondary platforms (new additions)
- < 60: Experimental/beta platforms
Step 2.1: Update SiteCard UI (Optional)
Add or update the platform card shown in settings so users can enable/disable the integration.
When to do this:
- You're adding a core platform (not just a custom site).
- You want the new platform visible in the Settings UI.
File: src/components/settings/SiteCard.tsx
Actions:
- Add an icon component in
src/components/icons/SiteIcons.tsx (or reuse an existing one).
- Update
src/components/settings/SiteCardList.tsx (or wherever cards are listed) to include the new platform with the right icon.
- Make sure the new card passes the correct hostname and uses
getBrandColors(hostname).
- Verify the badge colors/labels look correct; adjust
brandColors if needed.
Tip: The label for the platform name comes from displayName in SUPPORTED_PLATFORMS. Keep them in sync.
Step 3: Create the Platform Strategy
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);
}
canHandle(_element: HTMLElement): boolean {
return this.hostname === getPlatformById('yourplatform')?.hostname;
}
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
try {
element.focus();
element.click();
await new Promise(resolve => setTimeout(resolve, 50));
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' };
}
if (element.contentEditable === 'true') {
element.textContent = '';
const inserted = document.execCommand('insertText', false, content);
if (inserted) {
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 };
}
}
getSelectors(): string[] {
return this.config?.selectors || [];
}
createIcon(uiFactory: UIElementFactory): HTMLElement | null {
return uiFactory.createFloatingIcon();
}
}
Reference Example: See src/content/platforms/perplexity-strategy.ts for a complete implementation with multiple fallback methods.
Step 4: Register the Strategy
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:
import { YourPlatformStrategy } from './yourplatform-strategy';
constructor(options: PlatformManagerOptions = {}) {
this.strategies = [
new ClaudeStrategy(),
new ChatGPTStrategy(),
new PerplexityStrategy(),
new MistralStrategy(),
new GeminiStrategy(),
new YourPlatformStrategy(),
new DefaultStrategy()
];
}
Step 5: Update Manifest Permissions (if needed)
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.
Step 6: Create Unit Tests
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);
});
});
});
Step 7: Test Locally
Action: Build and load the extension:
npm run build
Manual Testing Checklist:
Debug Mode:
localStorage.setItem('prompt-library-debug', 'true');
console.log(window.__promptLibraryDebug?.strategies);
console.log(window.__promptLibraryDebug?.activeStrategy);
console.log(window.__promptLibraryDebug?.foundElements);
Step 8: Run Automated Tests
Action: Ensure all tests pass:
npm test
npm test -- yourplatform-strategy.test.ts
npm run test:coverage
CRITICAL: Both tests and linting MUST pass before proceeding:
npm run lint
npm run lint:fix
Step 9: Create Platform Icon (Optional)
When to create a custom icon:
- Platform has unique UI styling that requires custom integration
- You want to match the platform's native button design
- The generic floating icon doesn't fit the platform's visual language
When to use the generic icon:
- Platform has simple UI without specific styling requirements
- You want quick integration (just call
uiFactory.createFloatingIcon())
- Platform uses standard button patterns
Option A: Custom Platform Icon (Recommended for Major Platforms)
File: src/content/ui/element-factory.ts
Action: Add a custom icon creation method following the modern pattern:
createYourPlatformIcon(): HTMLElement {
const icon = document.createElement('button');
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';
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');
const svg = createSVGElement('svg', {
xmlns: 'http://www.w3.org/2000/svg',
width: '18',
height: '18',
viewBox: '0 0 24 24',
fill: 'currentColor',
'aria-hidden': 'true',
style: 'flex-shrink: 0;'
});
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);
const textElement = createElement('span', {
class: 'text-sm font-medium whitespace-nowrap'
});
textElement.textContent = 'My Prompts';
icon.appendChild(textElement);
return icon;
}
Key Patterns to Follow:
- Always include
prompt-library-integrated-icon class - Required for E2E test selectors
- Use Tailwind classes - No custom CSS needed, all styling via Tailwind utilities
- Standard 18x18 SVG sizing - Consistent across all platforms
- Use
createElement and createSVGElement - Secure DOM construction (never innerHTML)
- Include dark mode classes - Use
dark: variants for all styles
- Text label optional - Include for button-style integrations (Claude, Perplexity, Mistral, Copilot)
Reference Examples:
- Button with text: See
createClaudeIcon(), createPerplexityIcon(), createCopilotIcon()
- Icon only: See
createChatGPTIcon(), createGeminiIcon()
- Generic floating: See
createFloatingIcon()
Option B: React Icon Component (for Settings UI)
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>
);
};
Step 10: Update Documentation
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
Common Pitfalls and Solutions
Pitfall 1: Button Doesn't Appear
Symptoms: Icon doesn't show up on the platform
Solutions:
- Check
canHandle() returns true for your domain
- Verify selectors match actual DOM elements using DevTools
- Ensure strategy is registered in
platform-manager.ts
- Check browser console for injection errors
- Verify hostname in
platforms.ts matches exactly
Debug:
document.querySelectorAll('your-selector-here')
window.__promptLibraryDebug?.strategies
Pitfall 2: Button Appears in Wrong Location
Symptoms: Icon is visible but poorly positioned
Solutions:
- Update
buttonContainerSelector in platforms.ts
- Add platform-specific positioning in
src/content/core/injector.ts
- Use multiple container selectors (comma-separated) for fallbacks
Example:
buttonContainerSelector: [
'.primary-container',
'.fallback-container',
'.generic-container'
].join(', ')
Pitfall 3: Content Doesn't Insert
Symptoms: Icon works, but prompt doesn't appear in input
Solutions:
- Verify correct element type (textarea vs contenteditable)
- Try multiple insertion methods (execCommand, textContent, value)
- Add comprehensive event triggering
- Wait for platform initialization (50-100ms delay)
Pattern - Multiple Fallback Methods:
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
const execResult = await this._tryExecCommand(element, content);
if (execResult.success) return execResult;
const directResult = this._tryDirectAssignment(element, content);
if (directResult.success) return directResult;
return this._tryDOMManipulation(element, content);
}
Pitfall 4: Platform Doesn't Recognize Content
Symptoms: Text appears but submit button stays disabled
Solutions:
- Add platform-specific events (input, change, keyup)
- Trigger composition events (compositionstart, compositionend)
- Set focus properly before insertion
- Check if platform uses React/Vue (may need special handling)
Comprehensive Event Triggering:
private _triggerEvents(element: HTMLElement, content: string): void {
element.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: content
}));
const events = ['change', 'keyup', 'compositionend', 'blur', 'focus'];
events.forEach(eventType => {
element.dispatchEvent(new Event(eventType, { bubbles: true }));
});
}
Pitfall 5: Tests Fail After Implementation
Symptoms: Manual testing works but automated tests fail
Solutions:
- Ensure DOM elements are properly created in tests
- Mock
getPlatformById() if needed
- Add elements to
document.body before testing
- Clean up DOM elements after each test
Proper Test Setup:
describe('insert', () => {
it('inserts into textarea', async () => {
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
const result = await strategy.insert(textarea, 'Test');
expect(result.success).toBe(true);
document.body.removeChild(textarea);
});
});
Advanced Patterns
Pattern 1: React-Based Platforms
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 }));
}
}
Pattern 2: Shadow DOM Elements
For platforms using Shadow DOM:
getSelectors(): string[] {
const shadowHost = document.querySelector('your-shadow-host');
if (shadowHost?.shadowRoot) {
const shadowInput = shadowHost.shadowRoot.querySelector('textarea');
if (shadowInput) {
return [];
}
}
return this.config?.selectors || [];
}
Pattern 3: Dynamic Input Elements
For platforms that create inputs dynamically:
async insert(element: HTMLElement, content: string): Promise<InsertionResult> {
await this._waitForReady(element);
}
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 });
}
});
}
Quick Reference Checklist
Use this checklist when adding a new platform:
Related Documentation
- Architecture Deep Dive:
docs/ARCHITECTURE.md
- Platform Integration Guide:
docs/PLATFORM_INTEGRATION.md (comprehensive examples)
- Testing Guide:
docs/TESTING.md
- Component Catalog:
docs/COMPONENTS.md
Success Criteria
Your platform integration is complete when:
- Icon appears on the target platform without errors
- Icon is positioned correctly and doesn't interfere with UI
- Clicking icon opens the prompt selector
- Selected prompts insert correctly into the input field
- Platform recognizes the inserted content (submit button enabled)
- All automated tests pass
- ESLint shows no errors or warnings
- Works in both light and dark mode
- No console errors or warnings
- Code follows existing patterns and style
Last Updated: 2025-11-10
Skill Version: 1.1.0